diff --git a/android-core/src/main/kotlin/com/mparticle/rokt/RoktSession.kt b/android-core/src/main/kotlin/com/mparticle/rokt/RoktSession.kt new file mode 100644 index 000000000..2d273ae3d --- /dev/null +++ b/android-core/src/main/kotlin/com/mparticle/rokt/RoktSession.kt @@ -0,0 +1,17 @@ +package com.mparticle.rokt + +/** + * A Rokt session suitable for handoff between native and non-native integrations. + * + * Includes the session id plus an optional short-lived session token used to authorize offers and + * events. Mirrors Web launcher options: `sessionId` is required for handoff; `sessionToken` is + * optional (Bearer continuity when present). + * + * @param sessionId The Rokt session identifier. Must be non-empty when passed to [com.mparticle.kits.Rokt.setSession]. + * @param sessionToken Optional JWT session token used as a Bearer credential for offers and events. + * When omitted or blank, [com.mparticle.kits.Rokt.setSession] applies the session id only (same as + * Web `sessionId` without `sessionToken`). + * @param expiresAt Optional Unix epoch milliseconds when [sessionToken] expires (matches server + * `expires_at` when known). Ignored when [sessionToken] is absent. + */ +data class RoktSession @JvmOverloads constructor(val sessionId: String, val sessionToken: String? = null, val expiresAt: Long? = null) diff --git a/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/Rokt.kt b/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/Rokt.kt index 72354c919..959e6c8fe 100644 --- a/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/Rokt.kt +++ b/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/Rokt.kt @@ -4,6 +4,7 @@ import android.graphics.Typeface import com.mparticle.MParticle import com.mparticle.internal.KitManager import com.mparticle.internal.Logger +import com.mparticle.rokt.RoktSession import com.rokt.roktsdk.PlacementOptions import com.rokt.roktsdk.RoktConfig import com.rokt.roktsdk.RoktEvent @@ -137,6 +138,37 @@ class Rokt internal constructor(private val mKitManager: KitManager) { } } + /** + * Set the session to use for the next execute call. + * + * Use this when you have a session from a non-native integration (e.g. WebView) + * and want the session to stay consistent across integrations. Call before the next + * selectPlacements. + * + * Matches Web launcher options: pass [RoktSession.sessionId] with optional + * [RoktSession.sessionToken]. When the token is present, offers/events can send + * `Authorization: Bearer`. When only the id is present, the id is applied without Bearer + * seeding. Empty `sessionId` (or token without id) is ignored. + * + * @param session The session id and optional JWT session token (optional expiry). + */ + fun setSession(session: RoktSession) { + if (isEnabled()) { + resolveRoktKit()?.second?.setSession(session) + } + } + + /** + * Get the current session (id + token) for use within a non-native integration e.g. WebView. + * + * @return The session, or null if disabled, no session is present, or the token has expired. + */ + fun getSession(): RoktSession? = if (isEnabled()) { + resolveRoktKit()?.second?.getSession() + } else { + null + } + /** * Set the session id to use for the next execute call. * @@ -144,9 +176,11 @@ class Rokt internal constructor(private val mKitManager: KitManager) { * e.g. WebView, and you want the session to be consistent across integrations. * * **Note:** Empty strings are ignored and will not update the session. + * Prefer [setSession] so the session token is also applied for offers and events. * * @param sessionId The session id to be set. Must be a non-empty string. */ + @Deprecated("Use setSession to set session id and session token.") fun setSessionId(sessionId: String) { if (isEnabled()) { resolveRoktKit()?.second?.setSessionId(sessionId) @@ -156,8 +190,11 @@ class Rokt internal constructor(private val mKitManager: KitManager) { /** * Get the session id to use within a non-native integration e.g. WebView. * + * Prefer [getSession] to also read the session token. + * * @return The session id or null if no session is present or SDK is not initialized. */ + @Deprecated("Use getSession to read session id and session token.") fun getSessionId(): String? = if (isEnabled()) { resolveRoktKit()?.second?.getSessionId() } else { diff --git a/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/RoktKit.kt b/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/RoktKit.kt index 08a0ee069..4e6b24038 100644 --- a/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/RoktKit.kt +++ b/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/RoktKit.kt @@ -18,6 +18,7 @@ import com.mparticle.internal.Logger import com.mparticle.kits.KitIntegration.CommerceListener import com.mparticle.kits.KitIntegration.IdentityListener import com.mparticle.kits.KitIntegration.RoktListener +import com.mparticle.rokt.RoktSession import com.rokt.roktsdk.PlacementOptions import com.rokt.roktsdk.Rokt import com.rokt.roktsdk.Rokt.SdkFrameworkType.Android @@ -38,6 +39,7 @@ import kotlinx.coroutines.launch import java.lang.ref.WeakReference import java.math.BigDecimal import java.net.URL +import com.rokt.roktsdk.RoktSession as NativeRoktSession const val ROKT_ATTRIBUTE_SANDBOX_MODE: String = "sandbox" @@ -330,13 +332,55 @@ class RoktKit : Rokt.close() } + /** + * Set the session to use for the next execute call. + * + * Matches Web launcher options: non-empty [RoktSession.sessionId] + [RoktSession.sessionToken] + * seeds Bearer continuity via [Rokt.setSession]; id-only falls back to [Rokt.setSessionId]. + * Token without a non-empty id is ignored. Requires a Rokt Android SDK version that exposes + * [Rokt.setSession] / [NativeRoktSession]. + */ + override fun setSession(session: RoktSession) { + val sessionId = session.sessionId.trim() + if (sessionId.isEmpty()) { + return + } + val sessionToken = session.sessionToken?.trim().orEmpty() + if (sessionToken.isNotEmpty()) { + Rokt.setSession( + NativeRoktSession( + sessionId = sessionId, + sessionToken = sessionToken, + expiresAt = session.expiresAt, + ), + ) + } else { + Rokt.setSessionId(sessionId) + } + } + + /** + * Get the current session (id + token) for WebView / non-native handoff. + */ + override fun getSession(): RoktSession? { + val session = Rokt.getSession() ?: return null + return RoktSession( + sessionId = session.sessionId, + sessionToken = session.sessionToken, + expiresAt = session.expiresAt, + ) + } + /** * 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. * + * Prefer [setSession] so the session token is also applied for offers and events. + * * @param sessionId The session id to be set. Must be a non-empty string. */ + @Deprecated("Use setSession to set session id and session token.") override fun setSessionId(sessionId: String) { Rokt.setSessionId(sessionId) } @@ -344,8 +388,11 @@ class RoktKit : /** * Get the session id to use within a non-native integration e.g. WebView. * + * Prefer [getSession] to also read the session token. + * * @return The session id or null if no session is present. */ + @Deprecated("Use getSession to read session id and session token.") override fun getSessionId(): String? = Rokt.getSessionId() override fun enrichAttributes(attributes: MutableMap, user: FilteredMParticleUser?) { diff --git a/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/RoktKitBridge.kt b/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/RoktKitBridge.kt index 7a36b0230..945381b07 100644 --- a/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/RoktKitBridge.kt +++ b/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/RoktKitBridge.kt @@ -1,6 +1,7 @@ package com.mparticle.kits import android.graphics.Typeface +import com.mparticle.rokt.RoktSession import com.rokt.roktsdk.PlacementOptions import com.rokt.roktsdk.RoktConfig import com.rokt.roktsdk.RoktEvent @@ -36,6 +37,10 @@ internal interface RoktKitBridge { fun close() + fun setSession(session: RoktSession) + + fun getSession(): RoktSession? + fun setSessionId(sessionId: String) fun getSessionId(): String? diff --git a/kits/rokt/rokt/src/test/kotlin/com/mparticle/kits/RoktKitTests.kt b/kits/rokt/rokt/src/test/kotlin/com/mparticle/kits/RoktKitTests.kt index 228e39df1..5efe64707 100644 --- a/kits/rokt/rokt/src/test/kotlin/com/mparticle/kits/RoktKitTests.kt +++ b/kits/rokt/rokt/src/test/kotlin/com/mparticle/kits/RoktKitTests.kt @@ -1488,6 +1488,61 @@ class RoktKitTests { unmockkObject(Rokt) } + @Test + fun testSetSession_delegatesToRoktSdk() { + mockkObject(Rokt) + every { Rokt.setSession(any()) } just runs + + roktKit.setSession(com.mparticle.rokt.RoktSession("sid", "jwt", 123L)) + + verify { + Rokt.setSession( + match { + it.sessionId == "sid" && it.sessionToken == "jwt" && it.expiresAt == 123L + }, + ) + } + verify(exactly = 0) { Rokt.setSessionId(any()) } + unmockkObject(Rokt) + } + + @Test + fun testSetSession_idOnlyFallsBackToSetSessionId() { + mockkObject(Rokt) + every { Rokt.setSessionId(any()) } just runs + + roktKit.setSession(com.mparticle.rokt.RoktSession("sid")) + + verify { Rokt.setSessionId("sid") } + verify(exactly = 0) { Rokt.setSession(any()) } + unmockkObject(Rokt) + } + + @Test + fun testSetSession_blankIdIsIgnored() { + mockkObject(Rokt) + + roktKit.setSession(com.mparticle.rokt.RoktSession(" ", "jwt")) + + verify(exactly = 0) { Rokt.setSession(any()) } + verify(exactly = 0) { Rokt.setSessionId(any()) } + unmockkObject(Rokt) + } + + @Test + fun testGetSession_mapsValueFromRoktSdk() { + mockkObject(Rokt) + every { Rokt.getSession() } returns com.rokt.roktsdk.RoktSession("sid", "jwt", 456L) + + val result = roktKit.getSession() + + assertEquals("sid", result?.sessionId) + assertEquals("jwt", result?.sessionToken) + assertEquals(456L, result?.expiresAt) + verify { Rokt.getSession() } + unmockkObject(Rokt) + } + @Test fun testSetSessionId_delegatesToRoktSdk() { mockkObject(Rokt) diff --git a/kits/rokt/rokt/src/test/kotlin/com/mparticle/kits/RoktTest.kt b/kits/rokt/rokt/src/test/kotlin/com/mparticle/kits/RoktTest.kt index 88af7f60c..3aac2ea33 100644 --- a/kits/rokt/rokt/src/test/kotlin/com/mparticle/kits/RoktTest.kt +++ b/kits/rokt/rokt/src/test/kotlin/com/mparticle/kits/RoktTest.kt @@ -296,6 +296,40 @@ class RoktTest { verify(roktListener, never()).selectShoppableAds(any(), any(), any(), any()) } + @Test + fun testSetSession_whenEnabled_delegatesToKitManager() { + configManager.enabled = true + val session = com.mparticle.rokt.RoktSession("sid", "jwt", 123L) + rokt.setSession(session) + verify(roktListener).setSession(session) + } + + @Test + fun testSetSession_whenDisabled_doesNotCallKitManager() { + configManager.enabled = false + val session = com.mparticle.rokt.RoktSession("sid", "jwt", 123L) + rokt.setSession(session) + verify(roktListener, never()).setSession(any()) + } + + @Test + fun testGetSession_whenEnabled_delegatesToKitManager() { + configManager.enabled = true + val expected = com.mparticle.rokt.RoktSession("sid", "jwt", 123L) + `when`(roktListener.getSession()).thenReturn(expected) + val result = rokt.getSession() + verify(roktListener).getSession() + assertEquals(expected, result) + } + + @Test + fun testGetSession_whenDisabled_returnsNull() { + configManager.enabled = false + val result = rokt.getSession() + verify(roktListener, never()).getSession() + assertNull(result) + } + @Test fun testSetSessionId_whenEnabled_delegatesToKitManager() { configManager.enabled = true