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
import android.os.Bundle
import android.util.Log
import android.widget.Button
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.formbricks.formbrickssdk.Formbricks
import com.formbricks.formbrickssdk.FormbricksCallback
import com.formbricks.formbrickssdk.helper.FormbricksConfig
import com.formbricks.formbrickssdk.model.enums.SuccessType
import java.util.UUID
class MainActivity : AppCompatActivity() {
@@ -18,33 +15,6 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
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]")
.setLoggingEnabled(true)
.setFragmentManager(supportFragmentManager)

View File

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

View File

@@ -10,22 +10,10 @@ import com.formbricks.formbrickssdk.helper.FormbricksConfig
import com.formbricks.formbrickssdk.logger.Logger
import com.formbricks.formbrickssdk.manager.SurveyManager
import com.formbricks.formbrickssdk.manager.UserManager
import com.formbricks.formbrickssdk.model.enums.SuccessType
import com.formbricks.formbrickssdk.model.error.SDKError
import com.formbricks.formbrickssdk.webview.FormbricksFragment
import java.lang.RuntimeException
@Keep
interface FormbricksCallback {
fun onSurveyStarted()
fun onSurveyFinished()
fun onSurveyClosed()
fun onPageCommitVisible()
fun onError(error: Exception)
fun onSuccess(successType: SuccessType)
}
@Keep
object Formbricks {
internal lateinit var applicationContext: Context
@@ -37,8 +25,6 @@ object Formbricks {
private var fragmentManager: FragmentManager? = null
internal var isInitialized = false
var callback: FormbricksCallback? = null
/**
* 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.
@@ -62,7 +48,6 @@ object Formbricks {
fun setup(context: Context, config: FormbricksConfig, forceRefresh: Boolean = false) {
if (isInitialized && !forceRefresh) {
val error = SDKError.sdkIsAlreadyInitialized
callback?.onError(error)
Logger.e(error)
return
}
@@ -97,14 +82,12 @@ object Formbricks {
fun setUserId(userId: String) {
if (!isInitialized) {
val error = SDKError.sdkIsNotInitialized
callback?.onError(error)
Logger.e(error)
return
}
if(UserManager.userId != null) {
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)
return
}
@@ -124,7 +107,6 @@ object Formbricks {
fun setAttribute(attribute: String, key: String) {
if (!isInitialized) {
val error = SDKError.sdkIsNotInitialized
callback?.onError(error)
Logger.e(error)
return
}
@@ -143,7 +125,6 @@ object Formbricks {
fun setAttributes(attributes: Map<String, String>) {
if (!isInitialized) {
val error = SDKError.sdkIsNotInitialized
callback?.onError(error)
Logger.e(error)
return
}
@@ -162,7 +143,6 @@ object Formbricks {
fun setLanguage(language: String) {
if (!isInitialized) {
val error = SDKError.sdkIsNotInitialized
callback?.onError(error)
Logger.e(error)
return
}
@@ -182,14 +162,12 @@ object Formbricks {
fun track(action: String) {
if (!isInitialized) {
val error = SDKError.sdkIsNotInitialized
callback?.onError(error)
Logger.e(error)
return
}
if (!isInternetAvailable()) {
val error = SDKError.connectionIsNotAvailable
callback?.onError(error)
Logger.e(error)
return
}
@@ -209,12 +187,10 @@ object Formbricks {
fun logout() {
if (!isInitialized) {
val error = SDKError.sdkIsNotInitialized
callback?.onError(error)
Logger.e(error)
return
}
callback?.onSuccess(SuccessType.LOGOUT_SUCCESS)
UserManager.logout()
}
@@ -236,7 +212,6 @@ object Formbricks {
internal fun showSurvey(id: String) {
if (fragmentManager == null) {
val error = SDKError.fragmentManagerIsNotSet
callback?.onError(error)
Logger.e(error)
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.Survey
import com.formbricks.formbrickssdk.model.error.SDKError
import com.formbricks.formbrickssdk.model.enums.SuccessType
import com.formbricks.formbrickssdk.model.user.Display
import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope
@@ -60,7 +59,6 @@ object SurveyManager {
try {
Gson().fromJson(json, EnvironmentDataHolder::class.java)
} catch (e: Exception) {
Formbricks.callback?.onError(e)
Logger.e(RuntimeException("Unable to retrieve environment data from the local storage."))
null
}
@@ -118,11 +116,9 @@ object SurveyManager {
startRefreshTimer(environmentDataHolder?.expiresAt())
filterSurveys()
hasApiError = false
Formbricks.callback?.onSuccess(SuccessType.GET_ENVIRONMENT_SUCCESS)
} catch (e: Exception) {
hasApiError = true
val error = SDKError.unableToRefreshEnvironment
Formbricks.callback?.onError(error)
Logger.e(error)
startErrorTimer()
}
@@ -134,6 +130,7 @@ object SurveyManager {
* Handles the display percentage and the delay of the survey.
*/
fun track(action: String) {
print(environmentDataHolder?.data?.data?.actionClasses)
val actionClasses = environmentDataHolder?.data?.data?.actionClasses ?: listOf()
val codeActionClasses = actionClasses.filter { it.type == "code" }
val actionClass = codeActionClasses.firstOrNull { it.key == action }
@@ -143,7 +140,8 @@ object SurveyManager {
}
if (firstSurveyWithActionClass == null) {
Formbricks.callback?.onError(SDKError.surveyNotFoundError)
val error = SDKError.surveyNotFoundError
Logger.e(error)
return
}
@@ -154,7 +152,6 @@ object SurveyManager {
if (languageCode == null) {
val error = RuntimeException("Survey “${firstSurveyWithActionClass.name}” is not available in language “$currentLanguage”. Skipping.")
Formbricks.callback?.onError(error)
Logger.e(error)
return
}
@@ -177,7 +174,8 @@ object SurveyManager {
}, Date(System.currentTimeMillis() + timeout.toLong() * 1000))
}
} else {
Formbricks.callback?.onError(SDKError.surveyNotDisplayedError)
val error = SDKError.surveyNotDisplayedError
Logger.e(error)
}
}
@@ -192,7 +190,6 @@ object SurveyManager {
fun postResponse(surveyId: String?) {
val id = surveyId.guard {
val error = SDKError.missingSurveyId
Formbricks.callback?.onError(error)
Logger.e(error)
return
}
@@ -206,7 +203,6 @@ object SurveyManager {
fun onNewDisplay(surveyId: String?) {
val id = surveyId.guard {
val error = SDKError.missingSurveyId
Formbricks.callback?.onError(error)
Logger.e(error)
return
}
@@ -269,7 +265,6 @@ object SurveyManager {
else -> {
val error = SDKError.invalidDisplayOption
Formbricks.callback?.onError(error)
Logger.e(error)
false
}

View File

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

View File

@@ -7,5 +7,6 @@ import kotlinx.serialization.Serializable
data class EnvironmentData(
@SerializedName("surveys") val surveys: List<Survey>?,
@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
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Serializable
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
@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
data class Segment(
@SerializedName("id") val id: String? = null,
@SerializedName("createdAt") val createdAt: String? = null,
@SerializedName("updatedAt") val updatedAt: String? = null,
@SerializedName("title") val title: String? = null,
@SerializedName("description") val description: String? = null,
@SerializedName("isPrivate") val isPrivate: Boolean? = null,
@SerializedName("filters") val filters: List<String>? = null,
@SerializedName("environmentId") val environmentId: String? = null,
@SerializedName("surveys") val surveys: List<String>? = null
)
val id: String,
val title: String,
val description: String? = null,
@SerialName("isPrivate") val isPrivate: Boolean,
val filters: List<SegmentFilter>,
val environmentId: String,
val createdAt: String,
val updatedAt: String,
val surveys: List<String>
)

View File

@@ -30,5 +30,5 @@ data class Survey(
@SerializedName("displayOption") val displayOption: String?,
@SerializedName("segment") val segment: Segment?,
@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
import com.formbricks.formbrickssdk.Formbricks
import com.formbricks.formbrickssdk.logger.Logger
import com.formbricks.formbrickssdk.manager.UserManager
import com.formbricks.formbrickssdk.model.error.SDKError
@@ -68,7 +67,6 @@ class UpdateQueue private constructor() {
?: UserManager.userId
if (effectiveUserId == null) {
val error = SDKError.noUserIdSetError
Formbricks.callback?.onError(error)
Logger.e(error)
return
}

View File

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

View File

@@ -1,7 +1,6 @@
package com.formbricks.formbrickssdk.webview
import android.webkit.JavascriptInterface
import com.formbricks.formbrickssdk.Formbricks
import com.formbricks.formbrickssdk.logger.Logger
import com.formbricks.formbrickssdk.model.javascript.JsMessageData
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() }
}
} catch (e: Exception) {
Formbricks.callback?.onError(e)
Logger.e(RuntimeException(e.message))
} catch (e: JsonParseException) {
Logger.e(RuntimeException("Failed to parse JSON message: $data"))
} catch (e: IllegalArgumentException) {
Formbricks.callback?.onError(e)
Logger.e(RuntimeException("Invalid message format: $data"))
} catch (e: Exception) {
Formbricks.callback?.onError(e)
Logger.e(RuntimeException("Unexpected error processing message: $data"))
}
}

View File

@@ -1,29 +1,10 @@
import UIKit
import FormbricksSDK
class AppDelegate: NSObject, UIApplicationDelegate, FormbricksDelegate {
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
Formbricks.delegate = self
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 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.
@objc(Formbricks) public class Formbricks: NSObject {
@@ -23,7 +15,6 @@ public protocol FormbricksDelegate: AnyObject {
static internal var apiQueue: OperationQueue? = OperationQueue()
static internal var logger: Logger?
static internal var service = FormbricksService()
public static weak var delegate: FormbricksDelegate?
// make this class not instantiatable outside of the SDK
internal override init() {
@@ -58,7 +49,6 @@ public protocol FormbricksDelegate: AnyObject {
guard !isInitialized else {
let error = FormbricksSDKError(type: .sdkIsAlreadyInitialized)
delegate?.onError(error)
Formbricks.logger?.error(error.message)
return
}
@@ -101,7 +91,6 @@ public protocol FormbricksDelegate: AnyObject {
@objc public static func setUserId(_ userId: String) {
guard Formbricks.isInitialized else {
let error = FormbricksSDKError(type: .sdkIsNotInitialized)
delegate?.onError(error)
Formbricks.logger?.error(error.message)
return
}
@@ -126,7 +115,6 @@ public protocol FormbricksDelegate: AnyObject {
@objc public static func setAttribute(_ attribute: String, forKey key: String) {
guard Formbricks.isInitialized else {
let error = FormbricksSDKError(type: .sdkIsNotInitialized)
delegate?.onError(error)
Formbricks.logger?.error(error.message)
return
}
@@ -146,7 +134,6 @@ public protocol FormbricksDelegate: AnyObject {
@objc public static func setAttributes(_ attributes: [String : String]) {
guard Formbricks.isInitialized else {
let error = FormbricksSDKError(type: .sdkIsNotInitialized)
delegate?.onError(error)
Formbricks.logger?.error(error.message)
return
}
@@ -166,7 +153,6 @@ public protocol FormbricksDelegate: AnyObject {
@objc public static func setLanguage(_ language: String) {
guard Formbricks.isInitialized else {
let error = FormbricksSDKError(type: .sdkIsNotInitialized)
delegate?.onError(error)
Formbricks.logger?.error(error.message)
return
}
@@ -191,7 +177,6 @@ public protocol FormbricksDelegate: AnyObject {
@objc public static func track(_ action: String) {
guard Formbricks.isInitialized else {
let error = FormbricksSDKError(type: .sdkIsNotInitialized)
delegate?.onError(error)
Formbricks.logger?.error(error.message)
return
}
@@ -218,7 +203,6 @@ public protocol FormbricksDelegate: AnyObject {
@objc public static func logout() {
guard Formbricks.isInitialized else {
let error = FormbricksSDKError(type: .sdkIsNotInitialized)
delegate?.onError(error)
Formbricks.logger?.error(error.message)
return
}

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,198 @@
struct Segment: Codable {
let id: String?
let createdAt: String?
let updatedAt: String?
let title: String?
let description: String?
let isPrivate: Bool?
let filters: [String]?
let environmentId: String?
let surveys: [String]?
import Foundation
// MARK: - Connector
enum SegmentConnector: String, Codable {
case and
case or
}
// 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?) {
guard let httpStatus = (response as? HTTPURLResponse)?.status else {
let error = FormbricksAPIClientError(type: .invalidResponse)
Formbricks.delegate?.onError(error)
Formbricks.logger?.error("ERROR \(error.message)")
completion?(.failure(error))
return
@@ -57,7 +56,6 @@ class APIClient<Request: CodableRequest>: Operation, @unchecked Sendable {
private func handleSuccessResponse(data: Data?, statusCode: Int, message: inout String) {
guard let data = data else {
let error = FormbricksAPIClientError(type: .invalidResponse, statusCode: statusCode)
Formbricks.delegate?.onError(error)
completion?(.failure(error))
return
}
@@ -89,16 +87,13 @@ class APIClient<Request: CodableRequest>: Operation, @unchecked Sendable {
if let error = error {
log.append("\nError: \(error.localizedDescription)")
Formbricks.delegate?.onError(error)
Formbricks.logger?.error(log)
completion?(.failure(error))
} 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())")
completion?(.failure(apiError))
} else {
let error = FormbricksAPIClientError(type: .responseError, statusCode: statusCode)
Formbricks.delegate?.onError(error)
Formbricks.logger?.error("\(log)\n\(error.message)")
completion?(.failure(error))
}
@@ -119,10 +114,8 @@ class APIClient<Request: CodableRequest>: Operation, @unchecked Sendable {
}
let error = FormbricksAPIClientError(type: .invalidResponse, statusCode: statusCode)
Formbricks.delegate?.onError(error)
Formbricks.logger?.error(error.message)
completion?(.failure(error))
}
private func logRequest(_ request: URLRequest) {

View File

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

View File

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

View File

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