Compare commits

...

5 Commits

Author SHA1 Message Date
pandeymangg
1fba692626 fix: android sdk segment bug 2025-05-13 19:24:06 +05:30
pandeymangg
3ace91cdd5 fix: android sdk segment bug 2025-05-13 18:39:50 +05:30
pandeymangg
4ba7bf5b3c fix 2025-05-13 16:44:45 +05:30
pandeymangg
bd1402a58b fixes android sdk issues: 2025-05-13 15:55:54 +05:30
pandeymangg
c2af0c3fb6 fixes ios sdk issues and removes callbacks 2025-05-13 14:48:23 +05:30
23 changed files with 391 additions and 163 deletions

View File

@@ -1,16 +1,13 @@
package com.formbricks.demo package com.formbricks.demo
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.widget.Button import android.widget.Button
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import com.formbricks.formbrickssdk.Formbricks import com.formbricks.formbrickssdk.Formbricks
import com.formbricks.formbrickssdk.FormbricksCallback
import com.formbricks.formbrickssdk.helper.FormbricksConfig import com.formbricks.formbrickssdk.helper.FormbricksConfig
import com.formbricks.formbrickssdk.model.enums.SuccessType
import java.util.UUID import java.util.UUID
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@@ -18,33 +15,6 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
Formbricks.callback = object: FormbricksCallback {
override fun onSurveyStarted() {
Log.d("FormbricksCallback", "onSurveyStarted")
}
override fun onSurveyFinished() {
Log.d("FormbricksCallback", "onSurveyFinished")
}
override fun onSurveyClosed() {
Log.d("FormbricksCallback", "onSurveyClosed")
}
override fun onPageCommitVisible() {
Log.d("FormbricksCallback", "onPageCommitVisible")
}
override fun onError(error: Exception) {
Log.d("FormbricksCallback", "onError from the CB: ${error.localizedMessage}")
}
override fun onSuccess(successType: SuccessType) {
Log.d("FormbricksCallback", "onSuccess: ${successType.name}")
}
}
val config = FormbricksConfig.Builder("[appUrl]","[environmentId]") val config = FormbricksConfig.Builder("[appUrl]","[environmentId]")
.setLoggingEnabled(true) .setLoggingEnabled(true)
.setFragmentManager(supportFragmentManager) .setFragmentManager(supportFragmentManager)

View File

@@ -34,5 +34,4 @@
-keep class com.formbricks.formbrickssdk.DataBinderMapperImpl { *; } -keep class com.formbricks.formbrickssdk.DataBinderMapperImpl { *; }
-keep class com.formbricks.formbrickssdk.Formbricks { *; } -keep class com.formbricks.formbrickssdk.Formbricks { *; }
-keep class com.formbricks.formbrickssdk.helper.FormbricksConfig { *; } -keep class com.formbricks.formbrickssdk.helper.FormbricksConfig { *; }
-keep class com.formbricks.formbrickssdk.model.error.SDKError { *; } -keep class com.formbricks.formbrickssdk.model.error.SDKError { *; }
-keep interface com.formbricks.formbrickssdk.FormbricksCallback { *; }

View File

@@ -10,22 +10,10 @@ import com.formbricks.formbrickssdk.helper.FormbricksConfig
import com.formbricks.formbrickssdk.logger.Logger import com.formbricks.formbrickssdk.logger.Logger
import com.formbricks.formbrickssdk.manager.SurveyManager import com.formbricks.formbrickssdk.manager.SurveyManager
import com.formbricks.formbrickssdk.manager.UserManager import com.formbricks.formbrickssdk.manager.UserManager
import com.formbricks.formbrickssdk.model.enums.SuccessType
import com.formbricks.formbrickssdk.model.error.SDKError import com.formbricks.formbrickssdk.model.error.SDKError
import com.formbricks.formbrickssdk.webview.FormbricksFragment import com.formbricks.formbrickssdk.webview.FormbricksFragment
import java.lang.RuntimeException import java.lang.RuntimeException
@Keep
interface FormbricksCallback {
fun onSurveyStarted()
fun onSurveyFinished()
fun onSurveyClosed()
fun onPageCommitVisible()
fun onError(error: Exception)
fun onSuccess(successType: SuccessType)
}
@Keep @Keep
object Formbricks { object Formbricks {
internal lateinit var applicationContext: Context internal lateinit var applicationContext: Context
@@ -37,8 +25,6 @@ object Formbricks {
private var fragmentManager: FragmentManager? = null private var fragmentManager: FragmentManager? = null
internal var isInitialized = false internal var isInitialized = false
var callback: FormbricksCallback? = null
/** /**
* Initializes the Formbricks SDK with the given [Context] config [FormbricksConfig]. * Initializes the Formbricks SDK with the given [Context] config [FormbricksConfig].
* This method is mandatory to be called, and should be only once per application lifecycle. * This method is mandatory to be called, and should be only once per application lifecycle.
@@ -62,7 +48,6 @@ object Formbricks {
fun setup(context: Context, config: FormbricksConfig, forceRefresh: Boolean = false) { fun setup(context: Context, config: FormbricksConfig, forceRefresh: Boolean = false) {
if (isInitialized && !forceRefresh) { if (isInitialized && !forceRefresh) {
val error = SDKError.sdkIsAlreadyInitialized val error = SDKError.sdkIsAlreadyInitialized
callback?.onError(error)
Logger.e(error) Logger.e(error)
return return
} }
@@ -97,14 +82,12 @@ object Formbricks {
fun setUserId(userId: String) { fun setUserId(userId: String) {
if (!isInitialized) { if (!isInitialized) {
val error = SDKError.sdkIsNotInitialized val error = SDKError.sdkIsNotInitialized
callback?.onError(error)
Logger.e(error) Logger.e(error)
return return
} }
if(UserManager.userId != null) { if(UserManager.userId != null) {
val error = RuntimeException("A userId is already set ${UserManager.userId} - please call logout first before setting a new one") val error = RuntimeException("A userId is already set ${UserManager.userId} - please call logout first before setting a new one")
callback?.onError(error)
Logger.e(error) Logger.e(error)
return return
} }
@@ -124,7 +107,6 @@ object Formbricks {
fun setAttribute(attribute: String, key: String) { fun setAttribute(attribute: String, key: String) {
if (!isInitialized) { if (!isInitialized) {
val error = SDKError.sdkIsNotInitialized val error = SDKError.sdkIsNotInitialized
callback?.onError(error)
Logger.e(error) Logger.e(error)
return return
} }
@@ -143,7 +125,6 @@ object Formbricks {
fun setAttributes(attributes: Map<String, String>) { fun setAttributes(attributes: Map<String, String>) {
if (!isInitialized) { if (!isInitialized) {
val error = SDKError.sdkIsNotInitialized val error = SDKError.sdkIsNotInitialized
callback?.onError(error)
Logger.e(error) Logger.e(error)
return return
} }
@@ -162,7 +143,6 @@ object Formbricks {
fun setLanguage(language: String) { fun setLanguage(language: String) {
if (!isInitialized) { if (!isInitialized) {
val error = SDKError.sdkIsNotInitialized val error = SDKError.sdkIsNotInitialized
callback?.onError(error)
Logger.e(error) Logger.e(error)
return return
} }
@@ -182,14 +162,12 @@ object Formbricks {
fun track(action: String) { fun track(action: String) {
if (!isInitialized) { if (!isInitialized) {
val error = SDKError.sdkIsNotInitialized val error = SDKError.sdkIsNotInitialized
callback?.onError(error)
Logger.e(error) Logger.e(error)
return return
} }
if (!isInternetAvailable()) { if (!isInternetAvailable()) {
val error = SDKError.connectionIsNotAvailable val error = SDKError.connectionIsNotAvailable
callback?.onError(error)
Logger.e(error) Logger.e(error)
return return
} }
@@ -209,12 +187,10 @@ object Formbricks {
fun logout() { fun logout() {
if (!isInitialized) { if (!isInitialized) {
val error = SDKError.sdkIsNotInitialized val error = SDKError.sdkIsNotInitialized
callback?.onError(error)
Logger.e(error) Logger.e(error)
return return
} }
callback?.onSuccess(SuccessType.LOGOUT_SUCCESS)
UserManager.logout() UserManager.logout()
} }
@@ -236,7 +212,6 @@ object Formbricks {
internal fun showSurvey(id: String) { internal fun showSurvey(id: String) {
if (fragmentManager == null) { if (fragmentManager == null) {
val error = SDKError.fragmentManagerIsNotSet val error = SDKError.fragmentManagerIsNotSet
callback?.onError(error)
Logger.e(error) Logger.e(error)
return return
} }

View File

@@ -9,7 +9,6 @@ import com.formbricks.formbrickssdk.logger.Logger
import com.formbricks.formbrickssdk.model.environment.EnvironmentDataHolder import com.formbricks.formbrickssdk.model.environment.EnvironmentDataHolder
import com.formbricks.formbrickssdk.model.environment.Survey import com.formbricks.formbrickssdk.model.environment.Survey
import com.formbricks.formbrickssdk.model.error.SDKError import com.formbricks.formbrickssdk.model.error.SDKError
import com.formbricks.formbrickssdk.model.enums.SuccessType
import com.formbricks.formbrickssdk.model.user.Display import com.formbricks.formbrickssdk.model.user.Display
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -60,7 +59,6 @@ object SurveyManager {
try { try {
Gson().fromJson(json, EnvironmentDataHolder::class.java) Gson().fromJson(json, EnvironmentDataHolder::class.java)
} catch (e: Exception) { } catch (e: Exception) {
Formbricks.callback?.onError(e)
Logger.e(RuntimeException("Unable to retrieve environment data from the local storage.")) Logger.e(RuntimeException("Unable to retrieve environment data from the local storage."))
null null
} }
@@ -118,11 +116,9 @@ object SurveyManager {
startRefreshTimer(environmentDataHolder?.expiresAt()) startRefreshTimer(environmentDataHolder?.expiresAt())
filterSurveys() filterSurveys()
hasApiError = false hasApiError = false
Formbricks.callback?.onSuccess(SuccessType.GET_ENVIRONMENT_SUCCESS)
} catch (e: Exception) { } catch (e: Exception) {
hasApiError = true hasApiError = true
val error = SDKError.unableToRefreshEnvironment val error = SDKError.unableToRefreshEnvironment
Formbricks.callback?.onError(error)
Logger.e(error) Logger.e(error)
startErrorTimer() startErrorTimer()
} }
@@ -134,6 +130,7 @@ object SurveyManager {
* Handles the display percentage and the delay of the survey. * Handles the display percentage and the delay of the survey.
*/ */
fun track(action: String) { fun track(action: String) {
print(environmentDataHolder?.data?.data?.actionClasses)
val actionClasses = environmentDataHolder?.data?.data?.actionClasses ?: listOf() val actionClasses = environmentDataHolder?.data?.data?.actionClasses ?: listOf()
val codeActionClasses = actionClasses.filter { it.type == "code" } val codeActionClasses = actionClasses.filter { it.type == "code" }
val actionClass = codeActionClasses.firstOrNull { it.key == action } val actionClass = codeActionClasses.firstOrNull { it.key == action }
@@ -143,7 +140,8 @@ object SurveyManager {
} }
if (firstSurveyWithActionClass == null) { if (firstSurveyWithActionClass == null) {
Formbricks.callback?.onError(SDKError.surveyNotFoundError) val error = SDKError.surveyNotFoundError
Logger.e(error)
return return
} }
@@ -154,7 +152,6 @@ object SurveyManager {
if (languageCode == null) { if (languageCode == null) {
val error = RuntimeException("Survey “${firstSurveyWithActionClass.name}” is not available in language “$currentLanguage”. Skipping.") val error = RuntimeException("Survey “${firstSurveyWithActionClass.name}” is not available in language “$currentLanguage”. Skipping.")
Formbricks.callback?.onError(error)
Logger.e(error) Logger.e(error)
return return
} }
@@ -177,7 +174,8 @@ object SurveyManager {
}, Date(System.currentTimeMillis() + timeout.toLong() * 1000)) }, Date(System.currentTimeMillis() + timeout.toLong() * 1000))
} }
} else { } else {
Formbricks.callback?.onError(SDKError.surveyNotDisplayedError) val error = SDKError.surveyNotDisplayedError
Logger.e(error)
} }
} }
@@ -192,7 +190,6 @@ object SurveyManager {
fun postResponse(surveyId: String?) { fun postResponse(surveyId: String?) {
val id = surveyId.guard { val id = surveyId.guard {
val error = SDKError.missingSurveyId val error = SDKError.missingSurveyId
Formbricks.callback?.onError(error)
Logger.e(error) Logger.e(error)
return return
} }
@@ -206,7 +203,6 @@ object SurveyManager {
fun onNewDisplay(surveyId: String?) { fun onNewDisplay(surveyId: String?) {
val id = surveyId.guard { val id = surveyId.guard {
val error = SDKError.missingSurveyId val error = SDKError.missingSurveyId
Formbricks.callback?.onError(error)
Logger.e(error) Logger.e(error)
return return
} }
@@ -269,7 +265,6 @@ object SurveyManager {
else -> { else -> {
val error = SDKError.invalidDisplayOption val error = SDKError.invalidDisplayOption
Formbricks.callback?.onError(error)
Logger.e(error) Logger.e(error)
false false
} }

View File

@@ -9,7 +9,6 @@ import com.formbricks.formbrickssdk.extensions.guard
import com.formbricks.formbrickssdk.extensions.lastDisplayAt import com.formbricks.formbrickssdk.extensions.lastDisplayAt
import com.formbricks.formbrickssdk.logger.Logger import com.formbricks.formbrickssdk.logger.Logger
import com.formbricks.formbrickssdk.model.error.SDKError import com.formbricks.formbrickssdk.model.error.SDKError
import com.formbricks.formbrickssdk.model.enums.SuccessType
import com.formbricks.formbrickssdk.model.user.Display import com.formbricks.formbrickssdk.model.user.Display
import com.formbricks.formbrickssdk.network.queue.UpdateQueue import com.formbricks.formbrickssdk.network.queue.UpdateQueue
import com.google.gson.Gson import com.google.gson.Gson
@@ -147,10 +146,8 @@ object UserManager {
UpdateQueue.current.reset() UpdateQueue.current.reset()
SurveyManager.filterSurveys() SurveyManager.filterSurveys()
startSyncTimer() startSyncTimer()
Formbricks.callback?.onSuccess(SuccessType.SET_USER_SUCCESS)
} catch (e: Exception) { } catch (e: Exception) {
val error = SDKError.unableToPostResponse val error = SDKError.unableToPostResponse
Formbricks.callback?.onError(error)
Logger.e(error) Logger.e(error)
} }
} }
@@ -164,7 +161,6 @@ object UserManager {
if (!isUserIdDefined) { if (!isUserIdDefined) {
val error = SDKError.noUserIdSetError val error = SDKError.noUserIdSetError
Formbricks.callback?.onError(error)
Logger.e(error) Logger.e(error)
} }

View File

@@ -7,5 +7,6 @@ import kotlinx.serialization.Serializable
data class EnvironmentData( data class EnvironmentData(
@SerializedName("surveys") val surveys: List<Survey>?, @SerializedName("surveys") val surveys: List<Survey>?,
@SerializedName("actionClasses") val actionClasses: List<ActionClass>?, @SerializedName("actionClasses") val actionClasses: List<ActionClass>?,
@SerializedName("project") val project: Project @SerializedName("project") val project: Project,
@SerializedName("recaptchaSiteKey") val recaptchaSiteKey: String?
) )

View File

@@ -1,17 +1,183 @@
package com.formbricks.formbrickssdk.model.environment package com.formbricks.formbrickssdk.model.environment
import com.google.gson.annotations.SerializedName import kotlinx.serialization.*
import kotlinx.serialization.Serializable 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
@Serializable
data class SegmentPrimitiveFilter(
val id: String,
val root: SegmentFilterRoot,
val value: SegmentFilterValue,
val qualifier: SegmentFilterQualifier
)
// 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 youre 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)
}
}
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)
}
}
// MARK: - Filter Node
@Serializable
data class SegmentFilter(
val id: String,
val connector: SegmentConnector? = null,
val resource: SegmentFilterResource
)
// MARK: - Segment Model
@Serializable @Serializable
data class Segment( data class Segment(
@SerializedName("id") val id: String? = null, val id: String,
@SerializedName("createdAt") val createdAt: String? = null, val title: String,
@SerializedName("updatedAt") val updatedAt: String? = null, val description: String? = null,
@SerializedName("title") val title: String? = null, @SerialName("isPrivate") val isPrivate: Boolean,
@SerializedName("description") val description: String? = null, val filters: List<SegmentFilter>,
@SerializedName("isPrivate") val isPrivate: Boolean? = null, val environmentId: String,
@SerializedName("filters") val filters: List<String>? = null, val createdAt: String,
@SerializedName("environmentId") val environmentId: String? = null, val updatedAt: String,
@SerializedName("surveys") val surveys: List<String>? = null val surveys: List<String>
) )

View File

@@ -30,5 +30,5 @@ data class Survey(
@SerializedName("displayOption") val displayOption: String?, @SerializedName("displayOption") val displayOption: String?,
@SerializedName("segment") val segment: Segment?, @SerializedName("segment") val segment: Segment?,
@SerializedName("styling") val styling: Styling?, @SerializedName("styling") val styling: Styling?,
@SerializedName("languages") val languages: List<SurveyLanguage>? @SerializedName("languages") val languages: List<SurveyLanguage>?,
) )

View File

@@ -1,6 +1,5 @@
package com.formbricks.formbrickssdk.network.queue package com.formbricks.formbrickssdk.network.queue
import com.formbricks.formbrickssdk.Formbricks
import com.formbricks.formbrickssdk.logger.Logger import com.formbricks.formbrickssdk.logger.Logger
import com.formbricks.formbrickssdk.manager.UserManager import com.formbricks.formbrickssdk.manager.UserManager
import com.formbricks.formbrickssdk.model.error.SDKError import com.formbricks.formbrickssdk.model.error.SDKError
@@ -68,7 +67,6 @@ class UpdateQueue private constructor() {
?: UserManager.userId ?: UserManager.userId
if (effectiveUserId == null) { if (effectiveUserId == null) {
val error = SDKError.noUserIdSetError val error = SDKError.noUserIdSetError
Formbricks.callback?.onError(error)
Logger.e(error) Logger.e(error)
return return
} }

View File

@@ -25,7 +25,6 @@ import android.widget.FrameLayout
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import com.formbricks.formbrickssdk.Formbricks
import com.formbricks.formbrickssdk.R import com.formbricks.formbrickssdk.R
import com.formbricks.formbrickssdk.databinding.FragmentFormbricksBinding import com.formbricks.formbrickssdk.databinding.FragmentFormbricksBinding
import com.formbricks.formbrickssdk.logger.Logger import com.formbricks.formbrickssdk.logger.Logger
@@ -37,26 +36,22 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.gson.JsonObject import com.google.gson.JsonObject
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.InputStream import java.io.InputStream
import java.util.Timer
class FormbricksFragment : BottomSheetDialogFragment() { class FormbricksFragment : BottomSheetDialogFragment() {
private lateinit var binding: FragmentFormbricksBinding private lateinit var binding: FragmentFormbricksBinding
private lateinit var surveyId: String private lateinit var surveyId: String
private val closeTimer = Timer()
private val viewModel: FormbricksViewModel by viewModels() private val viewModel: FormbricksViewModel by viewModels()
private var webAppInterface = WebAppInterface(object : WebAppInterface.WebAppCallback { private var webAppInterface = WebAppInterface(object : WebAppInterface.WebAppCallback {
override fun onClose() { override fun onClose() {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
Formbricks.callback?.onSurveyClosed()
dismiss() dismiss()
} }
} }
override fun onDisplayCreated() { override fun onDisplayCreated() {
Formbricks.callback?.onSurveyStarted()
SurveyManager.onNewDisplay(surveyId) SurveyManager.onNewDisplay(surveyId)
} }
@@ -74,7 +69,8 @@ class FormbricksFragment : BottomSheetDialogFragment() {
} }
override fun onSurveyLibraryLoadError() { override fun onSurveyLibraryLoadError() {
Formbricks.callback?.onError(SDKError.unableToLoadFormbicksJs) val error = SDKError.unableToLoadFormbicksJs
Logger.e(error)
dismiss() dismiss()
} }
}) })
@@ -155,7 +151,8 @@ class FormbricksFragment : BottomSheetDialogFragment() {
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean { override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
consoleMessage?.let { cm -> consoleMessage?.let { cm ->
if (cm.messageLevel() == ConsoleMessage.MessageLevel.ERROR) { if (cm.messageLevel() == ConsoleMessage.MessageLevel.ERROR) {
Formbricks.callback?.onError(SDKError.surveyDisplayFetchError) val error = SDKError.surveyDisplayFetchError
Logger.e(error)
dismiss() dismiss()
} }
val log = "[CONSOLE:${cm.messageLevel()}] \"${cm.message()}\", source: ${cm.sourceId()} (${cm.lineNumber()})" val log = "[CONSOLE:${cm.messageLevel()}] \"${cm.message()}\", source: ${cm.sourceId()} (${cm.lineNumber()})"

View File

@@ -99,7 +99,7 @@ class FormbricksViewModel : ViewModel() {
observer.observe(document.body, { childList: true, subtree: true }); observer.observe(document.body, { childList: true, subtree: true });
const script = document.createElement("script"); const script = document.createElement("script");
script.src = "${Formbricks.appUrl}/js/surveys.umd.cjs"; script.src = "${Formbricks.appUrl}/js/surveys.umd.cjs?randQuery=123445";
script.async = true; script.async = true;
script.onload = () => loadSurvey(); script.onload = () => loadSurvey();
script.onerror = (error) => { script.onerror = (error) => {

View File

@@ -1,7 +1,6 @@
package com.formbricks.formbrickssdk.webview package com.formbricks.formbrickssdk.webview
import android.webkit.JavascriptInterface import android.webkit.JavascriptInterface
import com.formbricks.formbrickssdk.Formbricks
import com.formbricks.formbrickssdk.logger.Logger import com.formbricks.formbrickssdk.logger.Logger
import com.formbricks.formbrickssdk.model.javascript.JsMessageData import com.formbricks.formbrickssdk.model.javascript.JsMessageData
import com.formbricks.formbrickssdk.model.javascript.EventType import com.formbricks.formbrickssdk.model.javascript.EventType
@@ -36,15 +35,12 @@ class WebAppInterface(private val callback: WebAppCallback?) {
EventType.ON_SURVEY_LIBRARY_LOAD_ERROR -> { callback?.onSurveyLibraryLoadError() } EventType.ON_SURVEY_LIBRARY_LOAD_ERROR -> { callback?.onSurveyLibraryLoadError() }
} }
} catch (e: Exception) { } catch (e: Exception) {
Formbricks.callback?.onError(e)
Logger.e(RuntimeException(e.message)) Logger.e(RuntimeException(e.message))
} catch (e: JsonParseException) { } catch (e: JsonParseException) {
Logger.e(RuntimeException("Failed to parse JSON message: $data")) Logger.e(RuntimeException("Failed to parse JSON message: $data"))
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
Formbricks.callback?.onError(e)
Logger.e(RuntimeException("Invalid message format: $data")) Logger.e(RuntimeException("Invalid message format: $data"))
} catch (e: Exception) { } catch (e: Exception) {
Formbricks.callback?.onError(e)
Logger.e(RuntimeException("Unexpected error processing message: $data")) Logger.e(RuntimeException("Unexpected error processing message: $data"))
} }
} }

View File

@@ -1,29 +1,10 @@
import UIKit import UIKit
import FormbricksSDK import FormbricksSDK
class AppDelegate: NSObject, UIApplicationDelegate, FormbricksDelegate { class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
Formbricks.delegate = self
return true return true
} }
// MARK: - FormbricksDelegate
func onSurveyStarted() {
print("from the delegate: survey started")
}
func onSurveyFinished() {
print("survey finished")
}
func onSurveyClosed() {
print("survey closed")
}
func onError(_ error: Error) {
print("survey error:", error.localizedDescription)
}
} }

View File

@@ -1,14 +1,6 @@
import Foundation import Foundation
import Network import Network
/// Formbricks SDK delegate protocol. It contains the main methods to interact with the SDK.
public protocol FormbricksDelegate: AnyObject {
func onSurveyStarted()
func onSurveyFinished()
func onSurveyClosed()
func onError(_ error: Error)
}
/// The main class of the Formbricks SDK. It contains the main methods to interact with the SDK. /// The main class of the Formbricks SDK. It contains the main methods to interact with the SDK.
@objc(Formbricks) public class Formbricks: NSObject { @objc(Formbricks) public class Formbricks: NSObject {
@@ -23,7 +15,6 @@ public protocol FormbricksDelegate: AnyObject {
static internal var apiQueue: OperationQueue? = OperationQueue() static internal var apiQueue: OperationQueue? = OperationQueue()
static internal var logger: Logger? static internal var logger: Logger?
static internal var service = FormbricksService() static internal var service = FormbricksService()
public static weak var delegate: FormbricksDelegate?
// make this class not instantiatable outside of the SDK // make this class not instantiatable outside of the SDK
internal override init() { internal override init() {
@@ -58,7 +49,6 @@ public protocol FormbricksDelegate: AnyObject {
guard !isInitialized else { guard !isInitialized else {
let error = FormbricksSDKError(type: .sdkIsAlreadyInitialized) let error = FormbricksSDKError(type: .sdkIsAlreadyInitialized)
delegate?.onError(error)
Formbricks.logger?.error(error.message) Formbricks.logger?.error(error.message)
return return
} }
@@ -101,7 +91,6 @@ public protocol FormbricksDelegate: AnyObject {
@objc public static func setUserId(_ userId: String) { @objc public static func setUserId(_ userId: String) {
guard Formbricks.isInitialized else { guard Formbricks.isInitialized else {
let error = FormbricksSDKError(type: .sdkIsNotInitialized) let error = FormbricksSDKError(type: .sdkIsNotInitialized)
delegate?.onError(error)
Formbricks.logger?.error(error.message) Formbricks.logger?.error(error.message)
return return
} }
@@ -126,7 +115,6 @@ public protocol FormbricksDelegate: AnyObject {
@objc public static func setAttribute(_ attribute: String, forKey key: String) { @objc public static func setAttribute(_ attribute: String, forKey key: String) {
guard Formbricks.isInitialized else { guard Formbricks.isInitialized else {
let error = FormbricksSDKError(type: .sdkIsNotInitialized) let error = FormbricksSDKError(type: .sdkIsNotInitialized)
delegate?.onError(error)
Formbricks.logger?.error(error.message) Formbricks.logger?.error(error.message)
return return
} }
@@ -146,7 +134,6 @@ public protocol FormbricksDelegate: AnyObject {
@objc public static func setAttributes(_ attributes: [String : String]) { @objc public static func setAttributes(_ attributes: [String : String]) {
guard Formbricks.isInitialized else { guard Formbricks.isInitialized else {
let error = FormbricksSDKError(type: .sdkIsNotInitialized) let error = FormbricksSDKError(type: .sdkIsNotInitialized)
delegate?.onError(error)
Formbricks.logger?.error(error.message) Formbricks.logger?.error(error.message)
return return
} }
@@ -166,7 +153,6 @@ public protocol FormbricksDelegate: AnyObject {
@objc public static func setLanguage(_ language: String) { @objc public static func setLanguage(_ language: String) {
guard Formbricks.isInitialized else { guard Formbricks.isInitialized else {
let error = FormbricksSDKError(type: .sdkIsNotInitialized) let error = FormbricksSDKError(type: .sdkIsNotInitialized)
delegate?.onError(error)
Formbricks.logger?.error(error.message) Formbricks.logger?.error(error.message)
return return
} }
@@ -191,7 +177,6 @@ public protocol FormbricksDelegate: AnyObject {
@objc public static func track(_ action: String) { @objc public static func track(_ action: String) {
guard Formbricks.isInitialized else { guard Formbricks.isInitialized else {
let error = FormbricksSDKError(type: .sdkIsNotInitialized) let error = FormbricksSDKError(type: .sdkIsNotInitialized)
delegate?.onError(error)
Formbricks.logger?.error(error.message) Formbricks.logger?.error(error.message)
return return
} }
@@ -218,7 +203,6 @@ public protocol FormbricksDelegate: AnyObject {
@objc public static func logout() { @objc public static func logout() {
guard Formbricks.isInitialized else { guard Formbricks.isInitialized else {
let error = FormbricksSDKError(type: .sdkIsNotInitialized) let error = FormbricksSDKError(type: .sdkIsNotInitialized)
delegate?.onError(error)
Formbricks.logger?.error(error.message) Formbricks.logger?.error(error.message)
return return
} }

View File

@@ -106,7 +106,6 @@ extension SurveyManager {
case .failure: case .failure:
self?.hasApiError = true self?.hasApiError = true
let error = FormbricksSDKError(type: .unableToRefreshEnvironment) let error = FormbricksSDKError(type: .unableToRefreshEnvironment)
Formbricks.delegate?.onError(error)
Formbricks.logger?.error(error.message) Formbricks.logger?.error(error.message)
self?.startErrorTimer() self?.startErrorTimer()
} }
@@ -186,7 +185,6 @@ extension SurveyManager {
return try? JSONDecoder().decode(EnvironmentResponse.self, from: data) return try? JSONDecoder().decode(EnvironmentResponse.self, from: data)
} else { } else {
let error = FormbricksSDKError(type: .unableToRetrieveEnvironment) let error = FormbricksSDKError(type: .unableToRetrieveEnvironment)
Formbricks.delegate?.onError(error)
Formbricks.logger?.error(error.message) Formbricks.logger?.error(error.message)
return nil return nil
} }
@@ -197,7 +195,6 @@ extension SurveyManager {
backingEnvironmentResponse = newValue backingEnvironmentResponse = newValue
} else { } else {
let error = FormbricksSDKError(type: .unableToPersistEnvironment) let error = FormbricksSDKError(type: .unableToPersistEnvironment)
Formbricks.delegate?.onError(error)
Formbricks.logger?.error(error.message) Formbricks.logger?.error(error.message)
} }
} }
@@ -231,7 +228,6 @@ private extension SurveyManager {
default: default:
let error = FormbricksSDKError(type: .invalidDisplayOption) let error = FormbricksSDKError(type: .invalidDisplayOption)
Formbricks.delegate?.onError(error)
Formbricks.logger?.error(error.message) Formbricks.logger?.error(error.message)
return false return false
} }

View File

@@ -102,7 +102,6 @@ final class UserManager: UserManagerSyncable {
self?.surveyManager?.filterSurveys() self?.surveyManager?.filterSurveys()
self?.startSyncTimer() self?.startSyncTimer()
case .failure(let error): case .failure(let error):
Formbricks.delegate?.onError(error)
Formbricks.logger?.error(error) Formbricks.logger?.error(error)
} }
} }

View File

@@ -2,4 +2,5 @@ struct EnvironmentData: Codable {
let surveys: [Survey]? let surveys: [Survey]?
let actionClasses: [ActionClass]? let actionClasses: [ActionClass]?
let project: Project let project: Project
let recaptchaSiteKey: String?
} }

View File

@@ -1,11 +1,198 @@
struct Segment: Codable { import Foundation
let id: String?
let createdAt: String? // MARK: - Connector
let updatedAt: String?
let title: String? enum SegmentConnector: String, Codable {
let description: String? case and
let isPrivate: Bool? case or
let filters: [String]? }
let environmentId: String?
let surveys: [String]? // MARK: - Filter Operators
/// Combined operator set for all filter types
enum FilterOperator: String, Codable {
// Base / Arithmetic
case lessThan
case lessEqual
case greaterThan
case greaterEqual
case equals
case notEquals
// Attribute / String
case contains
case doesNotContain
case startsWith
case endsWith
// Existence
case isSet
case isNotSet
// Segment membership
case userIsIn
case userIsNotIn
}
// MARK: - Filter Value
enum SegmentFilterValue: Codable {
case string(String)
case number(Double)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let num = try? container.decode(Double.self) {
self = .number(num)
} else if let str = try? container.decode(String.self) {
self = .string(str)
} else {
throw DecodingError.typeMismatch(
SegmentFilterValue.self,
DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "Value is neither Double nor String"
)
)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .number(let num):
try container.encode(num)
case .string(let str):
try container.encode(str)
}
}
}
// MARK: - Root
enum SegmentFilterRoot: Codable {
case attribute(contactAttributeKey: String)
case person(personIdentifier: String)
case segment(segmentId: String)
case device(deviceType: String)
private enum CodingKeys: String, CodingKey {
case type
case contactAttributeKey
case personIdentifier
case segmentId
case deviceType
}
private enum RootType: String, Codable {
case attribute
case person
case segment
case device
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(RootType.self, forKey: .type)
switch type {
case .attribute:
let key = try container.decode(String.self, forKey: .contactAttributeKey)
self = .attribute(contactAttributeKey: key)
case .person:
let id = try container.decode(String.self, forKey: .personIdentifier)
self = .person(personIdentifier: id)
case .segment:
let id = try container.decode(String.self, forKey: .segmentId)
self = .segment(segmentId: id)
case .device:
let type = try container.decode(String.self, forKey: .deviceType)
self = .device(deviceType: type)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .attribute(let key):
try container.encode(RootType.attribute, forKey: .type)
try container.encode(key, forKey: .contactAttributeKey)
case .person(let id):
try container.encode(RootType.person, forKey: .type)
try container.encode(id, forKey: .personIdentifier)
case .segment(let id):
try container.encode(RootType.segment, forKey: .type)
try container.encode(id, forKey: .segmentId)
case .device(let type):
try container.encode(RootType.device, forKey: .type)
try container.encode(type, forKey: .deviceType)
}
}
}
// MARK: - Qualifier
struct SegmentFilterQualifier: Codable {
let `operator`: FilterOperator
}
// MARK: - Primitive Filter
struct SegmentPrimitiveFilter: Codable {
let id: String
let root: SegmentFilterRoot
let value: SegmentFilterValue
let qualifier: SegmentFilterQualifier
// Add run-time refinements if needed
}
// MARK: - Recursive Filter Resource
enum SegmentFilterResource: Codable {
case primitive(SegmentPrimitiveFilter)
case group([SegmentFilter])
init(from decoder: Decoder) throws {
// Try primitive first
if let prim = try? SegmentPrimitiveFilter(from: decoder) {
self = .primitive(prim)
} else {
let nested = try [SegmentFilter](from: decoder)
self = .group(nested)
}
}
func encode(to encoder: Encoder) throws {
switch self {
case .primitive(let prim):
try prim.encode(to: encoder)
case .group(let arr):
try arr.encode(to: encoder)
}
}
}
// MARK: - Base Filter (node)
struct SegmentFilter: Codable {
let id: String
let connector: SegmentConnector?
let resource: SegmentFilterResource
}
// MARK: - Segment Model
struct Segment: Codable {
let id: String
let title: String
let description: String?
let isPrivate: Bool
let filters: [SegmentFilter]
let environmentId: String
let createdAt: Date
let updatedAt: Date
let surveys: [String]
private enum CodingKeys: String, CodingKey {
case id, title, description, filters, surveys
case isPrivate = "isPrivate"
case environmentId, createdAt, updatedAt
}
} }

View File

@@ -39,7 +39,6 @@ class APIClient<Request: CodableRequest>: Operation, @unchecked Sendable {
private func processResponse(data: Data?, response: URLResponse?, error: Error?) { private func processResponse(data: Data?, response: URLResponse?, error: Error?) {
guard let httpStatus = (response as? HTTPURLResponse)?.status else { guard let httpStatus = (response as? HTTPURLResponse)?.status else {
let error = FormbricksAPIClientError(type: .invalidResponse) let error = FormbricksAPIClientError(type: .invalidResponse)
Formbricks.delegate?.onError(error)
Formbricks.logger?.error("ERROR \(error.message)") Formbricks.logger?.error("ERROR \(error.message)")
completion?(.failure(error)) completion?(.failure(error))
return return
@@ -57,7 +56,6 @@ class APIClient<Request: CodableRequest>: Operation, @unchecked Sendable {
private func handleSuccessResponse(data: Data?, statusCode: Int, message: inout String) { private func handleSuccessResponse(data: Data?, statusCode: Int, message: inout String) {
guard let data = data else { guard let data = data else {
let error = FormbricksAPIClientError(type: .invalidResponse, statusCode: statusCode) let error = FormbricksAPIClientError(type: .invalidResponse, statusCode: statusCode)
Formbricks.delegate?.onError(error)
completion?(.failure(error)) completion?(.failure(error))
return return
} }
@@ -89,16 +87,13 @@ class APIClient<Request: CodableRequest>: Operation, @unchecked Sendable {
if let error = error { if let error = error {
log.append("\nError: \(error.localizedDescription)") log.append("\nError: \(error.localizedDescription)")
Formbricks.delegate?.onError(error)
Formbricks.logger?.error(log) Formbricks.logger?.error(log)
completion?(.failure(error)) completion?(.failure(error))
} else if let data = data, let apiError = try? request.decoder.decode(FormbricksAPIError.self, from: data) { } else if let data = data, let apiError = try? request.decoder.decode(FormbricksAPIError.self, from: data) {
Formbricks.delegate?.onError(apiError)
Formbricks.logger?.error("\(log)\n\(apiError.getDetailedErrorMessage())") Formbricks.logger?.error("\(log)\n\(apiError.getDetailedErrorMessage())")
completion?(.failure(apiError)) completion?(.failure(apiError))
} else { } else {
let error = FormbricksAPIClientError(type: .responseError, statusCode: statusCode) let error = FormbricksAPIClientError(type: .responseError, statusCode: statusCode)
Formbricks.delegate?.onError(error)
Formbricks.logger?.error("\(log)\n\(error.message)") Formbricks.logger?.error("\(log)\n\(error.message)")
completion?(.failure(error)) completion?(.failure(error))
} }
@@ -119,10 +114,8 @@ class APIClient<Request: CodableRequest>: Operation, @unchecked Sendable {
} }
let error = FormbricksAPIClientError(type: .invalidResponse, statusCode: statusCode) let error = FormbricksAPIClientError(type: .invalidResponse, statusCode: statusCode)
Formbricks.delegate?.onError(error)
Formbricks.logger?.error(error.message) Formbricks.logger?.error(error.message)
completion?(.failure(error)) completion?(.failure(error))
} }
private func logRequest(_ request: URLRequest) { private func logRequest(_ request: URLRequest) {

View File

@@ -101,7 +101,6 @@ private extension UpdateQueue {
guard let userId = effectiveUserId else { guard let userId = effectiveUserId else {
let error = FormbricksSDKError(type: .userIdIsNotSetYet) let error = FormbricksSDKError(type: .userIdIsNotSetYet)
Formbricks.delegate?.onError(error)
Formbricks.logger?.error(error.message) Formbricks.logger?.error(error.message)
return return
} }

View File

@@ -110,7 +110,6 @@ private class WebViewData {
let jsonData = try JSONSerialization.data(withJSONObject: data, options: []) let jsonData = try JSONSerialization.data(withJSONObject: data, options: [])
return String(data: jsonData, encoding: .utf8)?.replacingOccurrences(of: "\\\"", with: "'") return String(data: jsonData, encoding: .utf8)?.replacingOccurrences(of: "\\\"", with: "'")
} catch { } catch {
Formbricks.delegate?.onError(error)
Formbricks.logger?.error(error.message) Formbricks.logger?.error(error.message)
return nil return nil
} }

View File

@@ -119,12 +119,10 @@ final class JsMessageHandler: NSObject, WKScriptMessageHandler {
/// Happens when a survey is shown. /// Happens when a survey is shown.
case .onDisplayCreated: case .onDisplayCreated:
Formbricks.delegate?.onSurveyStarted()
Formbricks.surveyManager?.onNewDisplay(surveyId: surveyId) Formbricks.surveyManager?.onNewDisplay(surveyId: surveyId)
/// Happens when the user closes the survey view with the close button. /// Happens when the user closes the survey view with the close button.
case .onClose: case .onClose:
Formbricks.delegate?.onSurveyClosed()
Formbricks.surveyManager?.dismissSurveyWebView() Formbricks.surveyManager?.dismissSurveyWebView()
/// Happens when the survey wants to open an external link in the default browser. /// Happens when the survey wants to open an external link in the default browser.
@@ -140,7 +138,6 @@ final class JsMessageHandler: NSObject, WKScriptMessageHandler {
} else { } else {
let error = FormbricksSDKError(type: .invalidJavascriptMessage) let error = FormbricksSDKError(type: .invalidJavascriptMessage)
Formbricks.delegate?.onError(error)
Formbricks.logger?.error("\(error.message): \(message.body)") Formbricks.logger?.error("\(error.message): \(message.body)")
} }
} }

View File

@@ -210,7 +210,6 @@ const refineFilters = (filters: TBaseFilters): boolean => {
// The filters can be nested, so we need to use z.lazy to define the type // The filters can be nested, so we need to use z.lazy to define the type
// more on recusrsive types -> https://zod.dev/?id=recursive-types // more on recusrsive types -> https://zod.dev/?id=recursive-types
// TODO: Figure out why this is not working, and then remove the ts-ignore
export const ZSegmentFilters: z.ZodType<TBaseFilters> = z export const ZSegmentFilters: z.ZodType<TBaseFilters> = z
.array( .array(
z.object({ z.object({