Skip to content
Merged
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
9 changes: 9 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(adb logcat:*)",
"Bash(./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release)",
"Bash(npx detox:*)"
]
}
}
89 changes: 89 additions & 0 deletions demo-app/src/main/java/com/personalization/demo/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.google.firebase.FirebaseApp
import com.personalization.Params
import com.personalization.Params.TrackEvent
import com.personalization.SDK
import com.personalization.api.OnApiCallbackListener
import com.personalization.api.params.ProductItemParams
import com.personalization.api.params.PurchasePredictParams
import com.personalization.demo.BuildConfig
import com.personalization.sdk.data.models.dto.popUp.Components
import org.json.JSONObject
Expand All @@ -32,6 +36,12 @@ class MainActivity : AppCompatActivity() {
const val COLLISION_PLACEHOLDER_VALUE = "collision_demo"
}

private object DemoProductViewConstants {
const val PRODUCT_ID = "demo-product-view-001"
const val DEMO_PRICE = 2499.99
const val DEMO_AMOUNT = 1
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Expand Down Expand Up @@ -73,6 +83,85 @@ class MainActivity : AppCompatActivity() {
findViewById<Button>(R.id.btnTrackEventCollision).setOnClickListener {
trackEventWithReservedKeyCollision()
}

findViewById<Button>(R.id.btnTrackViewNoParams).setOnClickListener {
trackProductViewIdOnly()
}

findViewById<Button>(R.id.btnTrackViewWithParams).setOnClickListener {
trackProductViewWithItemParams()
}

findViewById<Button>(R.id.btnPredictDidOnly).setOnClickListener {
predictPurchase(PurchasePredictParams())
}

findViewById<Button>(R.id.btnPredictWithEmail).setOnClickListener {
predictPurchase(
PurchasePredictParams(email = getString(R.string.predict_demo_email))
)
}
}

private fun predictPurchase(params: PurchasePredictParams) {
sdk.predictManager.getProbabilityToPurchase(
params = params,
onSuccess = { response ->
runOnUiThread {
Toast.makeText(
this,
getString(R.string.predict_ok, response.probability, response.clientId),
Toast.LENGTH_LONG
).show()
}
},
onError = { code, msg ->
runOnUiThread {
Toast.makeText(
this,
getString(R.string.predict_fail, code, msg ?: ""),
Toast.LENGTH_LONG
).show()
}
}
)
}

private fun trackProductViewIdOnly() {
sdk.trackEventManager.track(TrackEvent.VIEW, DemoProductViewConstants.PRODUCT_ID)
Toast.makeText(this, getString(R.string.track_view_no_params_queued), Toast.LENGTH_SHORT).show()
}

private fun trackProductViewWithItemParams() {
val item = ProductItemParams(DemoProductViewConstants.PRODUCT_ID)
.set(ProductItemParams.PARAMETER.PRICE, DemoProductViewConstants.DEMO_PRICE)
.set(ProductItemParams.PARAMETER.AMOUNT, DemoProductViewConstants.DEMO_AMOUNT)

sdk.trackEventManager.track(
TrackEvent.VIEW,
Params().put(item),
object : OnApiCallbackListener() {
override fun onSuccess(response: JSONObject?) {
runOnUiThread {
Toast.makeText(
this@MainActivity,
getString(R.string.track_view_ok),
Toast.LENGTH_LONG
).show()
}
}

override fun onError(code: Int, msg: String?) {
runOnUiThread {
Toast.makeText(
this@MainActivity,
"${getString(R.string.track_view_fail)}: $msg",
Toast.LENGTH_LONG
).show()
}
}
}
)
}

private fun trackEventWithCustomFieldsSuccess() {
Expand Down
28 changes: 28 additions & 0 deletions demo-app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,34 @@
android:text="@string/track_event_reserved_collision"
android:layout_marginTop="16dp" />

<Button
android:id="@+id/btnTrackViewNoParams"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/track_view_no_params"
android:layout_marginTop="24dp" />

<Button
android:id="@+id/btnTrackViewWithParams"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/track_view_with_params"
android:layout_marginTop="16dp" />

<Button
android:id="@+id/btnPredictDidOnly"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/predict_did_only"
android:layout_marginTop="24dp" />

<Button
android:id="@+id/btnPredictWithEmail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/predict_with_email"
android:layout_marginTop="16dp" />

</LinearLayout>


10 changes: 10 additions & 0 deletions demo-app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,20 @@
<string name="show_test_popup">Show Test Popup</string>
<string name="track_event_custom_fields">Track event (custom fields)</string>
<string name="track_event_reserved_collision">Track event (reserved key collision)</string>
<string name="track_view_no_params">Track view (product id only)</string>
<string name="track_view_with_params">Track view (with item params)</string>
<string name="track_view_no_params_queued">track(view): id only, request queued</string>
<string name="track_view_ok">track(view): sent</string>
<string name="track_view_fail">track(view) failed</string>
<string name="track_event_ok">trackEvent: request sent</string>
<string name="track_event_collision_ok">trackEvent: validation error (expected)</string>
<string name="track_event_fail">trackEvent failed</string>
<string name="track_event_unexpected_success">Unexpected: expected client validation failure</string>
<string name="predict_did_only">Predict purchase (did only)</string>
<string name="predict_with_email">Predict purchase (did + email)</string>
<string name="predict_demo_email">predict-demo@example.com</string>
<string name="predict_ok">probability=%1$.4f client_id=%2$s</string>
<string name="predict_fail">predict failed: %1$d %2$s</string>
</resources>


Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.personalization.api.OnApiCallbackListener
import com.personalization.api.managers.CartManager
import com.personalization.api.managers.InAppNotificationManager
import com.personalization.api.managers.ProductsManager
import com.personalization.api.managers.PredictManager
import com.personalization.api.managers.RecommendationManager
import com.personalization.api.managers.SearchManager
import com.personalization.api.managers.TrackEventManager
Expand Down Expand Up @@ -79,6 +80,9 @@ open class SDK {
@Inject
lateinit var searchManager: SearchManager

@Inject
lateinit var predictManager: PredictManager

@Inject
lateinit var inAppNotificationManager: InAppNotificationManager

Expand Down Expand Up @@ -495,7 +499,7 @@ open class SDK {
}

/**
* Custom event tracking (aligned with the iOS SDK).
* Custom event tracking.
*
* @param event Event key
* @param time Optional UNIX time in seconds
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.personalization.api.managers

import com.personalization.api.params.PurchasePredictParams
import com.personalization.api.responses.predict.PurchasePredictResponse

interface PredictManager {

/**
* GET predict/probability-to-purchase — predicted purchase probability for the current visitor.
*
* @param params Optional identifiers (email, phone, telegram_id, loyalty_id). At least one of
* `did` (from SDK) or these fields is required by the API.
*/
fun getProbabilityToPurchase(
params: PurchasePredictParams = PurchasePredictParams(),
onSuccess: (PurchasePredictResponse) -> Unit,
onError: (Int, String?) -> Unit = { _, _ -> }
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.personalization.api.params

import org.json.JSONObject

private const val QUERY_EMAIL = "email"
private const val QUERY_PHONE = "phone"
private const val QUERY_TELEGRAM_ID = "telegram_id"
private const val QUERY_LOYALTY_ID = "loyalty_id"

/**
* Optional query parameters for [com.personalization.api.managers.PredictManager.getProbabilityToPurchase].
* Device id and shop id are added by the SDK network layer.
*/
data class PurchasePredictParams(
val email: String? = null,
val phone: String? = null,
val telegramId: String? = null,
val loyaltyId: String? = null
) {

fun toQueryJson(): JSONObject {
val json = JSONObject()
email?.takeIf { it.isNotBlank() }?.let { json.put(QUERY_EMAIL, it) }
phone?.takeIf { it.isNotBlank() }?.let { json.put(QUERY_PHONE, it) }
telegramId?.takeIf { it.isNotBlank() }?.let { json.put(QUERY_TELEGRAM_ID, it) }
loyaltyId?.takeIf { it.isNotBlank() }?.let { json.put(QUERY_LOYALTY_ID, it) }
return json
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.personalization.api.responses.predict

import com.google.gson.annotations.SerializedName

data class PurchasePredictResponse(
@SerializedName("probability")
val probability: Double,
@SerializedName("client_id")
val clientId: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import com.personalization.api.managers.CartManager
import com.personalization.api.managers.InAppNotificationManager
import com.personalization.api.managers.ProductsManager
import com.personalization.api.managers.RecommendationManager
import com.personalization.api.managers.PredictManager
import com.personalization.api.managers.SearchManager
import com.personalization.api.managers.TrackEventManager
import com.personalization.features.cart.CartManagerImpl
import com.personalization.features.inAppNotification.impl.InAppNotificationManagerImpl
import com.personalization.features.notification.domain.data.NotificationDataExtractor
import com.personalization.features.predict.impl.PredictManagerImpl
import com.personalization.features.products.impl.ProductsManagerImpl
import com.personalization.features.recommendation.impl.RecommendationManagerImpl
import com.personalization.features.search.impl.SearchManagerImpl
Expand Down Expand Up @@ -109,6 +111,14 @@ class SdkModule {
sendNetworkMethodUseCase = sendNetworkMethodUseCase
)

@Singleton
@Provides
fun providePredictManager(
sendNetworkMethodUseCase: SendNetworkMethodUseCase
): PredictManager = PredictManagerImpl(
sendNetworkMethodUseCase = sendNetworkMethodUseCase
)

@Singleton
@Provides
fun provideInAppNotificationManager(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.personalization.features.predict.impl

import com.google.gson.Gson
import com.personalization.api.OnApiCallbackListener
import com.personalization.api.managers.PredictManager
import com.personalization.api.params.PurchasePredictParams
import com.personalization.api.responses.predict.PurchasePredictResponse
import com.personalization.sdk.domain.usecases.network.SendNetworkMethodUseCase
import org.json.JSONObject
import javax.inject.Inject

private const val PROBABILITY_TO_PURCHASE_PATH = "predict/probability-to-purchase"

internal class PredictManagerImpl @Inject constructor(
private val sendNetworkMethodUseCase: SendNetworkMethodUseCase
) : PredictManager {

override fun getProbabilityToPurchase(
params: PurchasePredictParams,
onSuccess: (PurchasePredictResponse) -> Unit,
onError: (Int, String?) -> Unit
) {
sendNetworkMethodUseCase.getAsync(
PROBABILITY_TO_PURCHASE_PATH,
params.toQueryJson(),
object : OnApiCallbackListener() {
override fun onSuccess(response: JSONObject?) {
if (response == null) {
onError(-1, null)
return
}
val parsed = Gson().fromJson(response.toString(), PurchasePredictResponse::class.java)
onSuccess(parsed)
}

override fun onError(code: Int, msg: String?) {
onError(code, msg)
}
}
)
}
}
Loading