Skip to content

Commit a2a3343

Browse files
fix: Wait for User Attributes to persist before Rokt selectPlacements forwards to the kit
1 parent 6c2079e commit a2a3343

4 files changed

Lines changed: 131 additions & 29 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
## [5.80.0](https://github.com/mParticle/mparticle-android-sdk/compare/v5.79.2...v5.80.0) (2026-06-25)
22

3-
43
### Features
54

6-
* 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))
5+
- 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))
76

87
## Unreleased
98

9+
### Fixed
10+
11+
- 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.
12+
1013
### Added
1114

1215
- 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.

android-kit-base/src/main/kotlin/com/mparticle/kits/RoktKitApiImpl.kt

Lines changed: 58 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.graphics.Typeface
44
import com.mparticle.MParticle
55
import com.mparticle.MpRoktEventCallback
66
import com.mparticle.RoktEvent
7+
import com.mparticle.TypedUserAttributeListener
78
import com.mparticle.identity.IdentityApi
89
import com.mparticle.identity.IdentityApiRequest
910
import com.mparticle.identity.MParticleUser
@@ -50,17 +51,20 @@ internal class RoktKitApiImpl(private val roktListener: KitIntegration.RoktListe
5051
val kitConfig = kitIntegration.configuration
5152

5253
confirmEmail(email, hashedEmail, user, instance.Identity(), kitConfig) {
53-
val finalAttributes = prepareAttributes(mutableAttributes, user)
54-
roktListener.selectPlacements(
55-
viewName,
56-
finalAttributes,
57-
mpRoktEventCallback,
58-
placeHolders,
59-
fontTypefaces,
60-
FilteredMParticleUser.getInstance(user?.id ?: 0L, kitIntegration),
61-
config,
62-
options,
63-
)
54+
val finalAttributes = applyPlacementAttributeMapping(mutableAttributes)
55+
setRoktAttributesOnUser(finalAttributes, user) {
56+
ensureSandboxMode(finalAttributes)
57+
roktListener.selectPlacements(
58+
viewName,
59+
finalAttributes,
60+
mpRoktEventCallback,
61+
placeHolders,
62+
fontTypefaces,
63+
FilteredMParticleUser.getInstance(user?.id ?: 0L, kitIntegration),
64+
config,
65+
options,
66+
)
67+
}
6468
}
6569
} catch (e: Exception) {
6670
Logger.warning("Failed to call execute for Rokt Kit: ${e.message}")
@@ -115,16 +119,19 @@ internal class RoktKitApiImpl(private val roktListener: KitIntegration.RoktListe
115119
return
116120
}
117121
val user = instance.Identity().currentUser
118-
val email = mutableAttributes["email"]
122+
val email = getValueIgnoreCase(mutableAttributes, "email")
119123
val hashedEmail = getValueIgnoreCase(mutableAttributes, "emailsha256")
120124
val kitConfig = kitIntegration.configuration
121125

122126
confirmEmail(email, hashedEmail, user, instance.Identity(), kitConfig) {
123-
val finalAttributes = prepareAttributes(mutableAttributes, user)
124-
roktListener.enrichAttributes(
125-
finalAttributes,
126-
FilteredMParticleUser.getInstance(user?.id ?: 0L, kitIntegration),
127-
)
127+
val finalAttributes = applyPlacementAttributeMapping(mutableAttributes)
128+
setRoktAttributesOnUser(finalAttributes, user) {
129+
ensureSandboxMode(finalAttributes)
130+
roktListener.enrichAttributes(
131+
finalAttributes,
132+
FilteredMParticleUser.getInstance(user?.id ?: 0L, kitIntegration),
133+
)
134+
}
128135
}
129136
} catch (e: Exception) {
130137
Logger.warning("Failed to call prepareAttributesAsync for Rokt Kit: ${e.message}")
@@ -142,7 +149,7 @@ internal class RoktKitApiImpl(private val roktListener: KitIntegration.RoktListe
142149
return null
143150
}
144151

145-
private fun prepareAttributes(finalAttributes: MutableMap<String, String>, user: MParticleUser?): MutableMap<String, String> {
152+
private fun applyPlacementAttributeMapping(attributes: MutableMap<String, String>): MutableMap<String, String> {
146153
val kitConfig = kitIntegration.configuration
147154
val jsonArray = try {
148155
kitConfig?.placementAttributesMapping ?: org.json.JSONArray()
@@ -155,27 +162,51 @@ internal class RoktKitApiImpl(private val roktListener: KitIntegration.RoktListe
155162
val obj = jsonArray.optJSONObject(i) ?: continue
156163
val mapFrom = obj.optString("map")
157164
val mapTo = obj.optString("value")
158-
if (finalAttributes.containsKey(mapFrom)) {
159-
val value = finalAttributes.remove(mapFrom)
165+
if (attributes.containsKey(mapFrom)) {
166+
val value = attributes.remove(mapFrom)
160167
if (value != null) {
161-
finalAttributes[mapTo] = value
168+
attributes[mapTo] = value
162169
}
163170
}
164171
}
172+
return attributes
173+
}
165174

175+
private fun ensureSandboxMode(finalAttributes: MutableMap<String, String>) {
176+
if (!finalAttributes.containsKey(Constants.MessageKey.SANDBOX_MODE_ROKT)) {
177+
finalAttributes[Constants.MessageKey.SANDBOX_MODE_ROKT] =
178+
Objects.toString(MPUtility.isDevEnv(), "false")
179+
}
180+
}
181+
182+
/**
183+
* Persists placement attributes on the mParticle user, then invokes [onReady] after the
184+
* attributes have been applied. This ensures the Rokt kit reads an up-to-date user profile
185+
* when merging attributes for the placement.
186+
*/
187+
private fun setRoktAttributesOnUser(finalAttributes: Map<String, String>, user: MParticleUser?, onReady: Runnable) {
166188
val objectAttributes = mutableMapOf<String, Any>()
167189
for ((key, value) in finalAttributes) {
168190
if (key != Constants.MessageKey.SANDBOX_MODE_ROKT) {
169191
objectAttributes[key] = value
170192
}
171193
}
172-
user?.setUserAttributes(objectAttributes)
173-
174-
if (!finalAttributes.containsKey(Constants.MessageKey.SANDBOX_MODE_ROKT)) {
175-
finalAttributes[Constants.MessageKey.SANDBOX_MODE_ROKT] =
176-
Objects.toString(MPUtility.isDevEnv(), "false")
194+
if (user != null) {
195+
user.setUserAttributes(objectAttributes)
196+
user.getUserAttributes(
197+
object : TypedUserAttributeListener {
198+
override fun onUserAttributesReceived(
199+
userAttributes: Map<String, Any?>,
200+
userAttributeLists: Map<String, List<String?>?>,
201+
mpid: Long,
202+
) {
203+
onReady.run()
204+
}
205+
},
206+
)
207+
} else {
208+
onReady.run()
177209
}
178-
return finalAttributes
179210
}
180211

181212
private fun confirmEmail(

android-kit-base/src/test/kotlin/com/mparticle/kits/KitManagerImplTest.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import com.mparticle.MParticle
1010
import com.mparticle.MParticleOptions
1111
import com.mparticle.MpRoktEventCallback
1212
import com.mparticle.RoktEvent
13+
import com.mparticle.TypedUserAttributeListener
1314
import com.mparticle.WrapperSdk
1415
import com.mparticle.WrapperSdkVersion
1516
import com.mparticle.commerce.CommerceEvent
@@ -1277,6 +1278,7 @@ class KitManagerImplTest {
12771278
@Test
12781279
fun testRokt_selectPlacements_with_PlacementOptions() {
12791280
val mockUser = mock(MParticleUser::class.java)
1281+
stubUserAttributesCallback(mockUser)
12801282
`when`(mockIdentity!!.currentUser).thenReturn(mockUser)
12811283

12821284
val manager: KitManagerImpl = MockKitManagerImpl()
@@ -1315,6 +1317,7 @@ class KitManagerImplTest {
13151317
@Test
13161318
fun testRokt_selectPlacements_without_PlacementOptions() {
13171319
val mockUser = mock(MParticleUser::class.java)
1320+
stubUserAttributesCallback(mockUser)
13181321
`when`(mockIdentity!!.currentUser).thenReturn(mockUser)
13191322

13201323
val manager: KitManagerImpl = MockKitManagerImpl()
@@ -1760,6 +1763,14 @@ class KitManagerImplTest {
17601763
}
17611764
}
17621765

1766+
private fun stubUserAttributesCallback(user: MParticleUser) {
1767+
`when`(user.getUserAttributes(any())).thenAnswer { invocation ->
1768+
val listener = invocation.arguments[0] as TypedUserAttributeListener
1769+
listener.onUserAttributesReceived(emptyMap(), emptyMap(), 0L)
1770+
null
1771+
}
1772+
}
1773+
17631774
internal inner class MockProvider(
17641775
val config: KitConfiguration,
17651776
) : KitIntegration(),

android-kit-base/src/test/kotlin/com/mparticle/kits/RoktKitApiImplTest.kt

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.mparticle.kits
22

33
import com.mparticle.MParticle
44
import com.mparticle.identity.IdentityApi
5+
import com.mparticle.identity.MParticleUser
56
import com.mparticle.internal.MPUtility
67
import com.mparticle.mock.MockMParticle
78
import com.mparticle.rokt.PlacementOptions
@@ -15,6 +16,7 @@ import org.junit.Test
1516
import org.mockito.ArgumentCaptor
1617
import org.mockito.ArgumentMatchers.any
1718
import org.mockito.Mockito.mock
19+
import org.mockito.Mockito.never
1820
import org.mockito.Mockito.verify
1921
import org.mockito.Mockito.`when`
2022
import org.mockito.Mockito.withSettings
@@ -82,6 +84,61 @@ class RoktKitApiImplTest {
8284
assertEquals(MPUtility.isDevEnv().toString(), captured["sandbox"])
8385
}
8486

87+
@Test
88+
fun testSelectPlacements_waitsForUserAttributesBeforeDelegating() {
89+
val kitConfig = KitConfiguration.createKitConfiguration(JSONObject().put("id", 42))
90+
val kitIntegration =
91+
mock(
92+
KitIntegration::class.java,
93+
withSettings().extraInterfaces(KitIntegration.RoktListener::class.java),
94+
)
95+
`when`(kitIntegration.configuration).thenReturn(kitConfig)
96+
val roktListener = kitIntegration as KitIntegration.RoktListener
97+
val roktApi = RoktKitApiImpl(roktListener, kitIntegration)
98+
99+
val identityApi = mock(IdentityApi::class.java)
100+
val user = mock(MParticleUser::class.java)
101+
`when`(user.id).thenReturn(12345L)
102+
`when`(identityApi.currentUser).thenReturn(user)
103+
val instance = MockMParticle()
104+
instance.setIdentityApi(identityApi)
105+
MParticle.setInstance(instance)
106+
107+
var capturedListener: com.mparticle.TypedUserAttributeListener? = null
108+
`when`(user.getUserAttributes(any())).thenAnswer { invocation ->
109+
capturedListener = invocation.arguments[0] as com.mparticle.TypedUserAttributeListener
110+
null
111+
}
112+
113+
val attributes = mapOf("country" to "US")
114+
roktApi.selectPlacements("Test", attributes, null, null, null, null, null)
115+
116+
verify(user).setUserAttributes(any())
117+
verify(roktListener, never()).selectPlacements(
118+
any(),
119+
any(),
120+
any(),
121+
any(),
122+
any(),
123+
any(),
124+
any(),
125+
any(),
126+
)
127+
128+
capturedListener!!.onUserAttributesReceived(emptyMap(), emptyMap(), 12345L)
129+
130+
verify(roktListener).selectPlacements(
131+
any(),
132+
any(),
133+
any(),
134+
any(),
135+
any(),
136+
any(),
137+
any(),
138+
any(),
139+
)
140+
}
141+
85142
@Test
86143
fun testSelectPlacements_passesPlacementOptions() {
87144
val kitConfig = KitConfiguration.createKitConfiguration(JSONObject().put("id", 42))

0 commit comments

Comments
 (0)