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/gradle.properties b/gradle.properties index 1bcb3decf..bfff37d77 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,5 +12,5 @@ JAVA_VERSION=17 # SDK VERSION) and are the single source of truth shared by the Rokt kit (com.rokt:roktsdk) and # the rokt-sdk-plus umbrella (com.rokt:payment-extension). Bump together when adopting a new # Rokt SDK release. -roktSdkVersion=6.0.3 -roktPaymentExtensionVersion=6.0.3 +roktSdkVersion=6.1.0 +roktPaymentExtensionVersion=6.1.0 diff --git a/kits/rokt/rokt/build.gradle b/kits/rokt/rokt/build.gradle index 1595b5542..dc1c739a9 100644 --- a/kits/rokt/rokt/build.gradle +++ b/kits/rokt/rokt/build.gradle @@ -82,7 +82,7 @@ dependencies { implementation 'androidx.annotation:annotation:1.5.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0' implementation 'androidx.compose.runtime:runtime' - api "com.rokt:roktsdk:${project.findProperty('roktSdkVersion') ?: '6.0.3'}" + api "com.rokt:roktsdk:${project.findProperty('roktSdkVersion') ?: '6.1.0'}" api "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" testImplementation files('libs/java-json.jar') 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 81b7e73fc..5b97eba92 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 @@ -147,6 +148,41 @@ 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) { + MParticle.logRoktApiUsage("ROKT_SET_SESSION") + 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? { + MParticle.logRoktApiUsage("ROKT_GET_SESSION") + return if (isEnabled()) { + resolveRoktKit()?.second?.getSession() + } else { + null + } + } + /** * Set the session id to use for the next execute call. * @@ -154,9 +190,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) { MParticle.logRoktApiUsage("ROKT_SET_SESSION_ID") if (isEnabled()) { @@ -167,8 +205,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? { MParticle.logRoktApiUsage("ROKT_GET_SESSION_ID") return if (isEnabled()) { 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 81aafe141..49a51d429 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 @@ -19,6 +19,7 @@ import com.mparticle.kits.KitIntegration.CommerceListener import com.mparticle.kits.KitIntegration.IdentityListener import com.mparticle.kits.KitIntegration.RoktListener import com.mparticle.rokt.RoktApiDiagnosticsForwarder +import com.mparticle.rokt.RoktSession import com.rokt.roktsdk.PlacementOptions import com.rokt.roktsdk.Rokt import com.rokt.roktsdk.Rokt.SdkFrameworkType.Android @@ -39,6 +40,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" @@ -337,13 +339,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) } @@ -351,8 +395,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 64b34db54..dc2f6d595 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 @@ -1499,6 +1499,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 diff --git a/rokt-sdk-plus/build.gradle b/rokt-sdk-plus/build.gradle index 044e91a18..7a11385c4 100644 --- a/rokt-sdk-plus/build.gradle +++ b/rokt-sdk-plus/build.gradle @@ -15,7 +15,7 @@ apply plugin: 'mparticle.android.library.publish' // The umbrella tracks the mParticle SDK release line; the Rokt artifacts ride their own line. def mpVersion = (project.findProperty('VERSION') ?: '0.0.0').toString() def roktPaymentExtensionVersion = - (project.findProperty('roktPaymentExtensionVersion') ?: '6.0.3').toString() + (project.findProperty('roktPaymentExtensionVersion') ?: '6.1.0').toString() mparticleMavenPublish { groupId.set('com.rokt')