Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Added

- Add Rokt Shoppable Ads payment extension registration and selection APIs.

### Changed

- Add support for qualified alpha, beta, and release candidate versions in release workflows.
Expand Down
25 changes: 25 additions & 0 deletions kits/rokt/rokt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,31 @@ Java consumers can use the kit helper:
MParticleRokt.Rokt().selectPlacements("RoktExperience", attributes);
```

### Shoppable Ads

Add the optional Rokt payment extension dependency in your app, then register the extension after mParticle starts. The Rokt kit reads `stripePublishableKey` from the mParticle dashboard configuration and forwards it to the Rokt SDK during registration.

Kotlin:

```kotlin
import com.mparticle.MParticle
import com.mparticle.kits.rokt
import com.rokt.payment.extension.StripePaymentExtension

MParticle.getInstance()?.rokt?.registerPaymentExtension(StripePaymentExtension())
MParticle.getInstance()?.rokt?.selectShoppableAds(
identifier = "RoktShoppableExperience",
attributes = attributes,
)
```

Java:

```java
MParticleRokt.Rokt().registerPaymentExtension(new StripePaymentExtension());
MParticleRokt.Rokt().selectShoppableAds("RoktShoppableExperience", attributes);
```

Compose integrations can receive native Rokt SDK events from `RoktLayout`:

```kotlin
Expand Down
2 changes: 1 addition & 1 deletion kits/rokt/rokt/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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:6.0.1-rc.1'
api 'com.rokt:roktsdk:6.0.1-rc.2'
api "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"

testImplementation files('libs/java-json.jar')
Expand Down
51 changes: 51 additions & 0 deletions kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/Rokt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.rokt.roktsdk.PlacementOptions
import com.rokt.roktsdk.RoktConfig
import com.rokt.roktsdk.RoktEvent
import com.rokt.roktsdk.payment.PaymentExtension
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import java.lang.ref.WeakReference
Expand Down Expand Up @@ -47,7 +48,7 @@
options = buildPlacementOptions(),
)
} else {
Logger.warning("Rokt Kit is not available. Make sure the Rokt Kit is included in your app.")

Check failure on line 51 in kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/Rokt.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "Rokt Kit is not available. Make sure the Rokt Kit is included in your app." 3 times.

See more on https://sonarcloud.io/project/issues?id=mParticle_mparticle-android-sdk&issues=AZ5qi0Tah7vbYupDtS2N&open=AZ5qi0Tah7vbYupDtS2N&pullRequest=713
}
}
}
Expand All @@ -64,6 +65,56 @@
flowOf()
}

/**
* Register a payment extension for Shoppable Ads.
*
* The Rokt Kit adds mParticle dashboard configuration before forwarding to the Rokt SDK.
*
* @param paymentExtension The payment extension implementation to register
* @return true if the Rokt SDK accepts the payment extension configuration
*/
fun registerPaymentExtension(paymentExtension: PaymentExtension): Boolean = if (isEnabled()) {
val resolved = resolveRoktKit()
if (resolved != null) {
resolved.second.registerPaymentExtension(paymentExtension)
} else {
Logger.warning("Rokt Kit is not available. Make sure the Rokt Kit is included in your app.")
false
}
} else {
false
}

/**
* Display a Rokt Shoppable Ads placement with the specified parameters.
*
* @param identifier The placement identifier
* @param attributes User attributes to pass to Rokt
* @param config Optional Rokt configuration
*/
@JvmOverloads
fun selectShoppableAds(
identifier: String,
attributes: Map<String, String> = emptyMap(),
config: RoktConfig? = null,
) {
if (isEnabled()) {
val resolved = resolveRoktKit()
if (resolved != null) {
val (kitIntegration, roktListener) = resolved
RoktKitRequestHelper.selectShoppableAds(
kitIntegration = kitIntegration,
roktListener = roktListener,
viewName = identifier,
attributes = HashMap(attributes),
config = config,
)
} else {
Logger.warning("Rokt Kit is not available. Make sure the Rokt Kit is included in your app.")
}
}
}

/**
* Notify Rokt that a purchase has been finalized.
*
Expand Down
42 changes: 39 additions & 3 deletions kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/RoktKit.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import com.rokt.roktsdk.RoktEvent
import com.rokt.roktsdk.RoktWidgetDimensionCallBack
import com.rokt.roktsdk.Widget
import com.rokt.roktsdk.logging.RoktLogLevel
import com.rokt.roktsdk.payment.PaymentExtension
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -230,6 +231,12 @@ class RoktKit :
private fun prepareFinalAttributes(
filterUser: FilteredMParticleUser?,
attributes: Map<String, String>,
): Map<String, String> = prepareFinalAttributesForEvent(filterUser, attributes, EVENT_NAME_SELECT_PLACEMENTS)

private fun prepareFinalAttributesForEvent(
filterUser: FilteredMParticleUser?,
attributes: Map<String, String>,
eventName: String,
): Map<String, String> {
val finalAttributes = mutableMapOf<String, String>()

Expand All @@ -251,7 +258,7 @@ class RoktKit :

verifyHashedEmail(finalAttributes)

logSelectPlacementEvent(finalAttributes)
logRoktEvent(eventName, finalAttributes)
return finalAttributes
}

Expand Down Expand Up @@ -279,6 +286,32 @@ class RoktKit :

override fun events(identifier: String): Flow<RoktEvent> = Rokt.events(identifier)

override fun registerPaymentExtension(paymentExtension: PaymentExtension): Boolean {
val stripeKey = configuration?.settings?.get(STRIPE_PUBLISHABLE_KEY)
val paymentConfig = if (stripeKey.isNullOrEmpty()) {
emptyMap()
} else {
mapOf(STRIPE_KEY_ALIAS to stripeKey)
}
return Rokt.registerPaymentExtension(paymentExtension, paymentConfig)
}

override fun selectShoppableAds(
viewName: String,
attributes: Map<String, String>,
filterUser: FilteredMParticleUser?,
roktConfig: RoktConfig?,
) {
val finalAttributes = prepareFinalAttributesForEvent(filterUser, attributes, EVENT_NAME_SELECT_SHOPPABLE_ADS)

Rokt.selectShoppableAds(
identifier = viewName,
attributes = finalAttributes,
config = roktConfig,
eventCollector = null,
)
}

override fun setWrapperSdkVersion(wrapperSdkVersion: WrapperSdkVersion) {
val sdkFrameworkType = when (wrapperSdkVersion.sdk) {
WrapperSdk.WrapperFlutter -> Flutter
Expand Down Expand Up @@ -378,8 +411,8 @@ class RoktKit :
}
}

private fun logSelectPlacementEvent(attributes: Map<String, String>) {
val event = MPEvent.Builder(EVENT_NAME_SELECT_PLACEMENTS, MParticle.EventType.Other)
private fun logRoktEvent(eventName: String, attributes: Map<String, String>) {
val event = MPEvent.Builder(eventName, MParticle.EventType.Other)
.customAttributes(attributes)
.build()
MParticle.getInstance()?.logEvent(event)
Expand Down Expand Up @@ -423,8 +456,11 @@ class RoktKit :
const val NAME = "Rokt"
const val ROKT_ACCOUNT_ID = "accountId"
const val HASHED_EMAIL_USER_IDENTITY_TYPE = "hashedEmailUserIdentityType"
const val STRIPE_PUBLISHABLE_KEY = "stripePublishableKey"
const val STRIPE_KEY_ALIAS = "stripeKey"
const val MPID = "mpid"
const val EVENT_NAME_SELECT_PLACEMENTS = "selectPlacements"
const val EVENT_NAME_SELECT_SHOPPABLE_ADS = "selectShoppableAds"
const val NO_ROKT_ACCOUNT_ID = "No Rokt account ID provided, can't initialize kit."
const val NO_APP_VERSION_FOUND = "No App version found, can't initialize kit."
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.graphics.Typeface
import com.rokt.roktsdk.PlacementOptions
import com.rokt.roktsdk.RoktConfig
import com.rokt.roktsdk.RoktEvent
import com.rokt.roktsdk.payment.PaymentExtension
import kotlinx.coroutines.flow.Flow
import java.lang.ref.WeakReference

Expand All @@ -22,6 +23,15 @@ internal interface RoktKitBridge {

fun enrichAttributes(attributes: MutableMap<String, String>, user: FilteredMParticleUser?)

fun registerPaymentExtension(paymentExtension: PaymentExtension): Boolean

fun selectShoppableAds(
viewName: String,
attributes: Map<String, String>,
user: FilteredMParticleUser?,
config: RoktConfig?,
)

fun purchaseFinalized(identifier: String, catalogItemId: String, success: Boolean)

fun close()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,35 @@
}
}

fun selectShoppableAds(
kitIntegration: KitIntegration,
roktListener: RoktKitBridge,
viewName: String,
attributes: Map<String, String>,
config: RoktConfig?,
) {
val mutableAttributes = attributes.toMutableMap()
val instance = MParticle.getInstance()
if (instance == null) {
Logger.warning("MParticle instance is null, cannot execute Rokt Shoppable Ads 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, kitConfig)

Check warning on line 96 in kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/RoktKitRequestHelper.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make this collection immutable.

See more on https://sonarcloud.io/project/issues?id=mParticle_mparticle-android-sdk&issues=AZ5qi0Sxh7vbYupDtS2M&open=AZ5qi0Sxh7vbYupDtS2M&pullRequest=713
roktListener.selectShoppableAds(
viewName,
finalAttributes,
FilteredMParticleUser.getInstance(user?.id ?: 0L, kitIntegration),
config,
)
}
}

private fun getValueIgnoreCase(map: Map<String, String>, searchKey: String): String? {
for ((key, value) in map) {
if (key.equals(searchKey, ignoreCase = true)) {
Expand Down
105 changes: 105 additions & 0 deletions kits/rokt/rokt/src/test/kotlin/com/mparticle/kits/RoktKitTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ import com.mparticle.internal.CoreCallbacks.KitListener
import com.mparticle.kits.mocks.MockKitConfiguration
import com.rokt.roktsdk.FulfillmentAttributes
import com.rokt.roktsdk.Rokt
import com.rokt.roktsdk.RoktConfig
import com.rokt.roktsdk.RoktEvent
import com.rokt.roktsdk.logging.RoktLogLevel
import com.rokt.roktsdk.payment.PaymentExtension
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
Expand Down Expand Up @@ -1330,6 +1332,109 @@ class RoktKitTests {
)
}

@Test
fun testRegisterPaymentExtensionPassesStripeKeyFromKitConfiguration() {
val paymentExtension = mock(PaymentExtension::class.java)
var registrationConfig: Map<*, *>? = null
Mockito.`when`(paymentExtension.id).thenReturn("capturing-payment-extension")
Mockito.doAnswer {
registrationConfig = it.arguments[0] as Map<*, *>
true
}.`when`(paymentExtension).onRegister(Mockito.anyMap())
roktKit.configuration = MockKitConfiguration.createKitConfiguration(
JSONObject()
.put("as", JSONObject().put("stripePublishableKey", "pk_test_123"))
.put("hs", JSONObject()),
)

val result = roktKit.registerPaymentExtension(paymentExtension)

assertTrue(result)
assertEquals(mapOf("stripeKey" to "pk_test_123"), registrationConfig)
}

@Test
fun testRegisterPaymentExtensionPassesEmptyConfigWhenStripeKeyAbsent() {
val paymentExtension = mock(PaymentExtension::class.java)
var registrationConfig: Map<*, *>? = null
Mockito.`when`(paymentExtension.id).thenReturn("capturing-payment-extension")
Mockito.doAnswer {
registrationConfig = it.arguments[0] as Map<*, *>
false
}.`when`(paymentExtension).onRegister(Mockito.anyMap())
roktKit.configuration = MockKitConfiguration.createKitConfiguration(JSONObject().put("hs", JSONObject()))

val result = roktKit.registerPaymentExtension(paymentExtension)

assertFalse(result)
assertTrue(registrationConfig?.isEmpty() == true)
}

@Test
fun testSelectShoppableAdsInvokesRoktAndLogsEvent() {
mockkObject(Rokt)
try {
val capturedAttributesSlot = slot<Map<String, String>>()
every {
Rokt.selectPlacements(
any<String>(),
capture(capturedAttributesSlot),
isNull(),
isNull(),
isNull(),
any(),
isNull(),
)
} just runs

val mockFilterUser = mock(FilteredMParticleUser::class.java)
val userIdentities = HashMap<IdentityType, String>()
userIdentities[IdentityType.Email] = "test@example.com"
Mockito.`when`(mockFilterUser.userIdentities).thenReturn(userIdentities)
Mockito.`when`(mockFilterUser.id).thenReturn(9876L)
val userAttributes = HashMap<String, Any>()
userAttributes["user_key"] = "user_val"
Mockito.`when`(mockFilterUser.userAttributes).thenReturn(userAttributes)
roktKit.configuration = MockKitConfiguration.createKitConfiguration(JSONObject().put("hs", JSONObject()))
val config = RoktConfig.Builder().colorMode(RoktConfig.ColorMode.DARK).build()

roktKit.selectShoppableAds(
viewName = "ShopView",
attributes = mapOf("attr1" to "val1"),
filterUser = mockFilterUser,
roktConfig = config,
)

assertEquals(
mapOf(
"user_key" to "user_val",
"attr1" to "val1",
"mpid" to "9876",
"email" to "test@example.com",
"adsExperience" to "shoppable",
),
capturedAttributesSlot.captured,
)

val eventCaptor = ArgumentCaptor.forClass(MPEvent::class.java)
Mockito.verify(MParticle.getInstance()!!).logEvent(eventCaptor.capture())
val loggedEvent = eventCaptor.value
assertEquals("selectShoppableAds", loggedEvent.eventName)
assertEquals(MParticle.EventType.Other, loggedEvent.eventType)
assertEquals(
mapOf(
"user_key" to "user_val",
"attr1" to "val1",
"mpid" to "9876",
"email" to "test@example.com",
),
loggedEvent.customAttributes,
)
} finally {
unmockkObject(Rokt)
}
}

@Test
fun test_execute_logsMPEventWhenMParticleInstanceIsNull() {
// Arrange
Expand Down
Loading
Loading