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
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ class SurveyManagerInstrumentedTest {
fun testGetLanguageCode_nullLanguage_returnsDefault() {
val survey = Survey(
id = "test",
name = "Test Survey",
triggers = null,
recontactDays = null,
displayLimit = null,
Expand Down Expand Up @@ -80,7 +79,6 @@ class SurveyManagerInstrumentedTest {
fun testGetLanguageCode_emptyLanguage_returnsDefault() {
val survey = Survey(
id = "test",
name = "Test Survey",
triggers = null,
recontactDays = null,
displayLimit = null,
Expand Down Expand Up @@ -109,7 +107,6 @@ class SurveyManagerInstrumentedTest {
fun testGetLanguageCode_explicitDefault_returnsDefault() {
val survey = Survey(
id = "test",
name = "Test Survey",
triggers = null,
recontactDays = null,
displayLimit = null,
Expand Down Expand Up @@ -138,7 +135,6 @@ class SurveyManagerInstrumentedTest {
fun testGetLanguageCode_matchByCode_returnsCode() {
val survey = Survey(
id = "test",
name = "Test Survey",
triggers = null,
recontactDays = null,
displayLimit = null,
Expand Down Expand Up @@ -177,7 +173,6 @@ class SurveyManagerInstrumentedTest {
fun testGetLanguageCode_matchByAlias_returnsCode() {
val survey = Survey(
id = "test",
name = "Test Survey",
triggers = null,
recontactDays = null,
displayLimit = null,
Expand Down Expand Up @@ -216,7 +211,6 @@ class SurveyManagerInstrumentedTest {
fun testGetLanguageCode_disabledLanguage_returnsNull() {
val survey = Survey(
id = "test",
name = "Test Survey",
triggers = null,
recontactDays = null,
displayLimit = null,
Expand Down Expand Up @@ -255,7 +249,6 @@ class SurveyManagerInstrumentedTest {
fun testGetLanguageCode_defaultLanguage_returnsDefault() {
val survey = Survey(
id = "test",
name = "Test Survey",
triggers = null,
recontactDays = null,
displayLimit = null,
Expand Down Expand Up @@ -448,7 +441,6 @@ class SurveyManagerInstrumentedTest {
): Survey {
return Survey(
id = id,
name = "Test Survey",
triggers = null,
recontactDays = recontactDays,
displayLimit = displayLimit,
Expand All @@ -464,7 +456,6 @@ class SurveyManagerInstrumentedTest {
private fun createMockSurvey(recontactDays: Double?): Survey {
return Survey(
id = "mockSurvey",
name = "Mock Survey",
triggers = null,
recontactDays = recontactDays,
displayLimit = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ class FormbricksViewModelInstrumentedTest {
val surveyId = "survey1"
val survey = Survey(
id = surveyId,
name = "Test Survey",
triggers = null,
recontactDays = null,
displayLimit = null,
Expand Down Expand Up @@ -309,7 +308,6 @@ class FormbricksViewModelInstrumentedTest {
val surveyId = "survey1"
val survey = Survey(
id = surveyId,
name = "Test Survey",
triggers = null,
recontactDays = null,
displayLimit = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import com.formbricks.android.extensions.expiresAt
import com.formbricks.android.extensions.guard
import com.formbricks.android.logger.Logger
import com.formbricks.android.model.workspace.WorkspaceDataHolder
import com.formbricks.android.model.workspace.SegmentFilterResource
import com.formbricks.android.model.workspace.SegmentFilterResourceDeserializer
import com.formbricks.android.model.workspace.Segment
import com.formbricks.android.model.workspace.SegmentDeserializer
import com.formbricks.android.model.workspace.Survey
import com.formbricks.android.model.error.SDKError
import com.formbricks.android.model.user.Display
Expand Down Expand Up @@ -42,10 +42,7 @@ object SurveyManager {
internal var filteredSurveys: MutableList<Survey> = mutableListOf()

val gson = GsonBuilder()
.registerTypeAdapter(
SegmentFilterResource::class.java,
SegmentFilterResourceDeserializer()
)
.registerTypeAdapter(Segment::class.java, SegmentDeserializer())
.create()

private var workspaceDataHolderJson: String?
Expand Down Expand Up @@ -116,8 +113,10 @@ object SurveyManager {

if (UserManager.userId == null) {
filteredSurveys = filteredSurveys.filter { survey ->
// Only include surveys that have no segment filters or null segment
survey.segment?.filters?.isEmpty() ?: true
// Only include surveys that have no segment filters or null segment.
// `hasFilters` is decoded directly from the server response, or
// derived from a legacy cached `filters` array (see SegmentDeserializer).
!(survey.segment?.hasFilters ?: false)
}.toMutableList()
}

Expand Down Expand Up @@ -193,7 +192,7 @@ object SurveyManager {
val languageCode = getLanguageCode(firstSurveyWithActionClass, currentLanguage)

if (languageCode == null) {
val error = RuntimeException("Survey “${firstSurveyWithActionClass.name}” is not available in language “$currentLanguage”. Skipping.")
val error = RuntimeException("Survey “${firstSurveyWithActionClass.id}” is not available in language “$currentLanguage”. Skipping.")
Logger.e(error)
return
}
Expand All @@ -208,8 +207,7 @@ object SurveyManager {
isShowingSurvey = true
val timeout = firstSurveyWithActionClass.delay ?: 0.0
if (timeout > 0.0) {
val surveyName = firstSurveyWithActionClass.name
Logger.d("Delaying survey \"$surveyName\" by $timeout seconds")
Logger.d("Delaying survey \"${firstSurveyWithActionClass.id}\" by $timeout seconds")
}
stopDisplayTimer()
displayTimer.schedule(object : TimerTask() {
Expand Down
218 changes: 37 additions & 181 deletions android/src/main/java/com/formbricks/android/model/workspace/Segment.kt
Original file line number Diff line number Diff line change
@@ -1,190 +1,46 @@
package com.formbricks.android.model.workspace

import com.google.gson.annotations.SerializedName
import kotlinx.serialization.*
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*

// MARK: - Connector
@Serializable
enum class SegmentConnector {
@SerialName("and") AND,
@SerialName("or") OR
}

// MARK: - Filter Operators
@Serializable
enum class FilterOperator {
@SerialName("lessThan") LESS_THAN,
@SerialName("lessEqual") LESS_EQUAL,
@SerialName("greaterThan") GREATER_THAN,
@SerialName("greaterEqual") GREATER_EQUAL,
@SerialName("equals") EQUALS,
@SerialName("notEquals") NOT_EQUALS,
@SerialName("contains") CONTAINS,
@SerialName("doesNotContain") DOES_NOT_CONTAIN,
@SerialName("startsWith") STARTS_WITH,
@SerialName("endsWith") ENDS_WITH,
@SerialName("isSet") IS_SET,
@SerialName("isNotSet") IS_NOT_SET,
@SerialName("userIsIn") USER_IS_IN,
@SerialName("userIsNotIn") USER_IS_NOT_IN
}

// MARK: - Filter Value
@Serializable(with = SegmentFilterValueSerializer::class)
sealed class SegmentFilterValue {
data class StringValue(val value: String) : SegmentFilterValue()
data class NumberValue(val value: Double) : SegmentFilterValue()
}

object SegmentFilterValueSerializer : KSerializer<SegmentFilterValue> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("SegmentFilterValue", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): SegmentFilterValue {
val jsonInput = decoder as JsonDecoder
val element = jsonInput.decodeJsonElement()
return when (element) {
is JsonPrimitive -> {
element.doubleOrNull?.let { SegmentFilterValue.NumberValue(it) }
?: SegmentFilterValue.StringValue(element.content)
}
else -> throw SerializationException("Unexpected type for SegmentFilterValue: $element")
}
}
override fun serialize(encoder: Encoder, value: SegmentFilterValue) {
val jsonOutput = encoder as JsonEncoder
val element = when (value) {
is SegmentFilterValue.NumberValue -> JsonPrimitive(value.value)
is SegmentFilterValue.StringValue -> JsonPrimitive(value.value)
}
jsonOutput.encodeJsonElement(element)
}
}

// MARK: - Filter Root
@Serializable(with = SegmentFilterRootSerializer::class)
sealed class SegmentFilterRoot {
data class Attribute(val contactAttributeKey: String) : SegmentFilterRoot()
data class Person(val personIdentifier: String) : SegmentFilterRoot()
data class Segment(val segmentId: String) : SegmentFilterRoot()
data class Device(val deviceType: String) : SegmentFilterRoot()
}

object SegmentFilterRootSerializer : KSerializer<SegmentFilterRoot> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("SegmentFilterRoot") {
element<String>("type")
}
override fun deserialize(decoder: Decoder): SegmentFilterRoot {
val input = decoder as JsonDecoder
val obj = input.decodeJsonElement().jsonObject
return when (val type = obj["type"]?.jsonPrimitive?.content) {
"attribute" -> SegmentFilterRoot.Attribute(obj["contactAttributeKey"]!!.jsonPrimitive.content)
"person" -> SegmentFilterRoot.Person(obj["personIdentifier"]!!.jsonPrimitive.content)
"segment" -> SegmentFilterRoot.Segment(obj["segmentId"]!!.jsonPrimitive.content)
"device" -> SegmentFilterRoot.Device(obj["deviceType"]!!.jsonPrimitive.content)
else -> throw SerializationException("Unknown root type: $type")
}
}
override fun serialize(encoder: Encoder, value: SegmentFilterRoot) {
val output = encoder as JsonEncoder
val json = buildJsonObject {
when (value) {
is SegmentFilterRoot.Attribute -> {
put("type", JsonPrimitive("attribute"))
put("contactAttributeKey", JsonPrimitive(value.contactAttributeKey))
}
is SegmentFilterRoot.Person -> {
put("type", JsonPrimitive("person"))
put("personIdentifier", JsonPrimitive(value.personIdentifier))
}
is SegmentFilterRoot.Segment -> {
put("type", JsonPrimitive("segment"))
put("segmentId", JsonPrimitive(value.segmentId))
}
is SegmentFilterRoot.Device -> {
put("type", JsonPrimitive("device"))
put("deviceType", JsonPrimitive(value.deviceType))
}
}
}
output.encodeJsonElement(json)
}
}

// MARK: - Qualifier
@Serializable
data class SegmentFilterQualifier(
@SerialName("operator") val `operator`: FilterOperator
)

// MARK: - Primitive Filter
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import kotlinx.serialization.Serializable
import java.lang.reflect.Type

/**
* Public client API returns the minimal `{ id, hasFilters }` shape — full
* filter logic (titles, descriptions, conditions) is evaluated server-side
* and must not reach the device.
*
* The custom deserializer also accepts legacy cached payloads that still
* carry a `filters` array (written by older SDK versions before the API was
* slimmed down). In that case `hasFilters` is derived from the array length
* so anonymous users continue to be excluded from segment-targeted surveys
* during the cache window after an SDK upgrade.
*/
@Serializable
data class SegmentPrimitiveFilter(
data class Segment(
val id: String,
val root: SegmentFilterRoot,
val value: SegmentFilterValue,
val qualifier: SegmentFilterQualifier
val hasFilters: Boolean
)

// MARK: - Recursive Resource
@Serializable(with = SegmentFilterResourceSerializer::class)
sealed class SegmentFilterResource {
data class Primitive(val filter: SegmentPrimitiveFilter) : SegmentFilterResource()
data class Group(val filters: List<SegmentFilter>) : SegmentFilterResource()
}

object SegmentFilterResourceSerializer : KSerializer<SegmentFilterResource> {
override val descriptor = buildClassSerialDescriptor("SegmentFilterResource") {
// You can declare children here if you like,
// or leave it empty if you're purely passing through.
}
override fun deserialize(decoder: Decoder): SegmentFilterResource {
val input = decoder as JsonDecoder
val element = input.decodeJsonElement()
return if (element is JsonArray) {
val list = element.map { input.json.decodeFromJsonElement(SegmentFilter.serializer(), it) }
SegmentFilterResource.Group(list)
} else {
val prim = input.json.decodeFromJsonElement(SegmentPrimitiveFilter.serializer(), element)
SegmentFilterResource.Primitive(prim)
class SegmentDeserializer : JsonDeserializer<Segment> {
override fun deserialize(
json: JsonElement,
typeOfT: Type,
context: JsonDeserializationContext
): Segment {
val obj = json.asJsonObject
val id = obj.get("id").asString
// Fail-closed: ambiguous payload (neither `hasFilters` nor `filters` present)
// is treated as "has filters" so anonymous users are excluded from segment-
// targeted surveys rather than over-shown.
val hasFilters = when {
obj.has("hasFilters") && !obj.get("hasFilters").isJsonNull ->
obj.get("hasFilters").asBoolean
obj.has("filters") && obj.get("filters").isJsonArray ->
obj.get("filters").asJsonArray.size() > 0
else -> true
}
}
override fun serialize(encoder: Encoder, value: SegmentFilterResource) {
val output = encoder as JsonEncoder
val json = when (value) {
is SegmentFilterResource.Primitive -> output.json.encodeToJsonElement(SegmentPrimitiveFilter.serializer(), value.filter)
is SegmentFilterResource.Group -> output.json.encodeToJsonElement(ListSerializer(SegmentFilter.serializer()), value.filters)
}
output.encodeJsonElement(json)
return Segment(id = id, hasFilters = hasFilters)
}
}

// MARK: - Filter Node
@Serializable
data class SegmentFilter(
val id: String,
val connector: SegmentConnector? = null,
val resource: SegmentFilterResource
)

// MARK: - Segment Model
@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class Segment(
val id: String,
val title: String,
val description: String? = null,
@SerialName("isPrivate") val isPrivate: Boolean,
val filters: List<SegmentFilter>,
// Server may send `workspaceId` (new) or `environmentId` (legacy). Field is
// informational only — not read by SDK logic — so keep it optional.
@SerializedName(value = "workspaceId", alternate = ["environmentId"])
@SerialName("workspaceId")
@JsonNames("environmentId")
val workspaceId: String? = null,
val createdAt: String,
val updatedAt: String,
val surveys: List<String>
)

This file was deleted.

Loading
Loading