diff --git a/.gitignore b/.gitignore
index 0e50d914..e5f0a728 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,8 @@ test.key
node_modules
*.tgz
+# Demo app build artifacts
+demo-app/build/
+demo-app/.gradle/
+demo-app/local.properties
+
diff --git a/demo-app/build.gradle b/demo-app/build.gradle
new file mode 100644
index 00000000..16927520
--- /dev/null
+++ b/demo-app/build.gradle
@@ -0,0 +1,83 @@
+plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.android'
+}
+
+android {
+ namespace 'com.personalization.demo'
+ compileSdkVersion 34
+
+ flavorDimensions += 'default'
+
+ // Read shop.id from local.properties (check both root and demo-app directories)
+ def localProperties = new Properties()
+ def localPropertiesFile = rootProject.file('local.properties')
+ if (!localPropertiesFile.exists()) {
+ localPropertiesFile = project.file('local.properties')
+ }
+ if (localPropertiesFile.exists()) {
+ localProperties.load(new FileInputStream(localPropertiesFile))
+ }
+ def shopId = localProperties.getProperty('shop.id', '357382bf66ac0ce2f1722677c59511')
+
+ defaultConfig {
+ applicationId "com.personalization.demo"
+ minSdkVersion 19
+ targetSdkVersion 34
+ versionCode 1
+ versionName "1.0"
+ multiDexEnabled true
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+
+ // Add shop ID to BuildConfig
+ buildConfigField "String", "SHOP_ID", "\"${shopId}\""
+ }
+
+ productFlavors {
+ rees46 {
+ dimension 'default'
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = '17'
+ }
+
+ buildFeatures {
+ viewBinding true
+ buildConfig true
+ }
+}
+
+dependencies {
+ implementation project(':personalization-sdk')
+
+ implementation 'androidx.multidex:multidex:2.0.1'
+ implementation 'androidx.core:core-ktx:1.13.1'
+ implementation 'androidx.appcompat:appcompat:1.6.1'
+ implementation 'com.google.android.material:material:1.12.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+ implementation 'androidx.fragment:fragment-ktx:1.6.2'
+
+ // Firebase
+ implementation platform('com.google.firebase:firebase-bom:32.7.0')
+ implementation 'com.google.firebase:firebase-messaging'
+
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.2.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+}
+
diff --git a/demo-app/proguard-rules.pro b/demo-app/proguard-rules.pro
new file mode 100644
index 00000000..a6775bd0
--- /dev/null
+++ b/demo-app/proguard-rules.pro
@@ -0,0 +1,5 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+
+
diff --git a/demo-app/src/main/AndroidManifest.xml b/demo-app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a8abb8a0
--- /dev/null
+++ b/demo-app/src/main/AndroidManifest.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demo-app/src/main/java/com/personalization/demo/DemoApplication.kt b/demo-app/src/main/java/com/personalization/demo/DemoApplication.kt
new file mode 100644
index 00000000..3c888d51
--- /dev/null
+++ b/demo-app/src/main/java/com/personalization/demo/DemoApplication.kt
@@ -0,0 +1,17 @@
+package com.personalization.demo
+
+import android.content.Context
+import androidx.multidex.MultiDexApplication
+import com.google.firebase.FirebaseApp
+
+class DemoApplication : MultiDexApplication() {
+
+ override fun onCreate() {
+ super.onCreate()
+ // Initialize Firebase if not already initialized
+ if (FirebaseApp.getApps(this).isEmpty()) {
+ FirebaseApp.initializeApp(this)
+ }
+ }
+}
+
diff --git a/demo-app/src/main/java/com/personalization/demo/MainActivity.kt b/demo-app/src/main/java/com/personalization/demo/MainActivity.kt
new file mode 100644
index 00000000..a211f1aa
--- /dev/null
+++ b/demo-app/src/main/java/com/personalization/demo/MainActivity.kt
@@ -0,0 +1,83 @@
+package com.personalization.demo
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import com.google.firebase.FirebaseApp
+import com.personalization.SDK
+import com.personalization.demo.BuildConfig
+import com.personalization.sdk.data.models.dto.popUp.Components
+import com.personalization.sdk.data.models.dto.popUp.PopupActions
+import com.personalization.sdk.data.models.dto.popUp.PopupDto
+import com.personalization.sdk.data.models.dto.popUp.Position
+
+class MainActivity : AppCompatActivity() {
+
+ private lateinit var sdk: SDK
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+
+ // Initialize Firebase if not already initialized
+ try {
+ if (FirebaseApp.getApps(this).isEmpty()) {
+ FirebaseApp.initializeApp(this)
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+
+ // Initialize SDK
+ try {
+ sdk = SDK()
+ sdk.initialize(
+ context = this,
+ shopId = BuildConfig.SHOP_ID,
+ apiDomain = "api.rees46.ru",
+ autoSendPushToken = false
+ )
+ } catch (e: Exception) {
+ e.printStackTrace()
+ // Continue even if SDK initialization fails for demo purposes
+ }
+
+ // Initialize fragment manager for popups
+ sdk.inAppNotificationManager.initFragmentManager(supportFragmentManager)
+
+ // Setup button
+ findViewById(R.id.btnShowTestPopup).setOnClickListener {
+ showTestPopup()
+ }
+ }
+
+ private fun showTestPopup() {
+ val testPopup = PopupDto(
+ id = 999,
+ channels = listOf("email"),
+ position = Position.CENTERED,
+ delay = 0,
+ html = """
+
+
+ """.trimIndent(),
+ components = Components(
+ header = "Test Popup",
+ text = "This is a test popup for Android SDK",
+ image = "",
+ button = "",
+ textEnabled = "",
+ imageEnabled = "",
+ headerEnabled = ""
+ ),
+ webPushSystem = false,
+ popupActions = PopupActions(
+ link = null,
+ close = null,
+ pushSubscribe = null
+ )
+ )
+
+ sdk.inAppNotificationManager.shopPopUp(testPopup)
+ }
+}
+
diff --git a/demo-app/src/main/res/layout/activity_main.xml b/demo-app/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000..395cd9d2
--- /dev/null
+++ b/demo-app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
diff --git a/demo-app/src/main/res/values/strings.xml b/demo-app/src/main/res/values/strings.xml
new file mode 100644
index 00000000..8132554d
--- /dev/null
+++ b/demo-app/src/main/res/values/strings.xml
@@ -0,0 +1,7 @@
+
+
+ Personalization SDK Demo
+ Show Test Popup
+
+
+
diff --git a/personalization-sdk/build.gradle b/personalization-sdk/build.gradle
index cc10b999..b719e6c7 100644
--- a/personalization-sdk/build.gradle
+++ b/personalization-sdk/build.gradle
@@ -34,12 +34,12 @@ android {
}
compileOptions {
- sourceCompatibility JavaVersion.VERSION_20
- targetCompatibility JavaVersion.VERSION_20
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
- jvmTarget = '20'
+ jvmTarget = '17'
}
sourceSets {
@@ -75,6 +75,12 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
+ // Explicitly exclude demo-app from SDK dependencies
+ // demo-app is a separate application module and should not be included in SDK artifacts
+ configurations.all {
+ exclude group: 'com.personalization.demo', module: 'demo-app'
+ }
+
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'com.google.android.material:material:1.12.0'
diff --git a/personalization-sdk/src/main/kotlin/com/personalization/api/managers/TrackEventManager.kt b/personalization-sdk/src/main/kotlin/com/personalization/api/managers/TrackEventManager.kt
index 99f6bf90..f0b02084 100644
--- a/personalization-sdk/src/main/kotlin/com/personalization/api/managers/TrackEventManager.kt
+++ b/personalization-sdk/src/main/kotlin/com/personalization/api/managers/TrackEventManager.kt
@@ -51,4 +51,12 @@ interface TrackEventManager {
value: Int? = null,
listener: OnApiCallbackListener? = null
)
+
+ /**
+ * Track popup shown event
+ *
+ * @param popupId Popup ID
+ * @param listener Callback
+ */
+ fun trackPopupShown(popupId: Int, listener: OnApiCallbackListener? = null)
}
diff --git a/personalization-sdk/src/main/kotlin/com/personalization/di/SdkModule.kt b/personalization-sdk/src/main/kotlin/com/personalization/di/SdkModule.kt
index ab015f5c..ec616ed5 100644
--- a/personalization-sdk/src/main/kotlin/com/personalization/di/SdkModule.kt
+++ b/personalization-sdk/src/main/kotlin/com/personalization/di/SdkModule.kt
@@ -81,12 +81,14 @@ class SdkModule {
getRecommendedByUseCase: GetRecommendedByUseCase,
setRecommendedByUseCase: SetRecommendedByUseCase,
sendNetworkMethodUseCase: SendNetworkMethodUseCase,
- inAppNotificationManager: InAppNotificationManager
+ inAppNotificationManager: InAppNotificationManager,
+ getUserSettingsValueUseCase: GetUserSettingsValueUseCase
): TrackEventManager = TrackEventManagerImpl(
getRecommendedByUseCase = getRecommendedByUseCase,
setRecommendedByUseCase = setRecommendedByUseCase,
sendNetworkMethodUseCase = sendNetworkMethodUseCase,
inAppNotificationManager = inAppNotificationManager,
+ getUserSettingsValueUseCase = getUserSettingsValueUseCase
)
@Singleton
@@ -110,9 +112,15 @@ class SdkModule {
@Singleton
@Provides
fun provideInAppNotificationManager(
- context: Context
+ context: Context,
+ getUserSettingsValueUseCase: GetUserSettingsValueUseCase,
+ trackEventManagerProvider: javax.inject.Provider
): InAppNotificationManager {
- return InAppNotificationManagerImpl(context)
+ return InAppNotificationManagerImpl(
+ context = context,
+ getUserSettingsValueUseCase = getUserSettingsValueUseCase,
+ trackEventManager = dagger.Lazy { trackEventManagerProvider.get() }
+ )
}
@Singleton
diff --git a/personalization-sdk/src/main/kotlin/com/personalization/features/inAppNotification/impl/InAppNotificationManagerImpl.kt b/personalization-sdk/src/main/kotlin/com/personalization/features/inAppNotification/impl/InAppNotificationManagerImpl.kt
index a32cedf2..337d990a 100644
--- a/personalization-sdk/src/main/kotlin/com/personalization/features/inAppNotification/impl/InAppNotificationManagerImpl.kt
+++ b/personalization-sdk/src/main/kotlin/com/personalization/features/inAppNotification/impl/InAppNotificationManagerImpl.kt
@@ -5,6 +5,8 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
+import android.os.Handler
+import android.os.Looper
import android.provider.Settings
import android.view.View
import android.widget.Toast
@@ -13,7 +15,10 @@ import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentManager
import com.personalization.R
import com.personalization.api.managers.InAppNotificationManager
+import com.personalization.api.managers.TrackEventManager
import com.personalization.errors.EmptyFieldError
+import com.personalization.sdk.domain.usecases.userSettings.GetUserSettingsValueUseCase
+import dagger.Lazy
import com.personalization.inAppNotification.view.component.dialog.fullScreen.FULL_SCREEN_DIALOG_TAG
import com.personalization.inAppNotification.view.component.dialog.alert.ALERT_DIALOG_TAG
import com.personalization.inAppNotification.view.component.dialog.alert.AlertDialog
@@ -30,18 +35,42 @@ import com.personalization.ui.click.NotificationClickListener
import javax.inject.Inject
class InAppNotificationManagerImpl @Inject constructor(
- private val context: Context
+ private val context: Context,
+ private val getUserSettingsValueUseCase: GetUserSettingsValueUseCase,
+ private val trackEventManager: Lazy
) : InAppNotificationManager {
private lateinit var fragmentManager: FragmentManager
+ private val popupShownFlags: MutableMap = mutableMapOf()
+ private val handler: Handler = Handler(Looper.getMainLooper())
override fun initFragmentManager(fragmentManager: FragmentManager) {
this.fragmentManager = fragmentManager
}
override fun shopPopUp(popupDto: PopupDto) {
+ // Check if popup was shown in the last 60 seconds
+ val shownTime = popupShownFlags[popupDto.id]
+ if (shownTime != null) {
+ val timeSinceShown = System.currentTimeMillis() - shownTime
+ if (timeSinceShown < 60_000) { // 60 seconds in milliseconds
+ return // Popup was already shown, skip
+ }
+ }
+
val dialogData = extractDialogData(popupDto)
showDialog(dialogData)
+
+ // Store popup shown flag in memory for 60 seconds
+ popupShownFlags[popupDto.id] = System.currentTimeMillis()
+
+ // Remove flag after 60 seconds
+ handler.postDelayed({
+ popupShownFlags.remove(popupDto.id)
+ }, 60_000)
+
+ // Send popup shown event to server
+ trackEventManager.get().trackPopupShown(popupId = popupDto.id, listener = null)
}
private fun extractDialogData(popupDto: PopupDto): DialogDataDto {
diff --git a/personalization-sdk/src/main/kotlin/com/personalization/features/trackEvent/impl/TrackEventManagerImpl.kt b/personalization-sdk/src/main/kotlin/com/personalization/features/trackEvent/impl/TrackEventManagerImpl.kt
index 23e3641d..2e9ef5ea 100644
--- a/personalization-sdk/src/main/kotlin/com/personalization/features/trackEvent/impl/TrackEventManagerImpl.kt
+++ b/personalization-sdk/src/main/kotlin/com/personalization/features/trackEvent/impl/TrackEventManagerImpl.kt
@@ -11,6 +11,8 @@ import com.personalization.sdk.data.models.params.SdkInitializationParams.PARAM_
import com.personalization.sdk.domain.usecases.network.SendNetworkMethodUseCase
import com.personalization.sdk.domain.usecases.recommendation.GetRecommendedByUseCase
import com.personalization.sdk.domain.usecases.recommendation.SetRecommendedByUseCase
+import com.personalization.sdk.domain.usecases.userSettings.GetUserSettingsValueUseCase
+import com.personalization.sdk.data.models.params.UserBasicParams
import org.json.JSONObject
import javax.inject.Inject
@@ -18,7 +20,8 @@ internal class TrackEventManagerImpl @Inject constructor(
val getRecommendedByUseCase: GetRecommendedByUseCase,
val setRecommendedByUseCase: SetRecommendedByUseCase,
private val sendNetworkMethodUseCase: SendNetworkMethodUseCase,
- private val inAppNotificationManager: InAppNotificationManager
+ private val inAppNotificationManager: InAppNotificationManager,
+ private val getUserSettingsValueUseCase: GetUserSettingsValueUseCase
) : TrackEventManager {
override fun track(event: TrackEvent, productId: String) {
@@ -105,6 +108,20 @@ internal class TrackEventManagerImpl @Inject constructor(
)
}
+ override fun trackPopupShown(popupId: Int, listener: OnApiCallbackListener?) {
+ val params = Params()
+ params.put(UserBasicParams.SHOP_ID, getUserSettingsValueUseCase.getShopId())
+ params.put(UserBasicParams.DID, getUserSettingsValueUseCase.getDid())
+ params.put(UserBasicParams.SID, getUserSettingsValueUseCase.getSid())
+ params.put("popup", popupId.toString())
+
+ sendNetworkMethodUseCase.postAsync(
+ POPUP_SHOWN_PATH,
+ params.build(),
+ listener
+ )
+ }
+
private fun handlePopup(response: JSONObject) {
val popUpData = response.optJSONObject(PARAM_POPUP)?.let { PopupDtoMapper.map(it) }
if (popUpData != null) {
@@ -115,6 +132,7 @@ internal class TrackEventManagerImpl @Inject constructor(
companion object {
private const val CUSTOM_PUSH_REQUEST = "push/custom"
private const val PUSH_REQUEST = "push"
+ private const val POPUP_SHOWN_PATH = "popup/showed"
private const val EVENT_PARAMETER = "event"
private const val EMAIL_PARAMETER = "email"
diff --git a/personalization-sdk/src/main/kotlin/com/personalization/sdk/data/repositories/network/NetworkRepositoryImpl.kt b/personalization-sdk/src/main/kotlin/com/personalization/sdk/data/repositories/network/NetworkRepositoryImpl.kt
index c7ea37a6..02032f84 100644
--- a/personalization-sdk/src/main/kotlin/com/personalization/sdk/data/repositories/network/NetworkRepositoryImpl.kt
+++ b/personalization-sdk/src/main/kotlin/com/personalization/sdk/data/repositories/network/NetworkRepositoryImpl.kt
@@ -218,12 +218,35 @@ class NetworkRepositoryImpl @Inject constructor(
SDK.debug(connection.responseCode.toString() + ": " + networkMethod.type + " " + buildUri.toString())
}
- if (listener != null && connection.responseCode == HttpURLConnection.HTTP_OK) {
- val json = JSONTokener(readStream(connection.inputStream)).nextValue()
- if (json is JSONObject) {
- listener.onSuccess(json)
- } else if (json is JSONArray) {
- listener.onSuccess(json)
+ if (listener != null) {
+ when (connection.responseCode) {
+ HttpURLConnection.HTTP_OK -> {
+ try {
+ val inputStream = connection.inputStream
+ val responseText = readStream(inputStream)
+ if (responseText.isNotEmpty()) {
+ val json = JSONTokener(responseText).nextValue()
+ if (json is JSONObject) {
+ listener.onSuccess(json)
+ } else if (json is JSONArray) {
+ listener.onSuccess(json)
+ } else {
+ // Empty or invalid JSON, treat as success with empty object
+ listener.onSuccess(JSONObject())
+ }
+ } else {
+ // Empty response, treat as success
+ listener.onSuccess(JSONObject())
+ }
+ } catch (e: Exception) {
+ // Failed to parse JSON, but response code is 200, treat as success
+ listener.onSuccess(JSONObject())
+ }
+ }
+ HttpURLConnection.HTTP_NO_CONTENT -> {
+ // 204 No Content - successful response with no body
+ listener.onSuccess(JSONObject())
+ }
}
}
diff --git a/personalization-sdk/src/main/kotlin/com/personalization/sdk/domain/usecases/userSettings/GetUserSettingsValueUseCase.kt b/personalization-sdk/src/main/kotlin/com/personalization/sdk/domain/usecases/userSettings/GetUserSettingsValueUseCase.kt
index 4b6266ea..0d5df35d 100644
--- a/personalization-sdk/src/main/kotlin/com/personalization/sdk/domain/usecases/userSettings/GetUserSettingsValueUseCase.kt
+++ b/personalization-sdk/src/main/kotlin/com/personalization/sdk/domain/usecases/userSettings/GetUserSettingsValueUseCase.kt
@@ -15,6 +15,8 @@ class GetUserSettingsValueUseCase @Inject constructor(
fun getSidLastActTime(): Long = userSettingsRepository.getSidLastActTime()
+ fun getShopId(): String = userSettingsRepository.getShopId()
+
fun getSegmentForABTesting(): String = userSettingsRepository.getSegmentForABTesting()
fun getAdvertisingId(): String = userSettingsRepository.getAdvertisingId()
diff --git a/settings.gradle b/settings.gradle
index 199b227f..0350fb70 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -6,3 +6,4 @@ pluginManagement {
}
include ':personalization-sdk'
+include ':demo-app'