From 4a3a7f807b220b3746879fd0b5bb237e03759418 Mon Sep 17 00:00:00 2001 From: Thomson Thomas Date: Thu, 15 Jan 2026 17:13:42 -0500 Subject: [PATCH 1/4] feat: Add PlacementOptions support to Rokt integration - Add integration start timestamp to the `PlacementOptions` object and pass it down to RoktKit - Add unit tests --- .../internal/KitFrameworkWrapper.java | 22 ++++- .../com/mparticle/internal/KitManager.java | 9 ++ .../src/main/kotlin/com/mparticle/Rokt.kt | 12 ++- .../com/mparticle/rokt/PlacementOptions.kt | 3 + .../src/test/kotlin/com/mparticle/RoktTest.kt | 36 +++++++- .../com/mparticle/kits/KitIntegration.java | 10 +++ .../com/mparticle/kits/KitManagerImpl.java | 15 +++- .../com/mparticle/kits/KitManagerImplTest.kt | 87 +++++++++++++++++++ 8 files changed, 188 insertions(+), 6 deletions(-) create mode 100644 android-core/src/main/kotlin/com/mparticle/rokt/PlacementOptions.kt diff --git a/android-core/src/main/java/com/mparticle/internal/KitFrameworkWrapper.java b/android-core/src/main/java/com/mparticle/internal/KitFrameworkWrapper.java index f6d7bc933..56d710e6f 100644 --- a/android-core/src/main/java/com/mparticle/internal/KitFrameworkWrapper.java +++ b/android-core/src/main/java/com/mparticle/internal/KitFrameworkWrapper.java @@ -26,6 +26,7 @@ import com.mparticle.identity.IdentityApiRequest; import com.mparticle.identity.MParticleUser; import com.mparticle.internal.listeners.InternalListenerManager; +import com.mparticle.rokt.PlacementOptions; import com.mparticle.rokt.RoktConfig; import com.mparticle.rokt.RoktEmbeddedView; import com.mparticle.rokt.RoktOptions; @@ -685,6 +686,25 @@ public void execute(@NonNull String viewName, } } + @Override + public void execute(@NonNull String identifier, + @NonNull Map attributes, + @Nullable MpRoktEventCallback mpRoktEventCallback, + @Nullable Map> embeddedViews, + @Nullable Map> fontTypefaces, + @Nullable RoktConfig config, + @Nullable PlacementOptions options) { + if (mKitManager != null) { + mKitManager.execute(identifier, + attributes, + mpRoktEventCallback, + embeddedViews, + fontTypefaces, + config, + options); + } + } + @Override public Flow events(@NonNull String identifier) { if (mKitManager != null) { @@ -852,4 +872,4 @@ public void onKitApiCalled(String methodName, int kitId, Boolean used, Object... } }; } -} \ No newline at end of file +} diff --git a/android-core/src/main/java/com/mparticle/internal/KitManager.java b/android-core/src/main/java/com/mparticle/internal/KitManager.java index ad5c9808d..260a4aeac 100644 --- a/android-core/src/main/java/com/mparticle/internal/KitManager.java +++ b/android-core/src/main/java/com/mparticle/internal/KitManager.java @@ -22,6 +22,7 @@ import com.mparticle.consent.ConsentState; import com.mparticle.identity.IdentityApiRequest; import com.mparticle.identity.MParticleUser; +import com.mparticle.rokt.PlacementOptions; import com.mparticle.rokt.RoktConfig; import com.mparticle.rokt.RoktEmbeddedView; import com.mparticle.rokt.RoktOptions; @@ -140,6 +141,14 @@ void execute(@NonNull String identifier, @Nullable Map> fontTypefaces, @Nullable RoktConfig config); + void execute(@NonNull String identifier, + @NonNull Map attributes, + @Nullable MpRoktEventCallback mpRoktEventCallback, + @Nullable Map> embeddedViews, + @Nullable Map> fontTypefaces, + @Nullable RoktConfig config, + @Nullable PlacementOptions options); + Flow events(@NonNull String identifier); void setWrapperSdkVersion(@NonNull WrapperSdkVersion wrapperSdkVersion); diff --git a/android-core/src/main/kotlin/com/mparticle/Rokt.kt b/android-core/src/main/kotlin/com/mparticle/Rokt.kt index 6b095b5e0..ed93a6f19 100644 --- a/android-core/src/main/kotlin/com/mparticle/Rokt.kt +++ b/android-core/src/main/kotlin/com/mparticle/Rokt.kt @@ -4,6 +4,7 @@ import android.graphics.Typeface import com.mparticle.internal.ConfigManager import com.mparticle.internal.KitManager import com.mparticle.internal.listeners.ApiClass +import com.mparticle.rokt.PlacementOptions import com.mparticle.rokt.RoktConfig import com.mparticle.rokt.RoktEmbeddedView import kotlinx.coroutines.flow.Flow @@ -12,6 +13,11 @@ import java.lang.ref.WeakReference @ApiClass class Rokt internal constructor(private val mConfigManager: ConfigManager, private val mKitManager: KitManager) { + + companion object { + const val JOINT_SDK_SELECT_PLACEMENTS = "jointSdkSelectPlacements" + } + @JvmOverloads fun selectPlacements( identifier: String, @@ -22,7 +28,7 @@ class Rokt internal constructor(private val mConfigManager: ConfigManager, priva config: RoktConfig? = null, ) { if (mConfigManager.isEnabled) { - mKitManager.execute(identifier, HashMap(attributes), callbacks, embeddedViews, fontTypefaces, config) + mKitManager.execute(identifier, HashMap(attributes), callbacks, embeddedViews, fontTypefaces, config, buildPlacementOptions()) } } @@ -70,4 +76,8 @@ class Rokt internal constructor(private val mConfigManager: ConfigManager, priva } else { null } + + private fun buildPlacementOptions(): PlacementOptions = PlacementOptions( + performanceMarkers = mutableMapOf(JOINT_SDK_SELECT_PLACEMENTS to System.currentTimeMillis()), + ) } diff --git a/android-core/src/main/kotlin/com/mparticle/rokt/PlacementOptions.kt b/android-core/src/main/kotlin/com/mparticle/rokt/PlacementOptions.kt new file mode 100644 index 000000000..cfe58b1e2 --- /dev/null +++ b/android-core/src/main/kotlin/com/mparticle/rokt/PlacementOptions.kt @@ -0,0 +1,3 @@ +package com.mparticle.rokt + +data class PlacementOptions(val performanceMarkers: MutableMap) diff --git a/android-core/src/test/kotlin/com/mparticle/RoktTest.kt b/android-core/src/test/kotlin/com/mparticle/RoktTest.kt index aa2878fa6..d27850a13 100644 --- a/android-core/src/test/kotlin/com/mparticle/RoktTest.kt +++ b/android-core/src/test/kotlin/com/mparticle/RoktTest.kt @@ -5,6 +5,7 @@ import android.os.Looper import android.os.SystemClock import com.mparticle.internal.ConfigManager import com.mparticle.internal.KitManager +import com.mparticle.rokt.PlacementOptions import com.mparticle.rokt.RoktConfig import com.mparticle.rokt.RoktEmbeddedView import kotlinx.coroutines.flow.Flow @@ -14,7 +15,10 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.eq +import org.mockito.ArgumentMatchers.isNull import org.mockito.Mock import org.mockito.Mockito.never import org.mockito.Mockito.verify @@ -82,7 +86,7 @@ class RoktTest { config = config, ) - verify(kitManager)?.execute("testView", attributes, callbacks, placeholders, fonts, config) + verify(kitManager)?.execute(eq("testView"), eq(attributes), eq(callbacks), eq(placeholders), eq(fonts), eq(config), any()) } @Test @@ -94,7 +98,7 @@ class RoktTest { rokt.selectPlacements(attributes = attributes, identifier = "basicView") - verify(kitManager).execute("basicView", attributes, null, null, null, null) + verify(kitManager).execute(eq("basicView"), eq(attributes), isNull(), isNull(), isNull(), isNull(), any()) } @Test @@ -106,7 +110,7 @@ class RoktTest { attributes = HashMap(), ) - verify(kitManager, never()).execute(any(), any(), any(), any(), any(), any()) + verify(kitManager, never()).execute(any(), any(), any(), any(), any(), any(), any()) } @Test @@ -201,4 +205,30 @@ class RoktTest { verify(kitManager, never()).getSessionId() assertNull(result) } + + @Test + fun testSelectPlacements_withOptions_whenEnabled() { + `when`(configManager.isEnabled).thenReturn(true) + val currentTimeMillis = System.currentTimeMillis() + + val attributes = mutableMapOf() + + rokt.selectPlacements( + identifier = "testView", + attributes = attributes, + ) + + // Verify call is forwarded + val optionsCaptor = ArgumentCaptor.forClass(PlacementOptions::class.java) + verify(kitManager).execute( + eq("testView"), + eq(HashMap(attributes)), + isNull(), + isNull(), + isNull(), + isNull(), + optionsCaptor.capture(), + ) + assertTrue(optionsCaptor.value.integrationStartTimestamp >= currentTimeMillis) + } } diff --git a/android-kit-base/src/main/java/com/mparticle/kits/KitIntegration.java b/android-kit-base/src/main/java/com/mparticle/kits/KitIntegration.java index a542373e1..04b37a8aa 100644 --- a/android-kit-base/src/main/java/com/mparticle/kits/KitIntegration.java +++ b/android-kit-base/src/main/java/com/mparticle/kits/KitIntegration.java @@ -21,6 +21,7 @@ import com.mparticle.commerce.CommerceEvent; import com.mparticle.consent.ConsentState; import com.mparticle.identity.MParticleUser; +import com.mparticle.rokt.PlacementOptions; import com.mparticle.rokt.RoktConfig; import com.mparticle.rokt.RoktEmbeddedView; @@ -626,6 +627,15 @@ void execute(@NonNull String viewName, @Nullable FilteredMParticleUser user, @Nullable RoktConfig config); + void execute(@NonNull String viewName, + @NonNull Map attributes, + @Nullable MpRoktEventCallback mpRoktEventCallback, + @Nullable Map> placeHolders, + @Nullable Map> fontTypefaces, + @Nullable FilteredMParticleUser user, + @Nullable RoktConfig config, + @Nullable PlacementOptions options); + Flow events(@NonNull String identifier); void enrichAttributes( diff --git a/android-kit-base/src/main/java/com/mparticle/kits/KitManagerImpl.java b/android-kit-base/src/main/java/com/mparticle/kits/KitManagerImpl.java index cb9922da0..30041502c 100644 --- a/android-kit-base/src/main/java/com/mparticle/kits/KitManagerImpl.java +++ b/android-kit-base/src/main/java/com/mparticle/kits/KitManagerImpl.java @@ -45,6 +45,7 @@ import com.mparticle.internal.MPUtility; import com.mparticle.internal.ReportingManager; import com.mparticle.kits.mappings.CustomMapping; +import com.mparticle.rokt.PlacementOptions; import com.mparticle.rokt.RoktConfig; import com.mparticle.rokt.RoktEmbeddedView; import com.mparticle.rokt.RoktOptions; @@ -1353,6 +1354,17 @@ public void execute(@NonNull String viewName, @Nullable Map> placeHolders, @Nullable Map> fontTypefaces, @Nullable RoktConfig config) { + execute(viewName, attributes, mpRoktEventCallback, placeHolders, fontTypefaces, config, null); + } + + @Override + public void execute(@NonNull String viewName, + @NonNull Map attributes, + @Nullable MpRoktEventCallback mpRoktEventCallback, + @Nullable Map> placeHolders, + @Nullable Map> fontTypefaces, + @Nullable RoktConfig config, + @Nullable PlacementOptions options) { for (KitIntegration provider : providers.values()) { try { if (provider instanceof KitIntegration.RoktListener && !provider.isDisabled()) { @@ -1374,7 +1386,8 @@ public void execute(@NonNull String viewName, placeHolders, fontTypefaces, FilteredMParticleUser.getInstance(user.getId(), provider), - config); + config, + options); }); } } catch (Exception e) { diff --git a/android-kit-base/src/test/kotlin/com/mparticle/kits/KitManagerImplTest.kt b/android-kit-base/src/test/kotlin/com/mparticle/kits/KitManagerImplTest.kt index 1f1251038..455859174 100644 --- a/android-kit-base/src/test/kotlin/com/mparticle/kits/KitManagerImplTest.kt +++ b/android-kit-base/src/test/kotlin/com/mparticle/kits/KitManagerImplTest.kt @@ -29,6 +29,7 @@ import com.mparticle.mock.MockContext import com.mparticle.mock.MockKitConfiguration import com.mparticle.mock.MockKitManagerImpl import com.mparticle.mock.MockMParticle +import com.mparticle.rokt.PlacementOptions import com.mparticle.rokt.RoktConfig import com.mparticle.rokt.RoktEmbeddedView import com.mparticle.testutils.TestingUtils @@ -43,10 +44,12 @@ import org.json.JSONException import org.json.JSONObject import org.junit.Assert import org.junit.Assert.assertNull +import org.junit.Assert.assertSame import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.any import org.mockito.Mockito import org.mockito.Mockito.mock @@ -1848,6 +1851,77 @@ class KitManagerImplTest { Assert.assertEquals("false", attributes["sandbox"]) } + @Test + fun testRokt_execute_with_PlacementOptions() { + val mockUser = mock(MParticleUser::class.java) + `when`(mockIdentity!!.currentUser).thenReturn(mockUser) + + val manager: KitManagerImpl = MockKitManagerImpl() + val roktListener = + mock( + KitIntegration::class.java, + withSettings().extraInterfaces(KitIntegration.RoktListener::class.java), + ) + `when`(roktListener.isDisabled).thenReturn(false) + manager.providers = + ConcurrentHashMap().apply { + put(1, roktListener) + } + + val attributes = hashMapOf() + val placementOptions = PlacementOptions(integrationStartTimestamp = 123L) + + manager.execute("Test", attributes, null, null, null, null, placementOptions) + + val optionsCaptor = ArgumentCaptor.forClass(PlacementOptions::class.java) + verify(roktListener as KitIntegration.RoktListener).execute( + any(), + any(), + any(), + any(), + any(), + any(), + any(), + optionsCaptor.capture(), + ) + assertSame(placementOptions, optionsCaptor.value) + } + + @Test + fun testRokt_execute_without_PlacementOptions() { + val mockUser = mock(MParticleUser::class.java) + `when`(mockIdentity!!.currentUser).thenReturn(mockUser) + + val manager: KitManagerImpl = MockKitManagerImpl() + val roktListener = + mock( + KitIntegration::class.java, + withSettings().extraInterfaces(KitIntegration.RoktListener::class.java), + ) + `when`(roktListener.isDisabled).thenReturn(false) + manager.providers = + ConcurrentHashMap().apply { + put(1, roktListener) + } + + val attributes = hashMapOf() + + manager.execute("Test", attributes, null, null, null, null) + + val optionsCaptor = ArgumentCaptor.forClass(PlacementOptions::class.java) + verify(roktListener as KitIntegration.RoktListener).execute( + any(), + any(), + any(), + any(), + any(), + any(), + any(), + optionsCaptor.capture(), + ) + assertNull(optionsCaptor.value) + } + @Test fun testRokt_SandboxMode_When_Environment_IS_Development() { val sideloadedKit = mock(MPSideloadedKit::class.java) @@ -2265,6 +2339,19 @@ class KitManagerImplTest { Logger.info("Executed with $attributes") } + override fun execute( + viewName: String, + attributes: MutableMap, + mpRoktEventCallback: MpRoktEventCallback?, + placeHolders: MutableMap>?, + fontTypefaces: MutableMap>?, + user: FilteredMParticleUser?, + config: RoktConfig?, + options: PlacementOptions?, + ) { + Logger.info("Executed with $attributes and options $options") + } + override fun events(identifier: String): Flow { Logger.info("events called with identfier: $identifier") return flowOf() From 88a682c9c7f6bca8e0433c609ac34869f0a7d3e2 Mon Sep 17 00:00:00 2001 From: Thomson Thomas Date: Mon, 2 Feb 2026 11:09:44 -0500 Subject: [PATCH 2/4] feat: Add PlacementOptions support to Rokt integration - Add integration start timestamp to the `PlacementOptions` object and pass it down to RoktKit - Add unit tests --- android-core/src/main/kotlin/com/mparticle/Rokt.kt | 6 +----- .../kotlin/com/mparticle/rokt/PlacementOptions.kt | 5 ++++- .../java/com/mparticle/kits/KitIntegration.java | 7 ------- .../com/mparticle/kits/KitManagerImplTest.kt | 14 +------------- 4 files changed, 6 insertions(+), 26 deletions(-) diff --git a/android-core/src/main/kotlin/com/mparticle/Rokt.kt b/android-core/src/main/kotlin/com/mparticle/Rokt.kt index ed93a6f19..200caa129 100644 --- a/android-core/src/main/kotlin/com/mparticle/Rokt.kt +++ b/android-core/src/main/kotlin/com/mparticle/Rokt.kt @@ -14,10 +14,6 @@ import java.lang.ref.WeakReference @ApiClass class Rokt internal constructor(private val mConfigManager: ConfigManager, private val mKitManager: KitManager) { - companion object { - const val JOINT_SDK_SELECT_PLACEMENTS = "jointSdkSelectPlacements" - } - @JvmOverloads fun selectPlacements( identifier: String, @@ -78,6 +74,6 @@ class Rokt internal constructor(private val mConfigManager: ConfigManager, priva } private fun buildPlacementOptions(): PlacementOptions = PlacementOptions( - performanceMarkers = mutableMapOf(JOINT_SDK_SELECT_PLACEMENTS to System.currentTimeMillis()), + jointSdkSelectPlacements = System.currentTimeMillis(), ) } diff --git a/android-core/src/main/kotlin/com/mparticle/rokt/PlacementOptions.kt b/android-core/src/main/kotlin/com/mparticle/rokt/PlacementOptions.kt index cfe58b1e2..9b3bfec7f 100644 --- a/android-core/src/main/kotlin/com/mparticle/rokt/PlacementOptions.kt +++ b/android-core/src/main/kotlin/com/mparticle/rokt/PlacementOptions.kt @@ -1,3 +1,6 @@ package com.mparticle.rokt -data class PlacementOptions(val performanceMarkers: MutableMap) +data class PlacementOptions( + val jointSdkSelectPlacements: Long, + val dynamicPerformanceMarkers: MutableMap = mutableMapOf(), +) diff --git a/android-kit-base/src/main/java/com/mparticle/kits/KitIntegration.java b/android-kit-base/src/main/java/com/mparticle/kits/KitIntegration.java index 04b37a8aa..a445b1b73 100644 --- a/android-kit-base/src/main/java/com/mparticle/kits/KitIntegration.java +++ b/android-kit-base/src/main/java/com/mparticle/kits/KitIntegration.java @@ -619,13 +619,6 @@ public interface BatchListener { } public interface RoktListener { - void execute(@NonNull String viewName, - @NonNull Map attributes, - @Nullable MpRoktEventCallback mpRoktEventCallback, - @Nullable Map> placeHolders, - @Nullable Map> fontTypefaces, - @Nullable FilteredMParticleUser user, - @Nullable RoktConfig config); void execute(@NonNull String viewName, @NonNull Map attributes, diff --git a/android-kit-base/src/test/kotlin/com/mparticle/kits/KitManagerImplTest.kt b/android-kit-base/src/test/kotlin/com/mparticle/kits/KitManagerImplTest.kt index 455859174..67a7d7404 100644 --- a/android-kit-base/src/test/kotlin/com/mparticle/kits/KitManagerImplTest.kt +++ b/android-kit-base/src/test/kotlin/com/mparticle/kits/KitManagerImplTest.kt @@ -1869,7 +1869,7 @@ class KitManagerImplTest { } val attributes = hashMapOf() - val placementOptions = PlacementOptions(integrationStartTimestamp = 123L) + val placementOptions = PlacementOptions(jointSdkSelectPlacements = 123L) manager.execute("Test", attributes, null, null, null, null, placementOptions) @@ -2327,18 +2327,6 @@ class KitManagerImplTest { override fun getConfiguration(): KitConfiguration = config - override fun execute( - viewName: String, - attributes: MutableMap, - mpRoktEventCallback: MpRoktEventCallback?, - placeHolders: MutableMap>?, - fontTypefaces: MutableMap>?, - user: FilteredMParticleUser?, - config: RoktConfig?, - ) { - Logger.info("Executed with $attributes") - } - override fun execute( viewName: String, attributes: MutableMap, From 241b1a6448b30ce05e6fdb099d165bd1795446c3 Mon Sep 17 00:00:00 2001 From: Thomson Thomas Date: Mon, 2 Feb 2026 13:40:46 -0500 Subject: [PATCH 3/4] refactor: Introduce RoktKitApi interface to reduce SDK-Kit coupling - Create RoktKitApi interface in android-core to abstract Rokt operations - Add getRoktKitApi() method to KitManager interface - Create RoktKitApiImpl wrapper in kit-base that handles user resolution - Update Rokt.kt to use the new interface-based approach - Update KitFrameworkWrapper to implement getRoktKitApi() --- .../internal/KitFrameworkWrapper.java | 88 +- .../com/mparticle/internal/KitManager.java | 47 +- .../src/main/kotlin/com/mparticle/Rokt.kt | 55 +- .../com/mparticle/internal/RoktKitApi.kt | 87 ++ .../src/test/kotlin/com/mparticle/RoktTest.kt | 91 +- .../internal/KitFrameworkWrapperTest.kt | 29 +- .../com/mparticle/kits/KitIntegration.java | 10 + .../com/mparticle/kits/KitManagerImpl.java | 280 +----- .../com/mparticle/kits/RoktKitApiImpl.kt | 266 ++++++ .../com/mparticle/kits/KitManagerImplTest.kt | 803 +++--------------- .../com/mparticle/kits/RoktKitApiImplTest.kt | 167 ++++ 11 files changed, 807 insertions(+), 1116 deletions(-) create mode 100644 android-core/src/main/kotlin/com/mparticle/internal/RoktKitApi.kt create mode 100644 android-kit-base/src/main/kotlin/com/mparticle/kits/RoktKitApiImpl.kt create mode 100644 android-kit-base/src/test/kotlin/com/mparticle/kits/RoktKitApiImplTest.kt diff --git a/android-core/src/main/java/com/mparticle/internal/KitFrameworkWrapper.java b/android-core/src/main/java/com/mparticle/internal/KitFrameworkWrapper.java index 56d710e6f..1065d4102 100644 --- a/android-core/src/main/java/com/mparticle/internal/KitFrameworkWrapper.java +++ b/android-core/src/main/java/com/mparticle/internal/KitFrameworkWrapper.java @@ -19,16 +19,11 @@ import com.mparticle.MPEvent; import com.mparticle.MParticle; import com.mparticle.MParticleOptions; -import com.mparticle.MpRoktEventCallback; -import com.mparticle.RoktEvent; import com.mparticle.WrapperSdkVersion; import com.mparticle.consent.ConsentState; import com.mparticle.identity.IdentityApiRequest; import com.mparticle.identity.MParticleUser; import com.mparticle.internal.listeners.InternalListenerManager; -import com.mparticle.rokt.PlacementOptions; -import com.mparticle.rokt.RoktConfig; -import com.mparticle.rokt.RoktEmbeddedView; import com.mparticle.rokt.RoktOptions; import org.json.JSONArray; @@ -44,8 +39,6 @@ import java.util.TreeMap; import java.util.concurrent.ConcurrentLinkedQueue; -import kotlinx.coroutines.flow.Flow; - public class KitFrameworkWrapper implements KitManager { private final Context mContext; final CoreCallbacks mCoreCallbacks; @@ -670,48 +663,12 @@ public void reset() { } @Override - public void execute(@NonNull String viewName, - @NonNull Map attributes, - @Nullable MpRoktEventCallback mpRoktEventCallback, - @Nullable Map> placeHolders, - @Nullable Map> fontTypefaces, - @Nullable RoktConfig config) { - if (mKitManager != null) { - mKitManager.execute(viewName, - attributes, - mpRoktEventCallback, - placeHolders, - fontTypefaces, - config); - } - } - - @Override - public void execute(@NonNull String identifier, - @NonNull Map attributes, - @Nullable MpRoktEventCallback mpRoktEventCallback, - @Nullable Map> embeddedViews, - @Nullable Map> fontTypefaces, - @Nullable RoktConfig config, - @Nullable PlacementOptions options) { - if (mKitManager != null) { - mKitManager.execute(identifier, - attributes, - mpRoktEventCallback, - embeddedViews, - fontTypefaces, - config, - options); - } - } - - @Override - public Flow events(@NonNull String identifier) { + @Nullable + public RoktKitApi getRoktKitApi() { if (mKitManager != null) { - return mKitManager.events(identifier); - } else { - return flowOf(); + return mKitManager.getRoktKitApi(); } + return null; } @Override @@ -721,43 +678,6 @@ public void setWrapperSdkVersion(@NonNull WrapperSdkVersion wrapperSdkVersion) { } } - @Override - public void purchaseFinalized(@NonNull String placementId, @NonNull String catalogItemId, boolean status) { - if (mKitManager != null) { - mKitManager.purchaseFinalized(placementId, catalogItemId, status); - } - } - - @Override - public void close() { - if (mKitManager != null) { - mKitManager.close(); - } - } - - @Override - public void prepareAttributesAsync(@NonNull Map attributes) { - if (mKitManager != null) { - mKitManager.prepareAttributesAsync(attributes); - } - } - - @Override - public void setSessionId(@NonNull String sessionId) { - if (mKitManager != null) { - mKitManager.setSessionId(sessionId); - } - } - - @Override - @Nullable - public String getSessionId() { - if (mKitManager != null) { - return mKitManager.getSessionId(); - } - return null; - } - static class CoreCallbacksImpl implements CoreCallbacks { KitFrameworkWrapper mKitFrameworkWrapper; ConfigManager mConfigManager; diff --git a/android-core/src/main/java/com/mparticle/internal/KitManager.java b/android-core/src/main/java/com/mparticle/internal/KitManager.java index 260a4aeac..b821bdd73 100644 --- a/android-core/src/main/java/com/mparticle/internal/KitManager.java +++ b/android-core/src/main/java/com/mparticle/internal/KitManager.java @@ -16,15 +16,10 @@ import com.mparticle.MPEvent; import com.mparticle.MParticle; import com.mparticle.MParticleOptions; -import com.mparticle.MpRoktEventCallback; -import com.mparticle.RoktEvent; import com.mparticle.WrapperSdkVersion; import com.mparticle.consent.ConsentState; import com.mparticle.identity.IdentityApiRequest; import com.mparticle.identity.MParticleUser; -import com.mparticle.rokt.PlacementOptions; -import com.mparticle.rokt.RoktConfig; -import com.mparticle.rokt.RoktEmbeddedView; import com.mparticle.rokt.RoktOptions; import org.json.JSONArray; @@ -34,8 +29,6 @@ import java.util.Map; import java.util.Set; -import kotlinx.coroutines.flow.Flow; - public interface KitManager { WeakReference getCurrentActivity(); @@ -134,49 +127,19 @@ public interface KitManager { void reset(); - void execute(@NonNull String identifier, - @NonNull Map attributes, - @Nullable MpRoktEventCallback mpRoktEventCallback, - @Nullable Map> embeddedViews, - @Nullable Map> fontTypefaces, - @Nullable RoktConfig config); - - void execute(@NonNull String identifier, - @NonNull Map attributes, - @Nullable MpRoktEventCallback mpRoktEventCallback, - @Nullable Map> embeddedViews, - @Nullable Map> fontTypefaces, - @Nullable RoktConfig config, - @Nullable PlacementOptions options); - - Flow events(@NonNull String identifier); - - void setWrapperSdkVersion(@NonNull WrapperSdkVersion wrapperSdkVersion); - - void purchaseFinalized(@NonNull String placementId, @NonNull String catalogItemId, boolean status); - - void close(); - - /** - * Set the session id to use for the next execute call. - * - * @param sessionId The session id to be set. Must be a non-empty string. - */ - void setSessionId(@NonNull String sessionId); - /** - * Get the session id to use within a non-native integration e.g. WebView. + * Get the RoktKitApi implementation if available. * - * @return The session id or null if no session is present. + * @return RoktKitApi instance or null if Rokt Kit is not configured or active */ @Nullable - String getSessionId(); + RoktKitApi getRoktKitApi(); - void prepareAttributesAsync(@NonNull Map attributes); + void setWrapperSdkVersion(@NonNull WrapperSdkVersion wrapperSdkVersion); enum KitStatus { NOT_CONFIGURED, STOPPED, ACTIVE } -} \ No newline at end of file +} diff --git a/android-core/src/main/kotlin/com/mparticle/Rokt.kt b/android-core/src/main/kotlin/com/mparticle/Rokt.kt index 200caa129..322169b36 100644 --- a/android-core/src/main/kotlin/com/mparticle/Rokt.kt +++ b/android-core/src/main/kotlin/com/mparticle/Rokt.kt @@ -3,6 +3,7 @@ package com.mparticle import android.graphics.Typeface import com.mparticle.internal.ConfigManager import com.mparticle.internal.KitManager +import com.mparticle.internal.Logger import com.mparticle.internal.listeners.ApiClass import com.mparticle.rokt.PlacementOptions import com.mparticle.rokt.RoktConfig @@ -14,6 +15,16 @@ import java.lang.ref.WeakReference @ApiClass class Rokt internal constructor(private val mConfigManager: ConfigManager, private val mKitManager: KitManager) { + /** + * Display a Rokt placement with the specified parameters. + * + * @param identifier The placement identifier + * @param attributes User attributes to pass to Rokt + * @param callbacks Optional callback for Rokt events + * @param embeddedViews Optional map of embedded view placeholders + * @param fontTypefaces Optional map of font typefaces + * @param config Optional Rokt configuration + */ @JvmOverloads fun selectPlacements( identifier: String, @@ -24,25 +35,46 @@ class Rokt internal constructor(private val mConfigManager: ConfigManager, priva config: RoktConfig? = null, ) { if (mConfigManager.isEnabled) { - mKitManager.execute(identifier, HashMap(attributes), callbacks, embeddedViews, fontTypefaces, config, buildPlacementOptions()) + val roktApi = mKitManager.roktKitApi + if (roktApi != null) { + roktApi.execute(identifier, HashMap(attributes), callbacks, embeddedViews, fontTypefaces, config, buildPlacementOptions()) + } else { + Logger.warning("Rokt Kit is not available. Make sure the Rokt Kit is included in your app.") + } } } + /** + * Get a Flow of Rokt events for the specified identifier. + * + * @param identifier The placement identifier to listen for events + * @return A Flow emitting RoktEvent objects + */ fun events(identifier: String): Flow = if (mConfigManager.isEnabled) { - mKitManager.events(identifier) + mKitManager.roktKitApi?.events(identifier) ?: flowOf() } else { flowOf() } + /** + * Notify Rokt that a purchase has been finalized. + * + * @param placementId The placement identifier + * @param catalogItemId The catalog item identifier + * @param status Whether the purchase was successful + */ fun purchaseFinalized(placementId: String, catalogItemId: String, status: Boolean) { if (mConfigManager.isEnabled) { - mKitManager.purchaseFinalized(placementId, catalogItemId, status) + mKitManager.roktKitApi?.purchaseFinalized(placementId, catalogItemId, status) } } + /** + * Close any active Rokt placements. + */ fun close() { if (mConfigManager.isEnabled) { - mKitManager.close() + mKitManager.roktKitApi?.close() } } @@ -58,7 +90,7 @@ class Rokt internal constructor(private val mConfigManager: ConfigManager, priva */ fun setSessionId(sessionId: String) { if (mConfigManager.isEnabled) { - mKitManager.setSessionId(sessionId) + mKitManager.roktKitApi?.setSessionId(sessionId) } } @@ -68,11 +100,22 @@ class Rokt internal constructor(private val mConfigManager: ConfigManager, priva * @return The session id or null if no session is present or SDK is not initialized. */ fun getSessionId(): String? = if (mConfigManager.isEnabled) { - mKitManager.getSessionId() + mKitManager.roktKitApi?.getSessionId() } else { null } + /** + * Prepare attributes asynchronously before executing a placement. + * + * @param attributes The attributes to prepare + */ + fun prepareAttributesAsync(attributes: Map) { + if (mConfigManager.isEnabled) { + mKitManager.roktKitApi?.prepareAttributesAsync(attributes) + } + } + private fun buildPlacementOptions(): PlacementOptions = PlacementOptions( jointSdkSelectPlacements = System.currentTimeMillis(), ) diff --git a/android-core/src/main/kotlin/com/mparticle/internal/RoktKitApi.kt b/android-core/src/main/kotlin/com/mparticle/internal/RoktKitApi.kt new file mode 100644 index 000000000..bea8acc04 --- /dev/null +++ b/android-core/src/main/kotlin/com/mparticle/internal/RoktKitApi.kt @@ -0,0 +1,87 @@ +package com.mparticle.internal + +import android.graphics.Typeface +import com.mparticle.MpRoktEventCallback +import com.mparticle.RoktEvent +import com.mparticle.rokt.PlacementOptions +import com.mparticle.rokt.RoktConfig +import com.mparticle.rokt.RoktEmbeddedView +import kotlinx.coroutines.flow.Flow +import java.lang.ref.WeakReference + +/** + * Interface for Rokt Kit operations. + * + * Implementations of this interface are provided by the Rokt Kit when it is + * configured and active. Use [KitManager.getRoktKitApi] to obtain an instance. + */ +interface RoktKitApi { + /** + * Execute a Rokt placement with the specified parameters. + * + * @param viewName The identifier for the placement view + * @param attributes User attributes to pass to Rokt + * @param mpRoktEventCallback Optional callback for Rokt events + * @param placeHolders Optional map of embedded view placeholders + * @param fontTypefaces Optional map of font typefaces + * @param config Optional Rokt configuration + * @param options Optional placement options + */ + fun execute( + viewName: String, + attributes: Map, + mpRoktEventCallback: MpRoktEventCallback?, + placeHolders: Map>?, + fontTypefaces: Map>?, + config: RoktConfig?, + options: PlacementOptions? = null, + ) + + /** + * Get a Flow of Rokt events for the specified identifier. + * + * @param identifier The placement identifier to listen for events + * @return A Flow emitting RoktEvent objects + */ + fun events(identifier: String): Flow + + /** + * Notify Rokt that a purchase has been finalized. + * + * @param placementId The placement identifier + * @param catalogItemId The catalog item identifier + * @param status Whether the purchase was successful + */ + fun purchaseFinalized(placementId: String, catalogItemId: String, status: Boolean) + + /** + * Close any active Rokt placements. + */ + fun close() + + /** + * Set the session id to use for the next execute call. + * + * This is useful for cases where you have a session id from a non-native integration, + * e.g. WebView, and you want the session to be consistent across integrations. + * + * **Note:** Empty strings are ignored and will not update the session. + * + * @param sessionId The session id to be set. Must be a non-empty string. + */ + fun setSessionId(sessionId: String) + + /** + * Get the session id to use within a non-native integration e.g. WebView. + * + * @return The session id or null if no session is present. + */ + fun getSessionId(): String? + + /** + * Prepare attributes asynchronously before executing a placement. + * + * @param attributes The attributes to prepare + */ + fun prepareAttributesAsync(attributes: Map) +} diff --git a/android-core/src/test/kotlin/com/mparticle/RoktTest.kt b/android-core/src/test/kotlin/com/mparticle/RoktTest.kt index d27850a13..5a5ad6c7c 100644 --- a/android-core/src/test/kotlin/com/mparticle/RoktTest.kt +++ b/android-core/src/test/kotlin/com/mparticle/RoktTest.kt @@ -5,6 +5,7 @@ import android.os.Looper import android.os.SystemClock import com.mparticle.internal.ConfigManager import com.mparticle.internal.KitManager +import com.mparticle.internal.RoktKitApi import com.mparticle.rokt.PlacementOptions import com.mparticle.rokt.RoktConfig import com.mparticle.rokt.RoktEmbeddedView @@ -16,8 +17,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor -import org.mockito.ArgumentMatchers.any -import org.mockito.ArgumentMatchers.eq +import org.mockito.ArgumentMatchers import org.mockito.ArgumentMatchers.isNull import org.mockito.Mock import org.mockito.Mockito.never @@ -37,10 +37,34 @@ class RoktTest { @Mock lateinit var kitManager: KitManager + @Mock + lateinit var roktKitApi: RoktKitApi + @Mock lateinit var configManager: ConfigManager private lateinit var rokt: Rokt + // Helpers to make Mockito matchers work in Kotlin with non-nullable types. + // Mockito matchers return null, which Kotlin rejects for non-nullable params. + // These helpers call the matcher (to register it) then return a cast null. + private fun capture(captor: ArgumentCaptor): T { + captor.capture() + @Suppress("UNCHECKED_CAST") + return null as T + } + + private fun any(): T { + ArgumentMatchers.any() + @Suppress("UNCHECKED_CAST") + return null as T + } + + private fun eq(value: T): T { + ArgumentMatchers.eq(value) + @Suppress("UNCHECKED_CAST") + return null as T + } + @Before fun setUp() { MockitoAnnotations.initMocks(this) @@ -50,6 +74,7 @@ class RoktTest { @Test fun testSelectPlacements_withFullParams_whenEnabled() { `when`(configManager.isEnabled).thenReturn(true) + `when`(kitManager.roktKitApi).thenReturn(roktKitApi) val attributes = mutableMapOf() attributes["key"] = "value" @@ -86,31 +111,49 @@ class RoktTest { config = config, ) - verify(kitManager)?.execute(eq("testView"), eq(attributes), eq(callbacks), eq(placeholders), eq(fonts), eq(config), any()) + verify(roktKitApi).execute( + eq("testView"), + eq(attributes), + any(), + any(), + any(), + any(), + any(), + ) } @Test fun testSelectPlacements_withBasicParams_whenEnabled() { `when`(configManager.isEnabled()).thenReturn(true) + `when`(kitManager.roktKitApi).thenReturn(roktKitApi) val attributes = mutableMapOf() attributes["a"] = "b" rokt.selectPlacements(attributes = attributes, identifier = "basicView") - verify(kitManager).execute(eq("basicView"), eq(attributes), isNull(), isNull(), isNull(), isNull(), any()) + verify(roktKitApi).execute( + eq("basicView"), + eq(attributes), + isNull(), + isNull(), + isNull(), + isNull(), + any(), + ) } @Test fun testSelectPlacements_withBasicParams_whenDisabled() { `when`(configManager.isEnabled()).thenReturn(false) + `when`(kitManager.roktKitApi).thenReturn(roktKitApi) rokt.selectPlacements( identifier = "basicView", attributes = HashMap(), ) - verify(kitManager, never()).execute(any(), any(), any(), any(), any(), any(), any()) + verify(roktKitApi, never()).execute(any(), any(), any(), any(), any(), any(), any()) } @Test @@ -128,47 +171,51 @@ class RoktTest { @Test fun testReportConversion_withBasicParams_whenEnabled() { `when`(configManager.isEnabled()).thenReturn(true) + `when`(kitManager.roktKitApi).thenReturn(roktKitApi) val attributes = mutableMapOf() attributes["a"] = "b" rokt.purchaseFinalized("132", "1111", true) - verify(kitManager).purchaseFinalized("132", "1111", true) + verify(roktKitApi).purchaseFinalized("132", "1111", true) } @Test fun testReportConversion_withBasicParams_whenDisabled() { `when`(configManager.isEnabled()).thenReturn(false) + `when`(kitManager.roktKitApi).thenReturn(roktKitApi) rokt.purchaseFinalized("132", "1111", true) - verify(kitManager, never()).purchaseFinalized("132", "1111", true) + verify(roktKitApi, never()).purchaseFinalized("132", "1111", true) } @Test fun testEvents_whenEnabled_delegatesToKitManager() { `when`(configManager.isEnabled).thenReturn(true) + `when`(kitManager.roktKitApi).thenReturn(roktKitApi) val testIdentifier = "test-identifier" val expectedFlow: Flow = flowOf() - `when`(kitManager.events(testIdentifier)).thenReturn(expectedFlow) + `when`(roktKitApi.events(testIdentifier)).thenReturn(expectedFlow) val result = rokt.events(testIdentifier) - verify(kitManager).events(testIdentifier) + verify(roktKitApi).events(testIdentifier) assertEquals(expectedFlow, result) } @Test fun testEvents_whenDisabled_returnsEmptyFlow() { `when`(configManager.isEnabled).thenReturn(false) + `when`(kitManager.roktKitApi).thenReturn(roktKitApi) val testIdentifier = "test-identifier" val result = rokt.events(testIdentifier) - verify(kitManager, never()).events(any()) + verify(roktKitApi, never()).events(any()) runTest { val elements = result.toList() assertTrue(elements.isEmpty()) @@ -178,37 +225,42 @@ class RoktTest { @Test fun testSetSessionId_whenEnabled_delegatesToKitManager() { `when`(configManager.isEnabled).thenReturn(true) + `when`(kitManager.roktKitApi).thenReturn(roktKitApi) rokt.setSessionId("test-session-id") - verify(kitManager).setSessionId("test-session-id") + verify(roktKitApi).setSessionId("test-session-id") } @Test fun testSetSessionId_whenDisabled_doesNotCallKitManager() { `when`(configManager.isEnabled).thenReturn(false) + `when`(kitManager.roktKitApi).thenReturn(roktKitApi) rokt.setSessionId("test-session-id") - verify(kitManager, never()).setSessionId(any()) + verify(roktKitApi, never()).setSessionId(any()) } @Test fun testGetSessionId_whenEnabled_delegatesToKitManager() { `when`(configManager.isEnabled).thenReturn(true) - `when`(kitManager.getSessionId()).thenReturn("expected-session-id") + `when`(kitManager.roktKitApi).thenReturn(roktKitApi) + `when`(roktKitApi.getSessionId()).thenReturn("expected-session-id") val result = rokt.getSessionId() - verify(kitManager).getSessionId() + verify(roktKitApi).getSessionId() assertEquals("expected-session-id", result) } @Test fun testGetSessionId_whenDisabled_returnsNull() { `when`(configManager.isEnabled).thenReturn(false) + `when`(kitManager.roktKitApi).thenReturn(roktKitApi) val result = rokt.getSessionId() - verify(kitManager, never()).getSessionId() + verify(roktKitApi, never()).getSessionId() assertNull(result) } @Test fun testSelectPlacements_withOptions_whenEnabled() { `when`(configManager.isEnabled).thenReturn(true) + `when`(kitManager.roktKitApi).thenReturn(roktKitApi) val currentTimeMillis = System.currentTimeMillis() val attributes = mutableMapOf() @@ -219,16 +271,17 @@ class RoktTest { ) // Verify call is forwarded + val viewNameCaptor = ArgumentCaptor.forClass(String::class.java) val optionsCaptor = ArgumentCaptor.forClass(PlacementOptions::class.java) - verify(kitManager).execute( + verify(roktKitApi).execute( eq("testView"), - eq(HashMap(attributes)), + any(), isNull(), isNull(), isNull(), isNull(), - optionsCaptor.capture(), + capture(optionsCaptor), ) - assertTrue(optionsCaptor.value.integrationStartTimestamp >= currentTimeMillis) + assertTrue(optionsCaptor.value.jointSdkSelectPlacements >= currentTimeMillis) } } diff --git a/android-core/src/test/kotlin/com/mparticle/internal/KitFrameworkWrapperTest.kt b/android-core/src/test/kotlin/com/mparticle/internal/KitFrameworkWrapperTest.kt index 6e5126bc7..f1b327c9d 100644 --- a/android-core/src/test/kotlin/com/mparticle/internal/KitFrameworkWrapperTest.kt +++ b/android-core/src/test/kotlin/com/mparticle/internal/KitFrameworkWrapperTest.kt @@ -8,16 +8,11 @@ import com.mparticle.MPEvent import com.mparticle.MParticle import com.mparticle.MParticleOptions import com.mparticle.MockMParticle -import com.mparticle.RoktEvent import com.mparticle.WrapperSdk import com.mparticle.WrapperSdkVersion import com.mparticle.commerce.CommerceEvent import com.mparticle.internal.PushRegistrationHelper.PushRegistration import com.mparticle.testutils.RandomUtils -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.test.runTest import org.json.JSONArray import org.junit.Assert import org.junit.Test @@ -34,7 +29,7 @@ import org.powermock.modules.junit4.PowerMockRunner import java.lang.ref.WeakReference import java.util.Random import kotlin.test.assertEquals -import kotlin.test.assertTrue +import kotlin.test.assertNull @RunWith(PowerMockRunner::class) class KitFrameworkWrapperTest { @@ -728,7 +723,7 @@ class KitFrameworkWrapperTest { } @Test - fun testEvents_kitManagerNull_returnsEmptyFlow() { + fun testGetRoktKitApi_kitManagerNull_returnsNull() { val wrapper = KitFrameworkWrapper( mock( @@ -741,16 +736,13 @@ class KitFrameworkWrapperTest { mock(MParticleOptions::class.java), ) - val result = wrapper.events("test-identifier") + val result = wrapper.roktKitApi - runTest { - val elements = result.toList() - assertTrue(elements.isEmpty()) - } + assertNull(result) } @Test - fun testEvents_kitManagerSet_delegatesToKitManager() { + fun testGetRoktKitApi_kitManagerSet_delegatesToKitManager() { val wrapper = KitFrameworkWrapper( mock( @@ -764,15 +756,14 @@ class KitFrameworkWrapperTest { ) val mockKitManager = mock(KitManager::class.java) - val expectedFlow: Flow = flowOf() - val testIdentifier = "test-identifier" + val mockRoktKitApi = mock(RoktKitApi::class.java) - `when`(mockKitManager.events(testIdentifier)).thenReturn(expectedFlow) + `when`(mockKitManager.roktKitApi).thenReturn(mockRoktKitApi) wrapper.setKitManager(mockKitManager) - val result = wrapper.events(testIdentifier) + val result = wrapper.roktKitApi - verify(mockKitManager).events(testIdentifier) - assertEquals(expectedFlow, result) + verify(mockKitManager).roktKitApi + assertEquals(mockRoktKitApi, result) } } diff --git a/android-kit-base/src/main/java/com/mparticle/kits/KitIntegration.java b/android-kit-base/src/main/java/com/mparticle/kits/KitIntegration.java index a445b1b73..a58ac8095 100644 --- a/android-kit-base/src/main/java/com/mparticle/kits/KitIntegration.java +++ b/android-kit-base/src/main/java/com/mparticle/kits/KitIntegration.java @@ -618,6 +618,16 @@ public interface BatchListener { List logBatch(JSONObject jsonObject); } + /** + * Interface for Rokt Kit implementations. + * + *

This interface is internal to kit-base and is bridged to the + * {@link com.mparticle.internal.RoktKitApi} interface via a wrapper implementation + * in {@link KitManagerImpl}. The wrapper handles user resolution and + * attribute preparation before delegating to the kit's methods.

+ * + * @see com.mparticle.internal.RoktKitApi + */ public interface RoktListener { void execute(@NonNull String viewName, diff --git a/android-kit-base/src/main/java/com/mparticle/kits/KitManagerImpl.java b/android-kit-base/src/main/java/com/mparticle/kits/KitManagerImpl.java index 30041502c..ef151ee9e 100644 --- a/android-kit-base/src/main/java/com/mparticle/kits/KitManagerImpl.java +++ b/android-kit-base/src/main/java/com/mparticle/kits/KitManagerImpl.java @@ -25,19 +25,14 @@ import com.mparticle.MPEvent; import com.mparticle.MParticle; import com.mparticle.MParticleOptions; -import com.mparticle.MParticleTask; -import com.mparticle.MpRoktEventCallback; -import com.mparticle.RoktEvent; +import com.mparticle.internal.RoktKitApi; import com.mparticle.UserAttributeListener; import com.mparticle.WrapperSdkVersion; import com.mparticle.commerce.CommerceEvent; import com.mparticle.consent.ConsentState; -import com.mparticle.identity.IdentityApi; import com.mparticle.identity.IdentityApiRequest; -import com.mparticle.identity.IdentityApiResult; import com.mparticle.identity.IdentityStateListener; import com.mparticle.identity.MParticleUser; -import com.mparticle.internal.Constants; import com.mparticle.internal.CoreCallbacks; import com.mparticle.internal.KitManager; import com.mparticle.internal.KitsLoadedCallback; @@ -45,9 +40,6 @@ import com.mparticle.internal.MPUtility; import com.mparticle.internal.ReportingManager; import com.mparticle.kits.mappings.CustomMapping; -import com.mparticle.rokt.PlacementOptions; -import com.mparticle.rokt.RoktConfig; -import com.mparticle.rokt.RoktEmbeddedView; import com.mparticle.rokt.RoktOptions; import org.json.JSONArray; @@ -63,13 +55,10 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; -import kotlinx.coroutines.flow.Flow; - public class KitManagerImpl implements KitManager, AttributionListener, UserAttributeListener, IdentityStateListener { private static HandlerThread kitHandlerThread; @@ -1348,116 +1337,16 @@ public void reset() { } @Override - public void execute(@NonNull String viewName, - @NonNull Map attributes, - @Nullable MpRoktEventCallback mpRoktEventCallback, - @Nullable Map> placeHolders, - @Nullable Map> fontTypefaces, - @Nullable RoktConfig config) { - execute(viewName, attributes, mpRoktEventCallback, placeHolders, fontTypefaces, config, null); - } - - @Override - public void execute(@NonNull String viewName, - @NonNull Map attributes, - @Nullable MpRoktEventCallback mpRoktEventCallback, - @Nullable Map> placeHolders, - @Nullable Map> fontTypefaces, - @Nullable RoktConfig config, - @Nullable PlacementOptions options) { + @Nullable + public RoktKitApi getRoktKitApi() { for (KitIntegration provider : providers.values()) { - try { - if (provider instanceof KitIntegration.RoktListener && !provider.isDisabled()) { - if (attributes == null) { - attributes = new HashMap<>(); - } - MParticle instance = MParticle.getInstance(); - MParticleUser user = instance.Identity().getCurrentUser(); - String email = getValueIgnoreCase(attributes, "email"); - String hashedEmail = getValueIgnoreCase(attributes, "emailsha256"); - Map tempAttributes = attributes; - KitConfiguration kitConfig = provider.getConfiguration(); - confirmEmail(email, hashedEmail, user, instance.Identity(), kitConfig, () -> { - Map finalAttributes = prepareAttributes(provider, tempAttributes, user); - - ((KitIntegration.RoktListener) provider).execute(viewName, - finalAttributes, - mpRoktEventCallback, - placeHolders, - fontTypefaces, - FilteredMParticleUser.getInstance(user.getId(), provider), - config, - options); - }); - } - } catch (Exception e) { - Logger.warning("Failed to call execute for kit: " + provider.getName() + ": " + e.getMessage()); - } - } - } - - private String getValueIgnoreCase(Map map, String searchKey) { - for (Map.Entry entry : map.entrySet()) { - if (entry.getKey().equalsIgnoreCase(searchKey)) { - return entry.getValue(); + if (provider instanceof KitIntegration.RoktListener && !provider.isDisabled()) { + return new RoktKitApiImpl((KitIntegration.RoktListener) provider, provider); } } return null; } - private Map prepareAttributes(KitIntegration provider, Map finalAttributes, MParticleUser user){ - JSONArray jsonArray = new JSONArray(); - - KitConfiguration kitConfig = provider.getConfiguration(); - if (kitConfig != null) { - try { - jsonArray = kitConfig.getPlacementAttributesMapping(); - } catch (JSONException e) { - Logger.warning("Invalid placementAttributes for kit: " + provider.getName() + " JSON: " + e.getMessage()); - } - } - for (int i = 0; i < jsonArray.length(); i++) { - JSONObject obj = jsonArray.optJSONObject(i); - if (obj == null) continue; - String mapFrom = obj.optString("map"); - String mapTo = obj.optString("value"); - if (finalAttributes.containsKey(mapFrom)) { - String value = finalAttributes.remove(mapFrom); - finalAttributes.put(mapTo, value); - } - } - Map objectAttributes = new HashMap<>(); - for (Map.Entry entry : finalAttributes.entrySet()) { - if(!entry.getKey().equals(Constants.MessageKey.SANDBOX_MODE_ROKT)) { - objectAttributes.put(entry.getKey(), entry.getValue()); - } - } - if (user != null) { - user.setUserAttributes(objectAttributes); - } - - if (!finalAttributes.containsKey(Constants.MessageKey.SANDBOX_MODE_ROKT)) { - finalAttributes.put(Constants.MessageKey.SANDBOX_MODE_ROKT, String.valueOf(Objects.toString(MPUtility.isDevEnv(), "false"))); // Default value is "false" if null - } - return finalAttributes; - } - - @Override - public Flow events(@NonNull String identifier) { - for (KitIntegration provider : providers.values()) { - try { - if (provider instanceof KitIntegration.RoktListener && !provider.isDisabled()) { - Logger.verbose("Calling events for kit: " + provider.getName() + " with identifier: " + identifier); - return ((KitIntegration.RoktListener) provider).events(identifier); - } - } catch (Exception e) { - Logger.warning("Failed to call setWrapperSdkVersion for kit: " + provider.getName() + ": " + e.getMessage()); - } - } - Logger.warning("No RoktListener found"); - return flowOf(); - } - @Override public void setWrapperSdkVersion(@NonNull WrapperSdkVersion wrapperSdkVersion) { for (KitIntegration provider : providers.values()) { @@ -1471,165 +1360,6 @@ public void setWrapperSdkVersion(@NonNull WrapperSdkVersion wrapperSdkVersion) { } } - @Override - public void purchaseFinalized(@NonNull String placementId, @NonNull String catalogItemId, boolean status) { - for (KitIntegration provider : providers.values()) { - try { - if (provider instanceof KitIntegration.RoktListener && !provider.isDisabled()) { - ((KitIntegration.RoktListener) provider).purchaseFinalized(placementId,catalogItemId,status); - } - } catch (Exception e) { - Logger.warning("Failed to call purchaseFinalized for kit: " + provider.getName() + ": " + e.getMessage()); - } - } - } - - @Override - public void close() { - for (final KitIntegration provider : providers.values()) { - try { - if (provider instanceof KitIntegration.RoktListener && !provider.isDisabled()) { - ((KitIntegration.RoktListener) provider).close(); - } - } catch (final Exception e) { - Logger.warning("Failed to call close for kit: " + provider.getName() + ": " + e.getMessage()); - } - } - } - - @Override - public void setSessionId(@NonNull String sessionId) { - for (KitIntegration provider : providers.values()) { - try { - if (provider instanceof KitIntegration.RoktListener && !provider.isDisabled()) { - ((KitIntegration.RoktListener) provider).setSessionId(sessionId); - } - } catch (Exception e) { - Logger.warning("Failed to call setSessionId for kit: " + provider.getName() + ": " + e.getMessage()); - } - } - } - - @Override - @Nullable - public String getSessionId() { - for (KitIntegration provider : providers.values()) { - try { - if (provider instanceof KitIntegration.RoktListener && !provider.isDisabled()) { - return ((KitIntegration.RoktListener) provider).getSessionId(); - } - } catch (Exception e) { - Logger.warning("Failed to call getSessionId for kit: " + provider.getName() + ": " + e.getMessage()); - } - } - return null; - } - - @Override - public void prepareAttributesAsync(@NonNull Map attributes) { - - for (KitIntegration provider : providers.values()) { - try { - if (provider instanceof KitIntegration.RoktListener && !provider.isDisabled()) { - if (attributes == null) { - attributes = new HashMap<>(); - } - MParticle instance = MParticle.getInstance(); - MParticleUser user = instance.Identity().getCurrentUser(); - String email = attributes.get("email"); - String hashedEmail = getValueIgnoreCase(attributes, "emailsha256"); - Map tempAttributes = attributes; - KitConfiguration kitConfig = provider.getConfiguration(); - confirmEmail(email, hashedEmail, user, instance.Identity(), kitConfig, () -> { - Map finalAttributes = prepareAttributes(provider, tempAttributes, user); - ((KitIntegration.RoktListener) provider).enrichAttributes( - finalAttributes, FilteredMParticleUser.getInstance(user.getId(), provider)); - }); - } - } catch (Exception e) { - Logger.warning("Failed to call prepareRoktListener for kit: " + provider.getName() + ": " + e.getMessage()); - } - } - } - - private void confirmEmail( - @Nullable String email, - @Nullable String hashedEmail, - @Nullable MParticleUser user, - IdentityApi identityApi, - KitConfiguration kitConfiguration, - Runnable runnable - ) { - boolean hasEmail = email != null && !email.isEmpty(); - boolean hasHashedEmail = hashedEmail != null && !hashedEmail.isEmpty(); - - if ((hasEmail || hasHashedEmail) && user != null) { - MParticle.IdentityType selectedIdentityType = null; - try { - String identityTypeStr = (kitConfiguration != null) - ? kitConfiguration.getHashedEmailUserIdentityType() - : null; - if (identityTypeStr != null ) { - selectedIdentityType = MParticle.IdentityType.valueOf(identityTypeStr); - } - } catch (IllegalArgumentException e) { - Logger.error("Invalid identity type "+e.getMessage()); - } - String existingEmail = user.getUserIdentities().get(MParticle.IdentityType.Email); - String existingHashedEmail = selectedIdentityType != null ? user.getUserIdentities().get(selectedIdentityType) : null; - boolean emailMismatch = hasEmail && !email.equalsIgnoreCase(existingEmail); - boolean hashedEmailMismatch = hasHashedEmail && !hashedEmail.equalsIgnoreCase(existingHashedEmail); - - if (emailMismatch || (hashedEmailMismatch && selectedIdentityType != null)) { - // If there's an existing email but it doesn't match the passed-in email, log a warning - if (emailMismatch && existingEmail != null) { - Logger.warning(String.format( - "The existing email on the user (%s) does not match the email passed to selectPlacements (%s). " + - "Please make sure to sync the email identity to mParticle as soon as it's available. " + - "Identifying user with the provided email before continuing to selectPlacements.", - existingEmail, email - )); - } - // If there's an existing other but it doesn't match the passed-in hashed email, log a warning - else if (hashedEmailMismatch && existingHashedEmail != null) { - Logger.warning(String.format( - "The existing hashed email on the user (%s) does not match the hashed email passed to selectPlacements (%s). " + - "Please make sure to sync the hashed email identity to mParticle as soon as it's available. " + - "Identifying user with the provided hashed email before continuing to selectPlacements.", - existingHashedEmail, hashedEmail - )); - } - - IdentityApiRequest.Builder identityBuilder = IdentityApiRequest.withUser(user); - if (emailMismatch) { - identityBuilder.email(email); - } - if (hashedEmailMismatch) { - identityBuilder.userIdentity(selectedIdentityType, hashedEmail); - } - - IdentityApiRequest identityRequest = identityBuilder.build(); - MParticleTask task = identityApi.identify(identityRequest); - - task.addFailureListener(result -> { - Logger.error("Failed to sync email from selectPlacement to user: " + result.getErrors()); - runnable.run(); - }); - - task.addSuccessListener(result -> { - Logger.debug("Updated email identity based on selectPlacement's attributes: " + - result.getUser().getUserIdentities().get(MParticle.IdentityType.Email)); - runnable.run(); - }); - - } else { - runnable.run(); - } - } else { - runnable.run(); - } - } - public void runOnKitThread(Runnable runnable) { if (mKitHandler == null) { mKitHandler = new Handler(kitHandlerThread.getLooper()); diff --git a/android-kit-base/src/main/kotlin/com/mparticle/kits/RoktKitApiImpl.kt b/android-kit-base/src/main/kotlin/com/mparticle/kits/RoktKitApiImpl.kt new file mode 100644 index 000000000..f5bfcc646 --- /dev/null +++ b/android-kit-base/src/main/kotlin/com/mparticle/kits/RoktKitApiImpl.kt @@ -0,0 +1,266 @@ +package com.mparticle.kits + +import android.graphics.Typeface +import com.mparticle.MParticle +import com.mparticle.MpRoktEventCallback +import com.mparticle.RoktEvent +import com.mparticle.identity.IdentityApi +import com.mparticle.identity.IdentityApiRequest +import com.mparticle.identity.MParticleUser +import com.mparticle.internal.Constants +import com.mparticle.internal.Logger +import com.mparticle.internal.MPUtility +import com.mparticle.internal.RoktKitApi +import com.mparticle.rokt.PlacementOptions +import com.mparticle.rokt.RoktConfig +import com.mparticle.rokt.RoktEmbeddedView +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import org.json.JSONException +import java.lang.ref.WeakReference +import java.util.Objects + +/** + * Implementation of [RoktKitApi] that wraps a [KitIntegration.RoktListener]. + * + * This class handles user resolution and attribute preparation before delegating + * to the underlying Rokt Kit implementation. + */ +internal class RoktKitApiImpl( + private val roktListener: KitIntegration.RoktListener, + private val kitIntegration: KitIntegration, +) : RoktKitApi { + + override fun execute( + viewName: String, + attributes: Map, + mpRoktEventCallback: MpRoktEventCallback?, + placeHolders: Map>?, + fontTypefaces: Map>?, + config: RoktConfig?, + options: PlacementOptions?, + ) { + try { + val mutableAttributes = attributes.toMutableMap() + val instance = MParticle.getInstance() + if (instance == null) { + Logger.warning("MParticle instance is null, cannot execute Rokt placement") + return + } + val user = instance.Identity().currentUser + 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.execute( + 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}") + } + } + + override fun events(identifier: String): Flow { + return try { + Logger.verbose("Calling events for Rokt Kit with identifier: $identifier") + roktListener.events(identifier) + } catch (e: Exception) { + Logger.warning("Failed to call events for Rokt Kit: ${e.message}") + flowOf() + } + } + + override fun purchaseFinalized(placementId: String, catalogItemId: String, status: Boolean) { + try { + roktListener.purchaseFinalized(placementId, catalogItemId, status) + } catch (e: Exception) { + Logger.warning("Failed to call purchaseFinalized for Rokt Kit: ${e.message}") + } + } + + override fun close() { + try { + roktListener.close() + } catch (e: Exception) { + Logger.warning("Failed to call close for Rokt Kit: ${e.message}") + } + } + + override fun setSessionId(sessionId: String) { + try { + roktListener.setSessionId(sessionId) + } catch (e: Exception) { + Logger.warning("Failed to call setSessionId for Rokt Kit: ${e.message}") + } + } + + override fun getSessionId(): String? { + return try { + roktListener.sessionId + } catch (e: Exception) { + Logger.warning("Failed to call getSessionId for Rokt Kit: ${e.message}") + null + } + } + + override fun prepareAttributesAsync(attributes: Map) { + try { + val mutableAttributes = attributes.toMutableMap() + val instance = MParticle.getInstance() + if (instance == null) { + Logger.warning("MParticle instance is null, cannot prepare attributes") + return + } + val user = instance.Identity().currentUser + val email = 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), + ) + } + } catch (e: Exception) { + Logger.warning("Failed to call prepareAttributesAsync for Rokt Kit: ${e.message}") + } + } + + // Helper methods + + private fun getValueIgnoreCase(map: Map, searchKey: String): String? { + for ((key, value) in map) { + if (key.equals(searchKey, ignoreCase = true)) { + return value + } + } + return null + } + + private fun prepareAttributes( + finalAttributes: MutableMap, + user: MParticleUser?, + ): MutableMap { + val kitConfig = kitIntegration.configuration + val jsonArray = try { + kitConfig?.placementAttributesMapping ?: org.json.JSONArray() + } catch (e: JSONException) { + Logger.warning("Invalid placementAttributes for Rokt Kit JSON: ${e.message}") + org.json.JSONArray() + } + + for (i in 0 until jsonArray.length()) { + 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 (value != null) { + finalAttributes[mapTo] = value + } + } + } + + val objectAttributes = mutableMapOf() + 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") + } + return finalAttributes + } + + private fun confirmEmail( + email: String?, + hashedEmail: String?, + user: MParticleUser?, + identityApi: IdentityApi, + kitConfiguration: KitConfiguration?, + runnable: Runnable, + ) { + val hasEmail = !email.isNullOrEmpty() + val hasHashedEmail = !hashedEmail.isNullOrEmpty() + + if ((hasEmail || hasHashedEmail) && user != null) { + var selectedIdentityType: MParticle.IdentityType? = null + try { + val identityTypeStr = kitConfiguration?.hashedEmailUserIdentityType + if (identityTypeStr != null) { + selectedIdentityType = MParticle.IdentityType.valueOf(identityTypeStr) + } + } catch (e: IllegalArgumentException) { + Logger.error("Invalid identity type ${e.message}") + } + + val existingEmail = user.userIdentities[MParticle.IdentityType.Email] + val existingHashedEmail = selectedIdentityType?.let { user.userIdentities[it] } + val emailMismatch = hasEmail && !email.equals(existingEmail, ignoreCase = true) + val hashedEmailMismatch = + hasHashedEmail && !hashedEmail.equals(existingHashedEmail, ignoreCase = true) + + if (emailMismatch || (hashedEmailMismatch && selectedIdentityType != null)) { + // If there's an existing email but it doesn't match the passed-in email, log a warning + if (emailMismatch && existingEmail != null) { + Logger.warning( + "The existing email on the user ($existingEmail) does not match the email passed to selectPlacements ($email). " + + "Please make sure to sync the email identity to mParticle as soon as it's available. " + + "Identifying user with the provided email before continuing to selectPlacements.", + ) + } else if (hashedEmailMismatch && existingHashedEmail != null) { + // If there's an existing other but it doesn't match the passed-in hashed email, log a warning + Logger.warning( + "The existing hashed email on the user ($existingHashedEmail) does not match the hashed email passed to selectPlacements ($hashedEmail). " + + "Please make sure to sync the hashed email identity to mParticle as soon as it's available. " + + "Identifying user with the provided hashed email before continuing to selectPlacements.", + ) + } + + val identityBuilder = IdentityApiRequest.withUser(user) + if (emailMismatch) { + identityBuilder.email(email) + } + if (hashedEmailMismatch && selectedIdentityType != null) { + identityBuilder.userIdentity(selectedIdentityType, hashedEmail) + } + + val identityRequest = identityBuilder.build() + val task = identityApi.identify(identityRequest) + + task.addFailureListener { result -> + Logger.error("Failed to sync email from selectPlacement to user: ${result?.errors}") + runnable.run() + } + + task.addSuccessListener { result -> + Logger.debug( + "Updated email identity based on selectPlacement's attributes: " + + result.user.userIdentities[MParticle.IdentityType.Email], + ) + runnable.run() + } + } else { + runnable.run() + } + } else { + runnable.run() + } + } +} diff --git a/android-kit-base/src/test/kotlin/com/mparticle/kits/KitManagerImplTest.kt b/android-kit-base/src/test/kotlin/com/mparticle/kits/KitManagerImplTest.kt index 67a7d7404..f4161cb58 100644 --- a/android-kit-base/src/test/kotlin/com/mparticle/kits/KitManagerImplTest.kt +++ b/android-kit-base/src/test/kotlin/com/mparticle/kits/KitManagerImplTest.kt @@ -8,7 +8,6 @@ import com.mparticle.BaseEvent import com.mparticle.MPEvent import com.mparticle.MParticle import com.mparticle.MParticleOptions -import com.mparticle.MParticleTask import com.mparticle.MpRoktEventCallback import com.mparticle.RoktEvent import com.mparticle.WrapperSdk @@ -18,7 +17,6 @@ import com.mparticle.commerce.Product import com.mparticle.consent.ConsentState import com.mparticle.consent.GDPRConsent import com.mparticle.identity.IdentityApi -import com.mparticle.identity.IdentityApiResult import com.mparticle.identity.MParticleUser import com.mparticle.internal.CoreCallbacks import com.mparticle.internal.Logger @@ -61,7 +59,6 @@ import org.powermock.api.mockito.PowerMockito import org.powermock.core.classloader.annotations.PrepareForTest import org.powermock.modules.junit4.PowerMockRunner import java.lang.ref.WeakReference -import java.lang.reflect.Method import java.util.Arrays import java.util.LinkedList import java.util.concurrent.ConcurrentHashMap @@ -955,14 +952,19 @@ class KitManagerImplTest { Pair("country", "US"), ) - manager.execute("Test", attributes, null, null, null, null) - Assert.assertEquals(6, attributes.size) - Assert.assertEquals("(123) 456-9898", attributes["no"]) - Assert.assertEquals("55555", attributes["minorcatid"]) - Assert.assertEquals("Test1", attributes["lastname"]) - Assert.assertEquals("Test", attributes["test"]) - Assert.assertEquals("US", attributes["country"]) - Assert.assertEquals("false", attributes["sandbox"]) + val roktApi = manager.getRoktKitApi() + Assert.assertNotNull(roktApi) + roktApi!!.execute("Test", attributes, null, null, null, null) + val finalAttributes = mockedProvider.lastAttributes + Assert.assertNotNull(finalAttributes) + finalAttributes!! + Assert.assertEquals(6, finalAttributes.size) + Assert.assertEquals("(123) 456-9898", finalAttributes["no"]) + Assert.assertEquals("55555", finalAttributes["minorcatid"]) + Assert.assertEquals("Test1", finalAttributes["lastname"]) + Assert.assertEquals("Test", finalAttributes["test"]) + Assert.assertEquals("US", finalAttributes["country"]) + Assert.assertEquals("false", finalAttributes["sandbox"]) } @Test @@ -1028,15 +1030,20 @@ class KitManagerImplTest { Pair("country", "US"), ) - manager.execute("Test", attributes, null, null, null, null) - Assert.assertEquals(6, attributes.size) - - Assert.assertEquals("(123) 456-9898", attributes["call"]) - Assert.assertEquals("5-45555", attributes["postal"]) - Assert.assertEquals("Test1", attributes["lastname"]) - Assert.assertEquals("Test", attributes["test"]) - Assert.assertEquals("US", attributes["country"]) - Assert.assertEquals("false", attributes["sandbox"]) + val roktApi = manager.getRoktKitApi() + Assert.assertNotNull(roktApi) + roktApi!!.execute("Test", attributes, null, null, null, null) + val finalAttributes = mockedProvider.lastAttributes + Assert.assertNotNull(finalAttributes) + finalAttributes!! + Assert.assertEquals(6, finalAttributes.size) + + Assert.assertEquals("(123) 456-9898", finalAttributes["call"]) + Assert.assertEquals("5-45555", finalAttributes["postal"]) + Assert.assertEquals("Test1", finalAttributes["lastname"]) + Assert.assertEquals("Test", finalAttributes["test"]) + Assert.assertEquals("US", finalAttributes["country"]) + Assert.assertEquals("false", finalAttributes["sandbox"]) } @Test @@ -1102,14 +1109,19 @@ class KitManagerImplTest { Pair("country", "US"), ) - manager.execute("Test", attributes, null, null, null, null) - Assert.assertEquals(6, attributes.size) - Assert.assertEquals("(123) 456-9898", attributes["no"]) - Assert.assertEquals("5-45555", attributes["minorcatid"]) - Assert.assertEquals("Test1", attributes["lastname"]) - Assert.assertEquals("Test", attributes["test"]) - Assert.assertEquals("US", attributes["country"]) - Assert.assertEquals("false", attributes["sandbox"]) + val roktApi = manager.getRoktKitApi() + Assert.assertNotNull(roktApi) + roktApi!!.execute("Test", attributes, null, null, null, null) + val finalAttributes = mockedProvider.lastAttributes + Assert.assertNotNull(finalAttributes) + finalAttributes!! + Assert.assertEquals(6, finalAttributes.size) + Assert.assertEquals("(123) 456-9898", finalAttributes["no"]) + Assert.assertEquals("5-45555", finalAttributes["minorcatid"]) + Assert.assertEquals("Test1", finalAttributes["lastname"]) + Assert.assertEquals("Test", finalAttributes["test"]) + Assert.assertEquals("US", finalAttributes["country"]) + Assert.assertEquals("false", finalAttributes["sandbox"]) } @Test @@ -1173,613 +1185,19 @@ class KitManagerImplTest { Pair("country", "US"), ) - manager.execute("Test", attributes, null, null, null, null) - Assert.assertEquals(6, attributes.size) - Assert.assertEquals("(123) 456-9898", attributes["number"]) - Assert.assertEquals("55555", attributes["customerId"]) - Assert.assertEquals("Test1", attributes["lastname"]) - Assert.assertEquals("Test", attributes["test"]) - Assert.assertEquals("US", attributes["country"]) - Assert.assertEquals("false", attributes["sandbox"]) - } - - @Test - fun testConfirmEmail_When_EmailSyncSuccess() { - var runnable: Runnable = mock(Runnable::class.java) - var user: MParticleUser = mock(MParticleUser::class.java) - val instance = MockMParticle() - val sideloadedKit = mock(MPSideloadedKit::class.java) - val kitId = 6000000 - - val configJSONObj = - JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - val identityApi = mock(IdentityApi::class.java) - val oldEmail = "old@example.com" - val mockTask = mock(MParticleTask::class.java) as MParticleTask - `when`(identityApi.identify(any())).thenReturn(mockTask) - val identities: MutableMap = HashMap() - identities.put(MParticle.IdentityType.Email, oldEmail) - `when`(user.userIdentities).thenReturn(identities) - instance.setIdentityApi(identityApi) - val settingsMap = - hashMapOf( - "placementAttributesMapping" to - """ - [ - ] - """.trimIndent(), - ) - val field = KitConfiguration::class.java.getDeclaredField("settings") - field.isAccessible = true - field.set(mockedKitConfig, settingsMap) - - val options = - MParticleOptions - .builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List) - .build() - val manager: KitManagerImpl = MockKitManagerImpl(options) - val method: Method = - KitManagerImpl::class.java.getDeclaredMethod( - "confirmEmail", - String::class.java, - String::class.java, - MParticleUser::class.java, - IdentityApi::class.java, - KitConfiguration::class.java, - Runnable::class.java, - ) - method.isAccessible = true - val result = method.invoke(manager, "Test@gmail.com", "", user, identityApi, mockedKitConfig, runnable) - verify(mockTask).addSuccessListener(any()) - } - - @Test - fun testConfirmEmail_When_EmailAlreadySynced() { - var runnable: Runnable = mock(Runnable::class.java) - var user: MParticleUser = mock(MParticleUser::class.java) - val instance = MockMParticle() - val sideloadedKit = mock(MPSideloadedKit::class.java) - val kitId = 6000000 - - val configJSONObj = - JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - val identityApi = mock(IdentityApi::class.java) - val oldEmail = "Test@gmail.com" - val mockTask = mock(MParticleTask::class.java) as MParticleTask - `when`(identityApi.identify(any())).thenReturn(mockTask) - val identities: MutableMap = HashMap() - identities.put(MParticle.IdentityType.Email, oldEmail) - `when`(user.userIdentities).thenReturn(identities) - instance.setIdentityApi(identityApi) - val settingsMap = - hashMapOf( - "placementAttributesMapping" to - """ - [ - ] - """.trimIndent(), - ) - val field = KitConfiguration::class.java.getDeclaredField("settings") - field.isAccessible = true - field.set(mockedKitConfig, settingsMap) - - val options = - MParticleOptions - .builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List) - .build() - val manager: KitManagerImpl = MockKitManagerImpl(options) - val method: Method = KitManagerImpl::class.java.getDeclaredMethod( - "confirmEmail", - String::class.java, - String::class.java, - MParticleUser::class.java, - IdentityApi::class.java, - KitConfiguration::class.java, - Runnable::class.java, - ) - method.isAccessible = true - val result = method.invoke(manager, "Test@gmail.com", null, user, identityApi, mockedKitConfig, runnable) - verify(runnable).run() - } - - @Test - fun testConfirmEmail_When_mailIsNull() { - var runnable: Runnable = mock(Runnable::class.java) - var user: MParticleUser = mock(MParticleUser::class.java) - val instance = MockMParticle() - val sideloadedKit = mock(MPSideloadedKit::class.java) - val kitId = 6000000 - - val configJSONObj = - JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - val identityApi = mock(IdentityApi::class.java) - val oldEmail = "Test@gmail.com" - val mockTask = mock(MParticleTask::class.java) as MParticleTask - `when`(identityApi.identify(any())).thenReturn(mockTask) - val identities: MutableMap = HashMap() - identities.put(MParticle.IdentityType.Email, oldEmail) - `when`(user.userIdentities).thenReturn(identities) - instance.setIdentityApi(identityApi) - val settingsMap = - hashMapOf( - "placementAttributesMapping" to - """ - [ - ] - """.trimIndent(), - ) - val field = KitConfiguration::class.java.getDeclaredField("settings") - field.isAccessible = true - field.set(mockedKitConfig, settingsMap) - - val options = - MParticleOptions - .builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List) - .build() - val manager: KitManagerImpl = MockKitManagerImpl(options) - val method: Method = KitManagerImpl::class.java.getDeclaredMethod( - "confirmEmail", - String::class.java, - String::class.java, - MParticleUser::class.java, - IdentityApi::class.java, - KitConfiguration::class.java, - Runnable::class.java, - ) - method.isAccessible = true - val result = method.invoke(manager, null, null, user, identityApi, mockedKitConfig, runnable) - verify(runnable).run() - } - - @Test - fun testConfirmEmail_When_User_IsNull() { - var runnable: Runnable = mock(Runnable::class.java) - var user: MParticleUser = mock(MParticleUser::class.java) - val instance = MockMParticle() - val sideloadedKit = mock(MPSideloadedKit::class.java) - val kitId = 6000000 - - val configJSONObj = - JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - val identityApi = mock(IdentityApi::class.java) - val oldEmail = "Test@gmail.com" - val mockTask = mock(MParticleTask::class.java) as MParticleTask - `when`(identityApi.identify(any())).thenReturn(mockTask) - val identities: MutableMap = HashMap() - identities.put(MParticle.IdentityType.Email, oldEmail) - `when`(user.userIdentities).thenReturn(identities) - instance.setIdentityApi(identityApi) - val settingsMap = - hashMapOf( - "placementAttributesMapping" to - """ - [ - ] - """.trimIndent(), - ) - val field = KitConfiguration::class.java.getDeclaredField("settings") - field.isAccessible = true - field.set(mockedKitConfig, settingsMap) - - val options = - MParticleOptions - .builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List) - .build() - val manager: KitManagerImpl = MockKitManagerImpl(options) - val method: Method = KitManagerImpl::class.java.getDeclaredMethod( - "confirmEmail", - String::class.java, - String::class.java, - MParticleUser::class.java, - IdentityApi::class.java, - KitConfiguration::class.java, - Runnable::class.java, - ) - method.isAccessible = true - val result = method.invoke(manager, "", "", user, identityApi, mockedKitConfig, runnable) - verify(runnable).run() - } - - @Test - fun testConfirmHashedEmail_When_EmailSyncSuccess() { - var runnable: Runnable = mock(Runnable::class.java) - var user: MParticleUser = mock(MParticleUser::class.java) - val instance = MockMParticle() - val sideloadedKit = mock(MPSideloadedKit::class.java) - val kitId = 6000000 - - val configJSONObj = - JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - val identityApi = mock(IdentityApi::class.java) - val oldHashedEmail = "hashed_old@example.com" - val mockTask = mock(MParticleTask::class.java) as MParticleTask - `when`(identityApi.identify(any())).thenReturn(mockTask) - val identities: MutableMap = HashMap() - identities.put(MParticle.IdentityType.Other, oldHashedEmail) - `when`(user.userIdentities).thenReturn(identities) - instance.setIdentityApi(identityApi) - val settingsMap = - hashMapOf( - "placementAttributesMapping" to - """ - [ - ] - """.trimIndent(), - ) - val field = KitConfiguration::class.java.getDeclaredField("settings") - field.isAccessible = true - field.set(mockedKitConfig, settingsMap) - - val options = - MParticleOptions - .builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List) - .build() - val manager: KitManagerImpl = MockKitManagerImpl(options) - val method: Method = KitManagerImpl::class.java.getDeclaredMethod( - "confirmEmail", - String::class.java, - String::class.java, - MParticleUser::class.java, - IdentityApi::class.java, - KitConfiguration::class.java, - Runnable::class.java, - ) - method.isAccessible = true - val result = method.invoke(manager, "", "hashed_Test@gmail.com", user, identityApi, mockedKitConfig, runnable) - verify(runnable).run() - } - - @Test - fun testConfirmHashedEmail_When_EmailAlreadySynced() { - var runnable: Runnable = mock(Runnable::class.java) - var user: MParticleUser = mock(MParticleUser::class.java) - val instance = MockMParticle() - val sideloadedKit = mock(MPSideloadedKit::class.java) - val kitId = 6000000 - - val configJSONObj = - JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - val identityApi = mock(IdentityApi::class.java) - val oldHashedEmail = "hashed_Test@gmail.com" - val mockTask = mock(MParticleTask::class.java) as MParticleTask - `when`(identityApi.identify(any())).thenReturn(mockTask) - val identities: MutableMap = HashMap() - identities.put(MParticle.IdentityType.Other, oldHashedEmail) - `when`(user.userIdentities).thenReturn(identities) - instance.setIdentityApi(identityApi) - val settingsMap = - hashMapOf( - "placementAttributesMapping" to - """ - [ - ] - """.trimIndent(), - ) - val field = KitConfiguration::class.java.getDeclaredField("settings") - field.isAccessible = true - field.set(mockedKitConfig, settingsMap) - - val options = - MParticleOptions - .builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List) - .build() - val manager: KitManagerImpl = MockKitManagerImpl(options) - val method: Method = KitManagerImpl::class.java.getDeclaredMethod( - "confirmEmail", - String::class.java, - String::class.java, - MParticleUser::class.java, - IdentityApi::class.java, - KitConfiguration::class.java, - Runnable::class.java, - ) - method.isAccessible = true - val result = method.invoke(manager, null, "hashed_Test@gmail.com", user, identityApi, mockedKitConfig, runnable) - verify(runnable).run() - } - - @Test - fun testConfirmHashedEmail_When_OtherIsNull() { - var runnable: Runnable = mock(Runnable::class.java) - var user: MParticleUser = mock(MParticleUser::class.java) - val instance = MockMParticle() - val sideloadedKit = mock(MPSideloadedKit::class.java) - val kitId = 6000000 - - val configJSONObj = - JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - val identityApi = mock(IdentityApi::class.java) - val oldHashedEmail = "hashed_Test@gmail.com" - val mockTask = mock(MParticleTask::class.java) as MParticleTask - `when`(identityApi.identify(any())).thenReturn(mockTask) - val identities: MutableMap = HashMap() - identities.put(MParticle.IdentityType.Other, oldHashedEmail) - `when`(user.userIdentities).thenReturn(identities) - instance.setIdentityApi(identityApi) - val settingsMap = - hashMapOf( - "placementAttributesMapping" to - """ - [ - ] - """.trimIndent(), - ) - val field = KitConfiguration::class.java.getDeclaredField("settings") - field.isAccessible = true - field.set(mockedKitConfig, settingsMap) - - val options = - MParticleOptions - .builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List) - .build() - val manager: KitManagerImpl = MockKitManagerImpl(options) - val method: Method = KitManagerImpl::class.java.getDeclaredMethod( - "confirmEmail", - String::class.java, - String::class.java, - MParticleUser::class.java, - IdentityApi::class.java, - KitConfiguration::class.java, - Runnable::class.java, - ) - method.isAccessible = true - val result = method.invoke(manager, null, null, user, identityApi, mockedKitConfig, runnable) - verify(runnable).run() - } - - @Test - fun testConfirmHashedEmail_When_HashedEmailUserIdentityType_Is_Other3() { - var runnable: Runnable = mock(Runnable::class.java) - var user: MParticleUser = mock(MParticleUser::class.java) - val instance = MockMParticle() - val sideloadedKit = mock(MPSideloadedKit::class.java) - val kitId = 6000000 - - val configJSONObj = JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - - `when`(mockedKitConfig.hashedEmailUserIdentityType).thenReturn("Other3") - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - val identityApi = mock(IdentityApi::class.java) - val oldHashedEmail = "hashed_old@example.com" - val mockTask = mock(MParticleTask::class.java) as MParticleTask - `when`(identityApi.identify(any())).thenReturn(mockTask) - val identities: MutableMap = HashMap() - identities.put(MParticle.IdentityType.Other, oldHashedEmail) - `when`(user.userIdentities).thenReturn(identities) - instance.setIdentityApi(identityApi) - val settingsMap = hashMapOf( - "placementAttributesMapping" to """ - [ - // add placement attributes here if needed - ] - """.trimIndent(), - "hashedEmailUserIdentityType" to "Other3", - ) - val field = KitConfiguration::class.java.getDeclaredField("settings") - field.isAccessible = true - field.set(mockedKitConfig, settingsMap) - - val options = MParticleOptions.builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List).build() - val manager: KitManagerImpl = MockKitManagerImpl(options) - val method: Method = KitManagerImpl::class.java.getDeclaredMethod( - "confirmEmail", - String::class.java, - String::class.java, - MParticleUser::class.java, - IdentityApi::class.java, - KitConfiguration::class.java, - Runnable::class.java, - ) - method.isAccessible = true - val result = method.invoke(manager, "", "hashed_Test@gmail.com", user, identityApi, mockedKitConfig, runnable) - verify(mockTask).addSuccessListener(any()) - } - - @Test - fun testConfirmHashedEmail_When_HashedEmailUserIdentityType_Is_Unknown() { - var runnable: Runnable = mock(Runnable::class.java) - var user: MParticleUser = mock(MParticleUser::class.java) - val instance = MockMParticle() - val sideloadedKit = mock(MPSideloadedKit::class.java) - val kitId = 6000000 - - val configJSONObj = JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - - `when`(mockedKitConfig.hashedEmailUserIdentityType).thenReturn("Unknown") - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - val identityApi = mock(IdentityApi::class.java) - val oldHashedEmail = "hashed_old@example.com" - val mockTask = mock(MParticleTask::class.java) as MParticleTask - `when`(identityApi.identify(any())).thenReturn(mockTask) - val identities: MutableMap = HashMap() - identities.put(MParticle.IdentityType.Other, oldHashedEmail) - `when`(user.userIdentities).thenReturn(identities) - instance.setIdentityApi(identityApi) - val settingsMap = hashMapOf( - "placementAttributesMapping" to """ - [ - // add placement attributes here if needed - ] - """.trimIndent(), - "hashedEmailUserIdentityType" to "Unknown", - ) - val field = KitConfiguration::class.java.getDeclaredField("settings") - field.isAccessible = true - field.set(mockedKitConfig, settingsMap) - - val options = MParticleOptions.builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List).build() - val manager: KitManagerImpl = MockKitManagerImpl(options) - val method: Method = KitManagerImpl::class.java.getDeclaredMethod( - "confirmEmail", - String::class.java, - String::class.java, - MParticleUser::class.java, - IdentityApi::class.java, - KitConfiguration::class.java, - Runnable::class.java, - ) - method.isAccessible = true - val result = method.invoke(manager, "", "hashed_Test@gmail.com", user, identityApi, mockedKitConfig, runnable) - verify(runnable).run() - } - - @Test - fun testGetValueIgnoreCase_keyExistsDifferentCase() { - val sideloadedKit = mock(MPSideloadedKit::class.java) - val options = - MParticleOptions - .builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List) - .build() - val kitId = 6000000 - - val configJSONObj = - JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - val manager: KitManagerImpl = MockKitManagerImpl(options) - val method: Method = - KitManagerImpl::class.java.getDeclaredMethod( - "getValueIgnoreCase", - Map::class.java, - String::class.java, - ) - method.isAccessible = true - val map = hashMapOf("Email" to "test@example.com") - val result = method.invoke(manager, map, "email") - assertEquals("test@example.com", result) - } - - @Test - fun testGetValueIgnoreCase_when__no_match() { - val sideloadedKit = mock(MPSideloadedKit::class.java) - val options = - MParticleOptions - .builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List) - .build() - val kitId = 6000000 - - val configJSONObj = - JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - val manager: KitManagerImpl = MockKitManagerImpl(options) - val method: Method = - KitManagerImpl::class.java.getDeclaredMethod( - "getValueIgnoreCase", - Map::class.java, - String::class.java, - ) - method.isAccessible = true - val map = mapOf("Name" to "Test") - val result = method.invoke(manager, map, "email") - assertNull(result) - } - - @Test - fun testGetValueIgnoreCase_when_empty_map_returns_null() { - val sideloadedKit = mock(MPSideloadedKit::class.java) - val options = - MParticleOptions - .builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List) - .build() - val kitId = 6000000 - - val configJSONObj = - JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - val manager: KitManagerImpl = MockKitManagerImpl(options) - val method: Method = - KitManagerImpl::class.java.getDeclaredMethod( - "getValueIgnoreCase", - Map::class.java, - String::class.java, - ) - method.isAccessible = true - val map = emptyMap() - val result = method.invoke(manager, map, "email") - assertNull(result) - } - - @Test - fun testGetValueIgnoreCase_when_empty_key_returns_null1() { - val sideloadedKit = mock(MPSideloadedKit::class.java) - val options = - MParticleOptions - .builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List) - .build() - val kitId = 6000000 - - val configJSONObj = - JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - val manager: KitManagerImpl = MockKitManagerImpl(options) - val method: Method = - KitManagerImpl::class.java.getDeclaredMethod( - "getValueIgnoreCase", - Map::class.java, - String::class.java, - ) - method.isAccessible = true - val map = mapOf("Name" to "Test") - val result = method.invoke(manager, map, null) - assertNull(result) + val roktApi = manager.getRoktKitApi() + Assert.assertNotNull(roktApi) + roktApi!!.execute("Test", attributes, null, null, null, null) + val finalAttributes = mockedProvider.lastAttributes + Assert.assertNotNull(finalAttributes) + finalAttributes!! + Assert.assertEquals(6, finalAttributes.size) + Assert.assertEquals("(123) 456-9898", finalAttributes["number"]) + Assert.assertEquals("55555", finalAttributes["customerId"]) + Assert.assertEquals("Test1", finalAttributes["lastname"]) + Assert.assertEquals("Test", finalAttributes["test"]) + Assert.assertEquals("US", finalAttributes["country"]) + Assert.assertEquals("false", finalAttributes["sandbox"]) } @Test @@ -1841,14 +1259,19 @@ class KitManagerImplTest { Pair("customerId", "55555"), Pair("country", "US"), ) - manager.execute("Test", attributes, null, null, null, null) - Assert.assertEquals(6, attributes.size) - Assert.assertEquals("(123) 456-9898", attributes["number"]) - Assert.assertEquals("55555", attributes["customerId"]) - Assert.assertEquals("Test1", attributes["lastname"]) - Assert.assertEquals("Test", attributes["test"]) - Assert.assertEquals("US", attributes["country"]) - Assert.assertEquals("false", attributes["sandbox"]) + val roktApi = manager.getRoktKitApi() + Assert.assertNotNull(roktApi) + roktApi!!.execute("Test", attributes, null, null, null, null) + val finalAttributes = mockedProvider.lastAttributes + Assert.assertNotNull(finalAttributes) + finalAttributes!! + Assert.assertEquals(6, finalAttributes.size) + Assert.assertEquals("(123) 456-9898", finalAttributes["number"]) + Assert.assertEquals("55555", finalAttributes["customerId"]) + Assert.assertEquals("Test1", finalAttributes["lastname"]) + Assert.assertEquals("Test", finalAttributes["test"]) + Assert.assertEquals("US", finalAttributes["country"]) + Assert.assertEquals("false", finalAttributes["sandbox"]) } @Test @@ -1871,7 +1294,9 @@ class KitManagerImplTest { val attributes = hashMapOf() val placementOptions = PlacementOptions(jointSdkSelectPlacements = 123L) - manager.execute("Test", attributes, null, null, null, null, placementOptions) + val roktApi = manager.getRoktKitApi() + Assert.assertNotNull(roktApi) + roktApi!!.execute("Test", attributes, null, null, null, null, placementOptions) val optionsCaptor = ArgumentCaptor.forClass(PlacementOptions::class.java) verify(roktListener as KitIntegration.RoktListener).execute( @@ -1906,7 +1331,9 @@ class KitManagerImplTest { val attributes = hashMapOf() - manager.execute("Test", attributes, null, null, null, null) + val roktApi = manager.getRoktKitApi() + Assert.assertNotNull(roktApi) + roktApi!!.execute("Test", attributes, null, null, null, null) val optionsCaptor = ArgumentCaptor.forClass(PlacementOptions::class.java) verify(roktListener as KitIntegration.RoktListener).execute( @@ -1982,14 +1409,19 @@ class KitManagerImplTest { Pair("customerId", "55555"), Pair("country", "US"), ) - manager.execute("Test", attributes, null, null, null, null) - Assert.assertEquals(6, attributes.size) - Assert.assertEquals("(123) 456-9898", attributes["number"]) - Assert.assertEquals("55555", attributes["customerId"]) - Assert.assertEquals("Test1", attributes["lastname"]) - Assert.assertEquals("Test", attributes["test"]) - Assert.assertEquals("US", attributes["country"]) - Assert.assertEquals("true", attributes["sandbox"]) + val roktApi = manager.getRoktKitApi() + Assert.assertNotNull(roktApi) + roktApi!!.execute("Test", attributes, null, null, null, null) + val finalAttributes = mockedProvider.lastAttributes + Assert.assertNotNull(finalAttributes) + finalAttributes!! + Assert.assertEquals(6, finalAttributes.size) + Assert.assertEquals("(123) 456-9898", finalAttributes["number"]) + Assert.assertEquals("55555", finalAttributes["customerId"]) + Assert.assertEquals("Test1", finalAttributes["lastname"]) + Assert.assertEquals("Test", finalAttributes["test"]) + Assert.assertEquals("US", finalAttributes["country"]) + Assert.assertEquals("true", finalAttributes["sandbox"]) } @Test @@ -2053,14 +1485,19 @@ class KitManagerImplTest { Pair("country", "US"), Pair("sandbox", "false"), ) - manager.execute("Test", attributes, null, null, null, null) - Assert.assertEquals(6, attributes.size) - Assert.assertEquals("(123) 456-9898", attributes["number"]) - Assert.assertEquals("55555", attributes["customerId"]) - Assert.assertEquals("Test1", attributes["lastname"]) - Assert.assertEquals("Test", attributes["test"]) - Assert.assertEquals("US", attributes["country"]) - Assert.assertEquals("false", attributes["sandbox"]) + val roktApi = manager.getRoktKitApi() + Assert.assertNotNull(roktApi) + roktApi!!.execute("Test", attributes, null, null, null, null) + val finalAttributes = mockedProvider.lastAttributes + Assert.assertNotNull(finalAttributes) + finalAttributes!! + Assert.assertEquals(6, finalAttributes.size) + Assert.assertEquals("(123) 456-9898", finalAttributes["number"]) + Assert.assertEquals("55555", finalAttributes["customerId"]) + Assert.assertEquals("Test1", finalAttributes["lastname"]) + Assert.assertEquals("Test", finalAttributes["test"]) + Assert.assertEquals("US", finalAttributes["country"]) + Assert.assertEquals("false", finalAttributes["sandbox"]) } @Test @@ -2104,7 +1541,9 @@ class KitManagerImplTest { fun testEvents_noProviders_returnsEmptyFlow() { val manager: KitManagerImpl = MockKitManagerImpl() - val result = manager.events("test-identifier") + val roktApi = manager.getRoktKitApi() + assertNull(roktApi) + val result = roktApi?.events("test-identifier") ?: flowOf() runTest { val elements = result.toList() @@ -2125,7 +1564,9 @@ class KitManagerImplTest { put(1, nonRoktProvider) } - val result = manager.events("test-identifier") + val roktApi = manager.getRoktKitApi() + assertNull(roktApi) + val result = roktApi?.events("test-identifier") ?: flowOf() runTest { val elements = result.toList() @@ -2150,7 +1591,9 @@ class KitManagerImplTest { put(1, disabledRoktProvider) } - val result = manager.events("test-identifier") + val roktApi = manager.getRoktKitApi() + assertNull(roktApi) + val result = roktApi?.events("test-identifier") ?: flowOf() runTest { val elements = result.toList() @@ -2180,7 +1623,9 @@ class KitManagerImplTest { put(1, enabledRoktProvider) } - val result = manager.events(testIdentifier) + val roktApi = manager.getRoktKitApi() + Assert.assertNotNull(roktApi) + val result = roktApi!!.events(testIdentifier) verify(enabledRoktProvider as KitIntegration.RoktListener).events(testIdentifier) assertEquals(expectedFlow, result) @@ -2233,7 +1678,9 @@ class KitManagerImplTest { put(4, secondEnabledRoktProvider) } - val result = manager.events(testIdentifier) + val roktApi = manager.getRoktKitApi() + Assert.assertNotNull(roktApi) + val result = roktApi!!.events(testIdentifier) verify(enabledRoktProvider as KitIntegration.RoktListener).events(testIdentifier) verify(secondEnabledRoktProvider as KitIntegration.RoktListener, never()).events(any()) @@ -2259,7 +1706,9 @@ class KitManagerImplTest { put(1, exceptionRoktProvider) } - val result = manager.events("test-identifier") + val roktApi = manager.getRoktKitApi() + Assert.assertNotNull(roktApi) + val result = roktApi!!.events("test-identifier") runTest { val elements = result.toList() @@ -2268,7 +1717,7 @@ class KitManagerImplTest { } @Test - fun testEvents_providerThrowsException_continuesToNextProvider() { + fun testEvents_providerThrowsException_returnsEmptyFlowWithoutFallback() { val manager: KitManagerImpl = MockKitManagerImpl() val exceptionRoktProvider = @@ -2300,16 +1749,25 @@ class KitManagerImplTest { put(2, workingRoktProvider) } - val result = manager.events(testIdentifier) + val roktApi = manager.getRoktKitApi() + Assert.assertNotNull(roktApi) + val result = roktApi!!.events(testIdentifier) - verify(workingRoktProvider as KitIntegration.RoktListener).events(testIdentifier) - assertEquals(expectedFlow, result) + verify(workingRoktProvider as KitIntegration.RoktListener, never()).events(any()) + runTest { + val elements = result.toList() + assertTrue(elements.isEmpty()) + } } internal inner class MockProvider( val config: KitConfiguration, ) : KitIntegration(), KitIntegration.RoktListener { + var lastAttributes: Map? = null + var lastOptions: PlacementOptions? = null + var lastUser: FilteredMParticleUser? = null + override fun isDisabled(): Boolean = false override fun getName(): String = "FakeProvider" @@ -2337,6 +1795,9 @@ class KitManagerImplTest { config: RoktConfig?, options: PlacementOptions?, ) { + lastAttributes = attributes.toMap() + lastOptions = options + lastUser = user Logger.info("Executed with $attributes and options $options") } diff --git a/android-kit-base/src/test/kotlin/com/mparticle/kits/RoktKitApiImplTest.kt b/android-kit-base/src/test/kotlin/com/mparticle/kits/RoktKitApiImplTest.kt new file mode 100644 index 000000000..a27086516 --- /dev/null +++ b/android-kit-base/src/test/kotlin/com/mparticle/kits/RoktKitApiImplTest.kt @@ -0,0 +1,167 @@ +package com.mparticle.kits + +import com.mparticle.MParticle +import com.mparticle.identity.IdentityApi +import com.mparticle.internal.MPUtility +import com.mparticle.mock.MockMParticle +import com.mparticle.rokt.PlacementOptions +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.test.runTest +import org.json.JSONObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.Mockito.withSettings + +class RoktKitApiImplTest { + @Before + fun setUp() { + val identityApi = mock(IdentityApi::class.java) + val instance = MockMParticle() + instance.setIdentityApi(identityApi) + MParticle.setInstance(instance) + } + + @Test + fun testExecute_mapsAttributesAndAddsSandbox() { + val kitConfig = KitConfiguration.createKitConfiguration(JSONObject().put("id", 42)) + val settingsMap = + hashMapOf( + "placementAttributesMapping" to + """ + [ + {"map": "number", "value": "no"}, + {"map": "customerId", "value": "minorcatid"} + ] + """.trimIndent(), + ) + val field = KitConfiguration::class.java.getDeclaredField("settings") + field.isAccessible = true + field.set(kitConfig, settingsMap) + + 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 attributes = + hashMapOf( + "number" to "(123) 456-9898", + "customerId" to "55555", + "country" to "US", + ) + + roktApi.execute("Test", attributes, null, null, null, null, null) + + @Suppress("UNCHECKED_CAST") + val attributesCaptor = ArgumentCaptor.forClass(Map::class.java) as ArgumentCaptor> + verify(roktListener).execute( + any(), + attributesCaptor.capture(), + any(), + any(), + any(), + any(), + any(), + any(), + ) + val captured = attributesCaptor.value + assertEquals("(123) 456-9898", captured["no"]) + assertEquals("55555", captured["minorcatid"]) + assertEquals("US", captured["country"]) + assertEquals(MPUtility.isDevEnv().toString(), captured["sandbox"]) + } + + @Test + fun testExecute_passesPlacementOptions() { + 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 placementOptions = PlacementOptions(jointSdkSelectPlacements = 123L) + + roktApi.execute("Test", emptyMap(), null, null, null, null, placementOptions) + + val optionsCaptor = ArgumentCaptor.forClass(PlacementOptions::class.java) + verify(roktListener).execute( + any(), + any(), + any(), + any(), + any(), + any(), + any(), + optionsCaptor.capture(), + ) + assertEquals(placementOptions, optionsCaptor.value) + } + + @Test + fun testEvents_returnsEmptyFlowWhenProviderThrows() = runTest { + 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 + `when`(roktListener.events(any())).thenThrow(RuntimeException("Test exception")) + val roktApi = RoktKitApiImpl(roktListener, kitIntegration) + + val result = roktApi.events("test-identifier") + + assertTrue(result.toList().isEmpty()) + } + + @Test + fun testPrepareAttributesAsync_delegatesToEnrichAttributes() { + val kitConfig = KitConfiguration.createKitConfiguration(JSONObject().put("id", 42)) + val settingsMap = + hashMapOf( + "placementAttributesMapping" to + """ + [ + {"map": "number", "value": "no"} + ] + """.trimIndent(), + ) + val field = KitConfiguration::class.java.getDeclaredField("settings") + field.isAccessible = true + field.set(kitConfig, settingsMap) + + 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) + + roktApi.prepareAttributesAsync(mapOf("number" to "(123) 456-9898")) + + @Suppress("UNCHECKED_CAST") + val attributesCaptor = ArgumentCaptor.forClass(Map::class.java) as ArgumentCaptor> + verify(roktListener).enrichAttributes(attributesCaptor.capture(), any()) + val captured = attributesCaptor.value + assertEquals("(123) 456-9898", captured["no"]) + assertEquals(MPUtility.isDevEnv().toString(), captured["sandbox"]) + } +} From 6923d9a6f62e14d5161179d5a7842a111e9c7852 Mon Sep 17 00:00:00 2001 From: Thomson Thomas Date: Mon, 2 Feb 2026 16:38:40 -0500 Subject: [PATCH 4/4] Ktlint fixes --- .trunk/trunk.yaml | 2 +- .../com/mparticle/rokt/PlacementOptions.kt | 5 +-- .../com/mparticle/kits/RoktKitApiImpl.kt | 39 +++++++------------ 3 files changed, 17 insertions(+), 29 deletions(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index fbd0abc7d..ece881dd8 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -29,7 +29,7 @@ lint: - actionlint@1.7.7 - checkov@3.2.470 - git-diff-check - - ktlint@0.48.2 + - ktlint@1.7.1 - markdownlint@0.45.0 - prettier@3.6.2 - shellcheck@0.11.0 diff --git a/android-core/src/main/kotlin/com/mparticle/rokt/PlacementOptions.kt b/android-core/src/main/kotlin/com/mparticle/rokt/PlacementOptions.kt index 9b3bfec7f..7699ccd84 100644 --- a/android-core/src/main/kotlin/com/mparticle/rokt/PlacementOptions.kt +++ b/android-core/src/main/kotlin/com/mparticle/rokt/PlacementOptions.kt @@ -1,6 +1,3 @@ package com.mparticle.rokt -data class PlacementOptions( - val jointSdkSelectPlacements: Long, - val dynamicPerformanceMarkers: MutableMap = mutableMapOf(), -) +data class PlacementOptions(val jointSdkSelectPlacements: Long, val dynamicPerformanceMarkers: MutableMap = mutableMapOf()) diff --git a/android-kit-base/src/main/kotlin/com/mparticle/kits/RoktKitApiImpl.kt b/android-kit-base/src/main/kotlin/com/mparticle/kits/RoktKitApiImpl.kt index f5bfcc646..7e57b2a88 100644 --- a/android-kit-base/src/main/kotlin/com/mparticle/kits/RoktKitApiImpl.kt +++ b/android-kit-base/src/main/kotlin/com/mparticle/kits/RoktKitApiImpl.kt @@ -26,10 +26,7 @@ import java.util.Objects * This class handles user resolution and attribute preparation before delegating * to the underlying Rokt Kit implementation. */ -internal class RoktKitApiImpl( - private val roktListener: KitIntegration.RoktListener, - private val kitIntegration: KitIntegration, -) : RoktKitApi { +internal class RoktKitApiImpl(private val roktListener: KitIntegration.RoktListener, private val kitIntegration: KitIntegration) : RoktKitApi { override fun execute( viewName: String, @@ -70,14 +67,12 @@ internal class RoktKitApiImpl( } } - override fun events(identifier: String): Flow { - return try { - Logger.verbose("Calling events for Rokt Kit with identifier: $identifier") - roktListener.events(identifier) - } catch (e: Exception) { - Logger.warning("Failed to call events for Rokt Kit: ${e.message}") - flowOf() - } + override fun events(identifier: String): Flow = try { + Logger.verbose("Calling events for Rokt Kit with identifier: $identifier") + roktListener.events(identifier) + } catch (e: Exception) { + Logger.warning("Failed to call events for Rokt Kit: ${e.message}") + flowOf() } override fun purchaseFinalized(placementId: String, catalogItemId: String, status: Boolean) { @@ -104,13 +99,11 @@ internal class RoktKitApiImpl( } } - override fun getSessionId(): String? { - return try { - roktListener.sessionId - } catch (e: Exception) { - Logger.warning("Failed to call getSessionId for Rokt Kit: ${e.message}") - null - } + override fun getSessionId(): String? = try { + roktListener.sessionId + } catch (e: Exception) { + Logger.warning("Failed to call getSessionId for Rokt Kit: ${e.message}") + null } override fun prepareAttributesAsync(attributes: Map) { @@ -149,10 +142,7 @@ internal class RoktKitApiImpl( return null } - private fun prepareAttributes( - finalAttributes: MutableMap, - user: MParticleUser?, - ): MutableMap { + private fun prepareAttributes(finalAttributes: MutableMap, user: MParticleUser?): MutableMap { val kitConfig = kitIntegration.configuration val jsonArray = try { kitConfig?.placementAttributesMapping ?: org.json.JSONArray() @@ -227,7 +217,8 @@ internal class RoktKitApiImpl( } else if (hashedEmailMismatch && existingHashedEmail != null) { // If there's an existing other but it doesn't match the passed-in hashed email, log a warning Logger.warning( - "The existing hashed email on the user ($existingHashedEmail) does not match the hashed email passed to selectPlacements ($hashedEmail). " + + "The existing hashed email on the user ($existingHashedEmail) does not match " + + "the hashed email passed to selectPlacements ($hashedEmail). " + "Please make sure to sync the hashed email identity to mParticle as soon as it's available. " + "Identifying user with the provided hashed email before continuing to selectPlacements.", )