Skip to content

Commit 8325080

Browse files
feat: Fire Identify if Provided Email in Rokt SelectPlacements (#578)
1 parent de96056 commit 8325080

3 files changed

Lines changed: 269 additions & 1 deletion

File tree

android-kit-base/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ android {
4444
tasks.withType(Test).configureEach {
4545
jvmArgs('--add-opens=java.base/java.lang=ALL-UNNAMED')
4646
jvmArgs("--add-opens=java.base/java.util=ALL-UNNAMED")
47+
jvmArgs("--add-opens=java.base/java.lang.reflect=ALL-UNNAMED")
4748
jvmArgs('--add-opens=java.base/java.util.concurrent=ALL-UNNAMED')
4849
}
4950

android-kit-base/src/main/java/com/mparticle/kits/KitManagerImpl.java

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.mparticle.kits;
22

3+
import static android.util.Log.e;
4+
35
import android.app.Activity;
46
import android.content.Context;
57
import android.content.Intent;
@@ -10,6 +12,7 @@
1012
import android.os.Handler;
1113
import android.os.HandlerThread;
1214
import android.os.Looper;
15+
import android.util.Log;
1316

1417
import androidx.annotation.MainThread;
1518
import androidx.annotation.NonNull;
@@ -23,12 +26,18 @@
2326
import com.mparticle.MPEvent;
2427
import com.mparticle.MParticle;
2528
import com.mparticle.MParticleOptions;
29+
import com.mparticle.MParticleTask;
2630
import com.mparticle.UserAttributeListener;
2731
import com.mparticle.commerce.CommerceEvent;
2832
import com.mparticle.consent.ConsentState;
33+
import com.mparticle.identity.IdentityApi;
2934
import com.mparticle.identity.IdentityApiRequest;
35+
import com.mparticle.identity.IdentityApiResult;
36+
import com.mparticle.identity.IdentityHttpResponse;
3037
import com.mparticle.identity.IdentityStateListener;
3138
import com.mparticle.identity.MParticleUser;
39+
import com.mparticle.identity.TaskFailureListener;
40+
import com.mparticle.identity.TaskSuccessListener;
3241
import com.mparticle.internal.CoreCallbacks;
3342
import com.mparticle.internal.KitManager;
3443
import com.mparticle.internal.KitsLoadedCallback;
@@ -54,6 +63,7 @@
5463
import java.util.Set;
5564
import java.util.TreeMap;
5665
import java.util.concurrent.ConcurrentHashMap;
66+
import java.util.function.Consumer;
5767

5868
public class KitManagerImpl implements KitManager, AttributionListener, UserAttributeListener, IdentityStateListener {
5969

@@ -1330,8 +1340,12 @@ public void execute(String viewName,
13301340
for (KitIntegration provider : providers.values()) {
13311341
try {
13321342
if (provider instanceof KitIntegration.RoktListener && !provider.isDisabled()) {
1333-
MParticleUser user = MParticle.getInstance().Identity().getCurrentUser();
1343+
MParticle instance = MParticle.getInstance();
1344+
MParticleUser user = instance.Identity().getCurrentUser();
1345+
String email = attributes != null ? attributes.get("email") : null;
1346+
confirmEmail(email,user,instance.Identity(), () -> {
13341347
JSONArray jsonArray = new JSONArray();
1348+
13351349
KitConfiguration kitConfig = provider.getConfiguration();
13361350
if (kitConfig != null) {
13371351
try {
@@ -1361,13 +1375,61 @@ public void execute(String viewName,
13611375
placeHolders,
13621376
fontTypefaces,
13631377
FilteredMParticleUser.getInstance(user.getId(), provider));
1378+
});
13641379
}
13651380
} catch (Exception e) {
13661381
Logger.warning("Failed to call execute for kit: " + provider.getName() + ": " + e.getMessage());
13671382
}
13681383
}
13691384
}
13701385

1386+
private void confirmEmail(
1387+
@Nullable String email,
1388+
@Nullable MParticleUser user,
1389+
IdentityApi identityApi,
1390+
Runnable runnable
1391+
) {
1392+
if (email != null && user != null) {
1393+
String existingEmail = user.getUserIdentities().get(MParticle.IdentityType.Email);
1394+
1395+
if (!email.equals(existingEmail)) {
1396+
// If there's an existing email but it doesn't match the passed-in email, log a warning
1397+
if (existingEmail != null) {
1398+
Logger.warning( String.format(
1399+
"The existing email on the user (%s) does not match the email passed to selectPlacements (%s). " +
1400+
"Please make sure to sync the email identity to mParticle as soon as it's available. " +
1401+
"Identifying user with the provided email before continuing to selectPlacements.",
1402+
existingEmail, email
1403+
));
1404+
}
1405+
1406+
IdentityApiRequest identityRequest = IdentityApiRequest.withUser(user)
1407+
.email(email)
1408+
.build();
1409+
MParticleTask<IdentityApiResult> task = identityApi.identify(identityRequest);
1410+
task.addFailureListener(new TaskFailureListener() {
1411+
@Override
1412+
public void onFailure(IdentityHttpResponse result) {
1413+
Logger.error( "Failed to sync email from selectPlacement to user: " + result.getErrors().toString());
1414+
1415+
runnable.run();
1416+
}
1417+
});
1418+
task.addSuccessListener(new TaskSuccessListener() {
1419+
@Override
1420+
public void onSuccess(IdentityApiResult result) {
1421+
Logger.debug("Updated email identity based on selectPlacement's attributes: " + result.getUser().getUserIdentities().get(MParticle.IdentityType.Email));
1422+
runnable.run();
1423+
}
1424+
});
1425+
} else {
1426+
runnable.run();
1427+
}
1428+
} else {
1429+
runnable.run();
1430+
}
1431+
}
1432+
13711433
public void runOnKitThread(Runnable runnable) {
13721434
if (mKitHandler == null) {
13731435
mKitHandler = new Handler(kitHandlerThread.getLooper());

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

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,19 @@ package com.mparticle.kits
22

33
import android.content.Context
44
import android.graphics.Typeface
5+
import android.os.Looper
6+
import android.os.SystemClock
57
import com.mparticle.BaseEvent
68
import com.mparticle.MPEvent
79
import com.mparticle.MParticle
810
import com.mparticle.MParticleOptions
11+
import com.mparticle.MParticleTask
912
import com.mparticle.commerce.CommerceEvent
1013
import com.mparticle.commerce.Product
1114
import com.mparticle.consent.ConsentState
1215
import com.mparticle.consent.GDPRConsent
1316
import com.mparticle.identity.IdentityApi
17+
import com.mparticle.identity.IdentityApiResult
1418
import com.mparticle.identity.MParticleUser
1519
import com.mparticle.internal.CoreCallbacks
1620
import com.mparticle.internal.SideloadedKit
@@ -28,18 +32,31 @@ import org.json.JSONObject
2832
import org.junit.Assert
2933
import org.junit.Before
3034
import org.junit.Test
35+
import org.junit.runner.RunWith
36+
import org.mockito.ArgumentMatchers.any
3137
import org.mockito.Mockito
38+
import org.mockito.Mockito.mock
39+
import org.mockito.Mockito.verify
40+
import org.mockito.Mockito.`when`
41+
import org.powermock.api.mockito.PowerMockito
42+
import org.powermock.core.classloader.annotations.PrepareForTest
43+
import org.powermock.modules.junit4.PowerMockRunner
3244
import java.lang.ref.WeakReference
45+
import java.lang.reflect.Method
3346
import java.util.Arrays
3447
import java.util.LinkedList
3548
import java.util.concurrent.ConcurrentHashMap
3649

50+
@RunWith(PowerMockRunner::class)
51+
@PrepareForTest(Looper::class, SystemClock::class)
3752
class KitManagerImplTest {
3853
var mparticle: MParticle? = null
3954
var mockIdentity: IdentityApi? = null
4055

4156
@Before
4257
fun before() {
58+
PowerMockito.mockStatic(Looper::class.java)
59+
PowerMockito.mockStatic(SystemClock::class.java)
4360
mockIdentity = Mockito.mock(IdentityApi::class.java)
4461
val instance = MockMParticle()
4562
instance.setIdentityApi(mockIdentity)
@@ -1023,6 +1040,194 @@ class KitManagerImplTest {
10231040
Assert.assertEquals("US", attributes["country"])
10241041
}
10251042

1043+
@Test
1044+
fun testConfirmEmail_When_EmailSyncSuccess() {
1045+
var runnable: Runnable = Mockito.mock(Runnable::class.java)
1046+
var user: MParticleUser = Mockito.mock(MParticleUser::class.java)
1047+
val instance = MockMParticle()
1048+
val sideloadedKit = Mockito.mock(MPSideloadedKit::class.java)
1049+
val kitId = 6000000
1050+
1051+
val configJSONObj = JSONObject().apply {
1052+
put("id", kitId)
1053+
}
1054+
val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj)
1055+
Mockito.`when`(sideloadedKit.configuration).thenReturn(mockedKitConfig)
1056+
val identityApi = mock(IdentityApi::class.java)
1057+
val oldEmail = "old@example.com"
1058+
val mockTask = mock(MParticleTask::class.java) as MParticleTask<IdentityApiResult>
1059+
`when`(identityApi.identify(any())).thenReturn(mockTask)
1060+
val identities: MutableMap<MParticle.IdentityType, String> = HashMap()
1061+
identities.put(MParticle.IdentityType.Email, oldEmail)
1062+
`when`(user.userIdentities).thenReturn(identities)
1063+
instance.setIdentityApi(identityApi)
1064+
val settingsMap = hashMapOf(
1065+
"placementAttributesMapping" to """
1066+
[
1067+
1068+
]
1069+
""".trimIndent()
1070+
)
1071+
val field = KitConfiguration::class.java.getDeclaredField("settings")
1072+
field.isAccessible = true
1073+
field.set(mockedKitConfig, settingsMap)
1074+
1075+
val options = MParticleOptions.builder(MockContext())
1076+
.sideloadedKits(mutableListOf(sideloadedKit) as List<SideloadedKit>).build()
1077+
val manager: KitManagerImpl = MockKitManagerImpl(options)
1078+
val method: Method = KitManagerImpl::class.java.getDeclaredMethod(
1079+
"confirmEmail",
1080+
String::class.java,
1081+
MParticleUser::class.java,
1082+
IdentityApi::class.java,
1083+
Runnable::class.java
1084+
)
1085+
method.isAccessible = true
1086+
val result = method.invoke(manager, "Test@gmail.com", user, identityApi, runnable)
1087+
verify(mockTask).addSuccessListener(any())
1088+
}
1089+
1090+
@Test
1091+
fun testConfirmEmail_When_EmailAlreadySynced() {
1092+
var runnable: Runnable = Mockito.mock(Runnable::class.java)
1093+
var user: MParticleUser = Mockito.mock(MParticleUser::class.java)
1094+
val instance = MockMParticle()
1095+
val sideloadedKit = Mockito.mock(MPSideloadedKit::class.java)
1096+
val kitId = 6000000
1097+
1098+
val configJSONObj = JSONObject().apply {
1099+
put("id", kitId)
1100+
}
1101+
val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj)
1102+
Mockito.`when`(sideloadedKit.configuration).thenReturn(mockedKitConfig)
1103+
val identityApi = mock(IdentityApi::class.java)
1104+
val oldEmail = "Test@gmail.com"
1105+
val mockTask = mock(MParticleTask::class.java) as MParticleTask<IdentityApiResult>
1106+
`when`(identityApi.identify(any())).thenReturn(mockTask)
1107+
val identities: MutableMap<MParticle.IdentityType, String> = HashMap()
1108+
identities.put(MParticle.IdentityType.Email, oldEmail)
1109+
`when`(user.userIdentities).thenReturn(identities)
1110+
instance.setIdentityApi(identityApi)
1111+
val settingsMap = hashMapOf(
1112+
"placementAttributesMapping" to """
1113+
[
1114+
1115+
]
1116+
""".trimIndent()
1117+
)
1118+
val field = KitConfiguration::class.java.getDeclaredField("settings")
1119+
field.isAccessible = true
1120+
field.set(mockedKitConfig, settingsMap)
1121+
1122+
val options = MParticleOptions.builder(MockContext())
1123+
.sideloadedKits(mutableListOf(sideloadedKit) as List<SideloadedKit>).build()
1124+
val manager: KitManagerImpl = MockKitManagerImpl(options)
1125+
val method: Method = KitManagerImpl::class.java.getDeclaredMethod(
1126+
"confirmEmail",
1127+
String::class.java,
1128+
MParticleUser::class.java,
1129+
IdentityApi::class.java,
1130+
Runnable::class.java
1131+
)
1132+
method.isAccessible = true
1133+
val result = method.invoke(manager, "Test@gmail.com", user, identityApi, runnable)
1134+
Mockito.verify(runnable).run()
1135+
}
1136+
1137+
@Test
1138+
fun testConfirmEmail_When_mailIsNull() {
1139+
var runnable: Runnable = Mockito.mock(Runnable::class.java)
1140+
var user: MParticleUser = Mockito.mock(MParticleUser::class.java)
1141+
val instance = MockMParticle()
1142+
val sideloadedKit = Mockito.mock(MPSideloadedKit::class.java)
1143+
val kitId = 6000000
1144+
1145+
val configJSONObj = JSONObject().apply {
1146+
put("id", kitId)
1147+
}
1148+
val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj)
1149+
Mockito.`when`(sideloadedKit.configuration).thenReturn(mockedKitConfig)
1150+
val identityApi = mock(IdentityApi::class.java)
1151+
val oldEmail = "Test@gmail.com"
1152+
val mockTask = mock(MParticleTask::class.java) as MParticleTask<IdentityApiResult>
1153+
`when`(identityApi.identify(any())).thenReturn(mockTask)
1154+
val identities: MutableMap<MParticle.IdentityType, String> = HashMap()
1155+
identities.put(MParticle.IdentityType.Email, oldEmail)
1156+
`when`(user.userIdentities).thenReturn(identities)
1157+
instance.setIdentityApi(identityApi)
1158+
val settingsMap = hashMapOf(
1159+
"placementAttributesMapping" to """
1160+
[
1161+
1162+
]
1163+
""".trimIndent()
1164+
)
1165+
val field = KitConfiguration::class.java.getDeclaredField("settings")
1166+
field.isAccessible = true
1167+
field.set(mockedKitConfig, settingsMap)
1168+
1169+
val options = MParticleOptions.builder(MockContext())
1170+
.sideloadedKits(mutableListOf(sideloadedKit) as List<SideloadedKit>).build()
1171+
val manager: KitManagerImpl = MockKitManagerImpl(options)
1172+
val method: Method = KitManagerImpl::class.java.getDeclaredMethod(
1173+
"confirmEmail",
1174+
String::class.java,
1175+
MParticleUser::class.java,
1176+
IdentityApi::class.java,
1177+
Runnable::class.java
1178+
)
1179+
method.isAccessible = true
1180+
val result = method.invoke(manager, null, user, identityApi, runnable)
1181+
Mockito.verify(runnable).run()
1182+
}
1183+
1184+
@Test
1185+
fun testConfirmEmail_When_User_IsNull() {
1186+
var runnable: Runnable = Mockito.mock(Runnable::class.java)
1187+
var user: MParticleUser = Mockito.mock(MParticleUser::class.java)
1188+
val instance = MockMParticle()
1189+
val sideloadedKit = Mockito.mock(MPSideloadedKit::class.java)
1190+
val kitId = 6000000
1191+
1192+
val configJSONObj = JSONObject().apply {
1193+
put("id", kitId)
1194+
}
1195+
val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj)
1196+
Mockito.`when`(sideloadedKit.configuration).thenReturn(mockedKitConfig)
1197+
val identityApi = mock(IdentityApi::class.java)
1198+
val oldEmail = "Test@gmail.com"
1199+
val mockTask = mock(MParticleTask::class.java) as MParticleTask<IdentityApiResult>
1200+
`when`(identityApi.identify(any())).thenReturn(mockTask)
1201+
val identities: MutableMap<MParticle.IdentityType, String> = HashMap()
1202+
identities.put(MParticle.IdentityType.Email, oldEmail)
1203+
`when`(user.userIdentities).thenReturn(identities)
1204+
instance.setIdentityApi(identityApi)
1205+
val settingsMap = hashMapOf(
1206+
"placementAttributesMapping" to """
1207+
[
1208+
1209+
]
1210+
""".trimIndent()
1211+
)
1212+
val field = KitConfiguration::class.java.getDeclaredField("settings")
1213+
field.isAccessible = true
1214+
field.set(mockedKitConfig, settingsMap)
1215+
1216+
val options = MParticleOptions.builder(MockContext())
1217+
.sideloadedKits(mutableListOf(sideloadedKit) as List<SideloadedKit>).build()
1218+
val manager: KitManagerImpl = MockKitManagerImpl(options)
1219+
val method: Method = KitManagerImpl::class.java.getDeclaredMethod(
1220+
"confirmEmail",
1221+
String::class.java,
1222+
MParticleUser::class.java,
1223+
IdentityApi::class.java,
1224+
Runnable::class.java
1225+
)
1226+
method.isAccessible = true
1227+
val result = method.invoke(manager, null, user, identityApi, runnable)
1228+
Mockito.verify(runnable).run()
1229+
}
1230+
10261231
internal inner class mockProvider(val config: KitConfiguration) : KitIntegration(), KitIntegration.RoktListener {
10271232
override fun isDisabled(): Boolean = false
10281233
override fun getName(): String = "FakeProvider"

0 commit comments

Comments
 (0)