fix: android sdk callbacks, tweaks and fixes (#5487)

This commit is contained in:
Anshuman Pandey
2025-04-23 19:07:22 +05:30
committed by GitHub
parent e1bbb0a10f
commit 36943bb786
19 changed files with 429 additions and 61 deletions
@@ -10,6 +10,7 @@ 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() {
@@ -30,8 +31,16 @@ class MainActivity : AppCompatActivity() {
Log.d("FormbricksCallback", "onSurveyClosed")
}
override fun onPageCommitVisible() {
Log.d("FormbricksCallback", "onPageCommitVisible")
}
override fun onError(error: Exception) {
Log.d("FormbricksCallback", "onError: ${error.localizedMessage}")
Log.d("FormbricksCallback", "onError from the CB: ${error.localizedMessage}")
}
override fun onSuccess(successType: SuccessType) {
Log.d("FormbricksCallback", "onSuccess: ${successType.name}")
}
}
@@ -39,10 +48,8 @@ class MainActivity : AppCompatActivity() {
val config = FormbricksConfig.Builder("[appUrl]","[environmentId]")
.setLoggingEnabled(true)
.setFragmentManager(supportFragmentManager)
Formbricks.setup(this, config.build())
Formbricks.logout()
Formbricks.setUserId(UUID.randomUUID().toString())
Formbricks.setup(this, config.build())
setContentView(R.layout.activity_main)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
@@ -55,5 +62,30 @@ class MainActivity : AppCompatActivity() {
button.setOnClickListener {
Formbricks.track("click_demo_button")
}
val setUserIdButton = findViewById<Button>(R.id.setUserId)
setUserIdButton.setOnClickListener {
Formbricks.setUserId(UUID.randomUUID().toString())
}
val setAttributeButton = findViewById<Button>(R.id.setAttribute)
setAttributeButton.setOnClickListener {
Formbricks.setAttribute("test@web.com", "email")
}
val setAttributesButton = findViewById<Button>(R.id.setAttributes)
setAttributesButton.setOnClickListener {
Formbricks.setAttributes(mapOf(Pair("attr1", "val1"), Pair("attr2", "val2")))
}
val setLanguageButton = findViewById<Button>(R.id.setLanguage)
setLanguageButton.setOnClickListener {
Formbricks.setLanguage("vi")
}
val logoutButton = findViewById<Button>(R.id.logout)
logoutButton.setOnClickListener {
Formbricks.logout()
}
}
}
@@ -11,11 +11,83 @@
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Click me!"
android:text="Track Action"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.495"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:layout_editor_absoluteX="158dp"
tools:layout_editor_absoluteY="336dp" />
app:layout_constraintVertical_bias="0.24" />
<Button
android:id="@+id/setUserId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="161dp"
android:layout_marginTop="27dp"
android:layout_marginEnd="154dp"
android:layout_marginBottom="12dp"
android:text="setUserId"
android:visibility="visible"
app:layout_constraintBottom_toTopOf="@+id/setLanguage"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button"
tools:visibility="visible" />
<Button
android:id="@+id/setLanguage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="161dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="128dp"
android:layout_marginBottom="11dp"
android:text="setLanguage"
app:layout_constraintBottom_toTopOf="@+id/setAttribute"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/setUserId" />
<Button
android:id="@+id/setAttribute"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="161dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="128dp"
android:layout_marginBottom="2dp"
android:text="setAttribute"
app:layout_constraintBottom_toTopOf="@+id/setAttributes"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/setLanguage" />
<Button
android:id="@+id/setAttributes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="161dp"
android:layout_marginTop="1dp"
android:layout_marginEnd="120dp"
android:layout_marginBottom="10dp"
android:text="setAttributes"
app:layout_constraintBottom_toTopOf="@+id/logout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/setAttribute" />
<Button
android:id="@+id/logout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="177dp"
android:layout_marginTop="9dp"
android:layout_marginEnd="146dp"
android:layout_marginBottom="199dp"
android:text="logout"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/setAttributes" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">192.168.0.12</domain>
<domain includeSubdomains="true">192.168.0.200</domain>
<domain includeSubdomains="true">localhost</domain>
</domain-config>
</network-security-config>
@@ -1,3 +1,5 @@
-keep class com.formbricks.formbrickssdk.DataBinderMapperImpl { *; }
-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 interface com.formbricks.formbrickssdk.FormbricksCallback { *; }
+3 -1
View File
@@ -33,4 +33,6 @@
-keep class com.formbricks.formbrickssdk.DataBinderMapperImpl { *; }
-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 interface com.formbricks.formbrickssdk.FormbricksCallback { *; }
@@ -10,8 +10,21 @@ 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 {
@@ -24,6 +37,8 @@ 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.
@@ -44,7 +59,14 @@ object Formbricks {
* ```
*
*/
fun setup(context: Context, config: FormbricksConfig) {
fun setup(context: Context, config: FormbricksConfig, forceRefresh: Boolean = false) {
if (isInitialized && !forceRefresh) {
val error = SDKError.sdkIsAlreadyInitialized
callback?.onError(error)
Logger.e(error)
return
}
applicationContext = context
appUrl = config.appUrl
@@ -57,7 +79,7 @@ object Formbricks {
config.attributes?.get("language")?.let { UserManager.setLanguage(it) }
FormbricksApi.initialize()
SurveyManager.refreshEnvironmentIfNeeded()
SurveyManager.refreshEnvironmentIfNeeded(force = forceRefresh)
UserManager.syncUserStateIfNeeded()
isInitialized = true
@@ -74,9 +96,19 @@ object Formbricks {
*/
fun setUserId(userId: String) {
if (!isInitialized) {
Logger.e(exception = SDKError.sdkIsNotInitialized)
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
}
UserManager.set(userId)
}
@@ -91,7 +123,9 @@ object Formbricks {
*/
fun setAttribute(attribute: String, key: String) {
if (!isInitialized) {
Logger.e(exception = SDKError.sdkIsNotInitialized)
val error = SDKError.sdkIsNotInitialized
callback?.onError(error)
Logger.e(error)
return
}
UserManager.addAttribute(attribute, key)
@@ -108,7 +142,9 @@ object Formbricks {
*/
fun setAttributes(attributes: Map<String, String>) {
if (!isInitialized) {
Logger.e(exception = SDKError.sdkIsNotInitialized)
val error = SDKError.sdkIsNotInitialized
callback?.onError(error)
Logger.e(error)
return
}
UserManager.setAttributes(attributes)
@@ -125,7 +161,9 @@ object Formbricks {
*/
fun setLanguage(language: String) {
if (!isInitialized) {
Logger.e(exception = SDKError.sdkIsNotInitialized)
val error = SDKError.sdkIsNotInitialized
callback?.onError(error)
Logger.e(error)
return
}
Formbricks.language = language
@@ -143,12 +181,16 @@ object Formbricks {
*/
fun track(action: String) {
if (!isInitialized) {
Logger.e(exception = SDKError.sdkIsNotInitialized)
val error = SDKError.sdkIsNotInitialized
callback?.onError(error)
Logger.e(error)
return
}
if (!isInternetAvailable()) {
Logger.w(exception = SDKError.connectionIsNotAvailable)
val error = SDKError.connectionIsNotAvailable
callback?.onError(error)
Logger.e(error)
return
}
@@ -166,10 +208,13 @@ object Formbricks {
*/
fun logout() {
if (!isInitialized) {
Logger.e(exception = SDKError.sdkIsNotInitialized)
val error = SDKError.sdkIsNotInitialized
callback?.onError(error)
Logger.e(error)
return
}
callback?.onSuccess(SuccessType.LOGOUT_SUCCESS)
UserManager.logout()
}
@@ -190,7 +235,9 @@ object Formbricks {
/// Assembles the survey fragment and presents it
internal fun showSurvey(id: String) {
if (fragmentManager == null) {
Logger.e(exception = SDKError.fragmentManagerIsNotSet)
val error = SDKError.fragmentManagerIsNotSet
callback?.onError(error)
Logger.e(error)
return
}
@@ -6,11 +6,26 @@ import com.formbricks.formbrickssdk.model.user.PostUserBody
import com.formbricks.formbrickssdk.model.user.UserResponse
import com.formbricks.formbrickssdk.network.FormbricksApiService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
object FormbricksApi {
var service = FormbricksApiService()
private suspend fun <T> retryApiCall(
retries: Int = 2,
delayTime: Long = 1000,
block: suspend () -> Result<T>
): Result<T> {
repeat(retries) { attempt ->
val result = block()
if (result.isSuccess) return result
println("⚠️ Retry ${attempt + 1} due to error: ${result.exceptionOrNull()?.localizedMessage}")
delay(delayTime)
}
return block()
}
fun initialize() {
service.initialize(
appUrl = Formbricks.appUrl,
@@ -19,21 +34,25 @@ object FormbricksApi {
}
suspend fun getEnvironmentState(): Result<EnvironmentDataHolder> = withContext(Dispatchers.IO) {
try {
val response = service.getEnvironmentStateObject(Formbricks.environmentId)
val result = response.getOrThrow()
Result.success(result)
} catch (e: Exception) {
Result.failure(e)
retryApiCall {
try {
val response = service.getEnvironmentStateObject(Formbricks.environmentId)
val result = response.getOrThrow()
Result.success(result)
} catch (e: Exception) {
Result.failure(e)
}
}
}
suspend fun postUser(userId: String, attributes: Map<String, *>?): Result<UserResponse> = withContext(Dispatchers.IO) {
try {
val result = service.postUser(Formbricks.environmentId, PostUserBody.create(userId, attributes)).getOrThrow()
Result.success(result)
} catch (e: Exception) {
Result.failure(e)
retryApiCall {
try {
val result = service.postUser(Formbricks.environmentId, PostUserBody.create(userId, attributes)).getOrThrow()
Result.success(result)
} catch (e: Exception) {
Result.failure(e)
}
}
}
}
@@ -10,9 +10,9 @@ object Logger {
}
}
fun e(message: String? = "Exception", exception: RuntimeException? = null) {
fun e(exception: RuntimeException) {
if (Formbricks.loggingEnabled) {
Log.e("FormbricksSDK", message, exception)
Log.e("FormbricksSDK", exception.localizedMessage, exception)
}
}
@@ -8,11 +8,14 @@ import com.formbricks.formbrickssdk.extensions.guard
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.lang.RuntimeException
import java.util.Date
import java.util.Timer
import java.util.TimerTask
@@ -57,7 +60,8 @@ object SurveyManager {
try {
Gson().fromJson(json, EnvironmentDataHolder::class.java)
} catch (e: Exception) {
Logger.e("Unable to retrieve environment data from the local storage.")
Formbricks.callback?.onError(e)
Logger.e(RuntimeException("Unable to retrieve environment data from the local storage."))
null
}
}
@@ -114,9 +118,12 @@ object SurveyManager {
startRefreshTimer(environmentDataHolder?.expiresAt())
filterSurveys()
hasApiError = false
Formbricks.callback?.onSuccess(SuccessType.GET_ENVIRONMENT_SUCCESS)
} catch (e: Exception) {
hasApiError = true
Logger.e("Unable to refresh environment state.")
val error = SDKError.unableToRefreshEnvironment
Formbricks.callback?.onError(error)
Logger.e(error)
startErrorTimer()
}
}
@@ -135,10 +142,30 @@ object SurveyManager {
triggers.firstOrNull { it.actionClass?.name.equals(actionClass?.name) } != null
}
val shouldDisplay = shouldDisplayBasedOnPercentage(firstSurveyWithActionClass?.displayPercentage)
if (firstSurveyWithActionClass == null) {
Formbricks.callback?.onError(SDKError.surveyNotFoundError)
return
}
val isMultiLangSurvey = (firstSurveyWithActionClass.languages?.size ?: 0) > 1
if(isMultiLangSurvey) {
val currentLanguage = Formbricks.language
val languageCode = getLanguageCode(firstSurveyWithActionClass, currentLanguage)
if (languageCode == null) {
val error = RuntimeException("Survey “${firstSurveyWithActionClass.name}” is not available in language “$currentLanguage”. Skipping.")
Formbricks.callback?.onError(error)
Logger.e(error)
return
}
Formbricks.setLanguage(languageCode)
}
val shouldDisplay = shouldDisplayBasedOnPercentage(firstSurveyWithActionClass.displayPercentage)
if (shouldDisplay) {
firstSurveyWithActionClass?.id?.let {
firstSurveyWithActionClass.id.let {
isShowingSurvey = true
val timeout = firstSurveyWithActionClass.delay ?: 0.0
stopDisplayTimer()
@@ -149,6 +176,8 @@ object SurveyManager {
}, Date(System.currentTimeMillis() + timeout.toLong() * 1000))
}
} else {
Formbricks.callback?.onError(SDKError.surveyNotDisplayedError)
}
}
@@ -166,7 +195,9 @@ object SurveyManager {
*/
fun postResponse(surveyId: String?) {
val id = surveyId.guard {
Logger.e("Survey id is mandatory to set.")
val error = SDKError.missingSurveyId
Formbricks.callback?.onError(error)
Logger.e(error)
return
}
@@ -178,7 +209,9 @@ object SurveyManager {
*/
fun onNewDisplay(surveyId: String?) {
val id = surveyId.guard {
Logger.e("Survey id is mandatory to set.")
val error = SDKError.missingSurveyId
Formbricks.callback?.onError(error)
Logger.e(error)
return
}
@@ -239,7 +272,9 @@ object SurveyManager {
}
else -> {
Logger.e("Invalid Display Option")
val error = SDKError.invalidDisplayOption
Formbricks.callback?.onError(error)
Logger.e(error)
false
}
}
@@ -282,4 +317,39 @@ object SurveyManager {
val randomNum = (0 until 10000).random() / 100.0
return randomNum <= percentage
}
private fun getLanguageCode(survey: Survey, language: String?): String? {
// 1) Gather all valid codes
val availableLanguageCodes = survey.languages
?.map { it.language.code }
?: emptyList()
// 2) No input or explicit "default" → default
val raw = language
?.lowercase()
?.takeIf { it.isNotEmpty() }
?: return "default"
if (raw == "default") return "default"
// 3) Find matching entry by code or alias
val selected = survey.languages
?.firstOrNull { entry ->
entry.language.code.lowercase() == raw ||
entry.language.alias?.lowercase() == raw
}
// 4) If that entry is marked default → default
if (selected?.default == true) return "default"
// 5) If missing, disabled, or not in the available list → null
if (selected == null
|| !selected.enabled
|| !availableLanguageCodes.contains(selected.language.code)
) {
return null
}
// 6) Otherwise return its code
return selected.language.code
}
}
@@ -8,6 +8,8 @@ import com.formbricks.formbrickssdk.extensions.expiresAt
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
@@ -136,11 +138,20 @@ object UserManager {
responses = userResponse.data.state.data.responses
lastDisplayedAt = userResponse.data.state.data.lastDisplayAt()
expiresAt = userResponse.data.state.expiresAt()
val languageFromUserResponse = userResponse.data.state.data.language
if(languageFromUserResponse != null) {
Formbricks.language = languageFromUserResponse
}
UpdateQueue.current.reset()
SurveyManager.filterSurveys()
startSyncTimer()
Formbricks.callback?.onSuccess(SuccessType.SET_USER_SUCCESS)
} catch (e: Exception) {
Logger.e("Unable to post survey response.")
val error = SDKError.unableToPostResponse
Formbricks.callback?.onError(error)
Logger.e(error)
}
}
}
@@ -149,7 +160,16 @@ object UserManager {
* Logs out the user and clears the user state.
*/
fun logout() {
val isUserIdDefined = userId != null
if (!isUserIdDefined) {
val error = SDKError.noUserIdSetError
Formbricks.callback?.onError(error)
Logger.e(error)
}
prefManager.edit().apply {
remove(CONTACT_ID_KEY)
remove(USER_ID_KEY)
remove(SEGMENTS_KEY)
remove(DISPLAYS_KEY)
@@ -158,13 +178,20 @@ object UserManager {
remove(EXPIRES_AT_KEY)
apply()
}
backingUserId = null
backingContactId = null
backingSegments = null
backingDisplays = null
backingResponses = null
backingLastDisplayedAt = null
backingExpiresAt = null
Formbricks.language = "default"
UpdateQueue.current.reset()
if(isUserIdDefined) {
Logger.d("User logged out successfully!")
}
}
private fun startSyncTimer() {
@@ -0,0 +1,7 @@
package com.formbricks.formbrickssdk.model.enums
enum class SuccessType {
SET_USER_SUCCESS,
GET_ENVIRONMENT_SUCCESS,
LOGOUT_SUCCESS
}
@@ -3,6 +3,21 @@ package com.formbricks.formbrickssdk.model.environment
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Serializable
@Serializable
data class SurveyLanguage(
@SerializedName("enabled") val enabled: Boolean,
@SerializedName("default") val default: Boolean,
@SerializedName("language") val language: LanguageDetail
)
@Serializable
data class LanguageDetail(
@SerializedName("id") val id: String,
@SerializedName("code") val code: String,
@SerializedName("alias") val alias: String?,
@SerializedName("projectId") val projectId: String
)
@Serializable
data class Survey(
@SerializedName("id") val id: String,
@@ -15,4 +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>?
)
@@ -1,7 +1,27 @@
package com.formbricks.formbrickssdk.model.error
import androidx.annotation.Keep
@Keep
object SDKError {
// Errors related to SDK initialization and configuration
val sdkIsNotInitialized = RuntimeException("Formbricks SDK is not initialized")
val sdkIsAlreadyInitialized = RuntimeException("Formbricks SDK is already initialized")
val fragmentManagerIsNotSet = RuntimeException("The fragment manager is not set.")
// Errors related to network and connectivity
val connectionIsNotAvailable = RuntimeException("There is no connection.")
}
val unableToLoadFormbicksJs = RuntimeException("Unable to load Formbricks Javascript package.")
// Errors related to surveys
val surveyDisplayFetchError =
RuntimeException("Error: creating display: TypeError: Failure to fetch the survey data.")
val surveyNotDisplayedError = RuntimeException("Survey was not displayed due to display percentage restrictions.")
val unableToRefreshEnvironment = RuntimeException("Unable to refresh environment state.")
val missingSurveyId = RuntimeException("Survey id is mandatory to set.")
val invalidDisplayOption = RuntimeException("Invalid Display Option.")
val unableToPostResponse = RuntimeException("Unable to post survey response.")
val surveyNotFoundError = RuntimeException("No survey found matching the action class.")
val noUserIdSetError = RuntimeException("No userId is set, please set a userId first using the setUserId function")
}
@@ -8,5 +8,6 @@ data class UserStateData(
@SerializedName("segments") val segments: List<String>?,
@SerializedName("displays") val displays: List<Display>?,
@SerializedName("responses") val responses: List<String>?,
@SerializedName("lastDisplayAt") val lastDisplayAt: String?
@SerializedName("lastDisplayAt") val lastDisplayAt: String?,
@SerializedName("language") val language: String?
)
@@ -9,7 +9,6 @@ import retrofit2.http.POST
import retrofit2.http.Path
interface FormbricksService {
@GET("$API_PREFIX/client/{environmentId}/environment")
fun getEnvironmentState(@Path("environmentId") environmentId: String): Call<Map<String, Any>>
@@ -19,5 +18,4 @@ interface FormbricksService {
companion object {
const val API_PREFIX = "/api/v2"
}
}
@@ -1,7 +1,9 @@
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
import java.util.*
import kotlin.concurrent.timer
@@ -36,8 +38,15 @@ class UpdateQueue private constructor() {
}
fun setLanguage(language: String) {
addAttribute("language", language)
startDebounceTimer()
val effectiveUserId = userId ?: UserManager.userId
if(effectiveUserId != null) {
addAttribute("language", language)
startDebounceTimer()
} else {
Logger.d("UpdateQueue - updating language locally: ${language}")
return
}
}
fun reset() {
@@ -55,14 +64,17 @@ class UpdateQueue private constructor() {
}
private fun commit() {
val currentUserId = userId
if (currentUserId == null) {
Logger.d("Error: User ID is not set yet")
val effectiveUserId = userId
?: UserManager.userId
if (effectiveUserId == null) {
val error = SDKError.noUserIdSetError
Formbricks.callback?.onError(error)
Logger.e(error)
return
}
Logger.d("UpdateQueue - commit() called on UpdateQueue with $currentUserId and $attributes")
UserManager.syncUser(currentUserId, attributes)
Logger.d("UpdateQueue - commit() called on UpdateQueue with $effectiveUserId and $attributes")
UserManager.syncUser(effectiveUserId, attributes)
}
companion object {
@@ -7,6 +7,8 @@ import android.content.Intent
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.OpenableColumns
import android.util.Base64
import android.view.LayoutInflater
@@ -15,7 +17,10 @@ import android.view.ViewGroup
import android.view.WindowManager
import android.webkit.ConsoleMessage
import android.webkit.WebChromeClient
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.FrameLayout
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.FragmentManager
@@ -25,15 +30,14 @@ import com.formbricks.formbrickssdk.R
import com.formbricks.formbrickssdk.databinding.FragmentFormbricksBinding
import com.formbricks.formbrickssdk.logger.Logger
import com.formbricks.formbrickssdk.manager.SurveyManager
import com.formbricks.formbrickssdk.model.error.SDKError
import com.formbricks.formbrickssdk.model.javascript.FileUploadData
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.gson.JsonObject
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.util.Date
import java.util.Timer
import java.util.TimerTask
class FormbricksFragment : BottomSheetDialogFragment() {
@@ -45,10 +49,14 @@ class FormbricksFragment : BottomSheetDialogFragment() {
private var webAppInterface = WebAppInterface(object : WebAppInterface.WebAppCallback {
override fun onClose() {
dismiss()
Handler(Looper.getMainLooper()).post {
Formbricks.callback?.onSurveyClosed()
dismiss()
}
}
override fun onDisplayCreated() {
Formbricks.callback?.onSurveyStarted()
SurveyManager.onNewDisplay(surveyId)
}
@@ -66,6 +74,7 @@ class FormbricksFragment : BottomSheetDialogFragment() {
}
override fun onSurveyLibraryLoadError() {
Formbricks.callback?.onError(SDKError.unableToLoadFormbicksJs)
dismiss()
}
})
@@ -139,6 +148,7 @@ class FormbricksFragment : BottomSheetDialogFragment() {
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
dialog?.window?.setDimAmount(0.0f)
binding.formbricksWebview.setBackgroundColor(Color.TRANSPARENT)
binding.formbricksWebview.let {
@@ -150,6 +160,7 @@ class FormbricksFragment : BottomSheetDialogFragment() {
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
consoleMessage?.let { cm ->
if (cm.messageLevel() == ConsoleMessage.MessageLevel.ERROR) {
Formbricks.callback?.onError(SDKError.surveyDisplayFetchError)
dismiss()
}
val log = "[CONSOLE:${cm.messageLevel()}] \"${cm.message()}\", source: ${cm.sourceId()} (${cm.lineNumber()})"
@@ -166,6 +177,22 @@ class FormbricksFragment : BottomSheetDialogFragment() {
useWideViewPort = true
}
it.webViewClient = object : WebViewClient() {
override fun onReceivedError(
view: WebView?,
request: WebResourceRequest?,
error: WebResourceError?
) {
super.onReceivedError(view, request, error)
Logger.d("WebView Error: ${error?.description}")
}
override fun onPageCommitVisible(view: WebView?, url: String?) {
dialog?.window?.setDimAmount(0.5f)
super.onPageCommitVisible(view, url)
}
}
it.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
dialog?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
@@ -220,5 +247,7 @@ class FormbricksFragment : BottomSheetDialogFragment() {
fragment.surveyId = surveyId
fragment.show(childFragmentManager, TAG)
}
private const val CLOSING_TIMEOUT_IN_SECONDS = 5L
}
}
}
@@ -123,11 +123,20 @@ class FormbricksViewModel : ViewModel() {
environmentDataHolder.getSurveyJson(surveyId).let { jsonObject.add("survey", it) }
jsonObject.addProperty("isBrandingEnabled", true)
jsonObject.addProperty("appUrl", Formbricks.appUrl)
jsonObject.addProperty("languageCode", Formbricks.language)
jsonObject.addProperty("environmentId", Formbricks.environmentId)
jsonObject.addProperty("contactId", UserManager.contactId)
jsonObject.addProperty("isWebEnvironment", false)
val isMultiLangSurvey =
(environmentDataHolder.data?.data?.surveys?.first { it.id == surveyId }?.languages?.size
?: 0) > 1
if (isMultiLangSurvey) {
jsonObject.addProperty("languageCode", Formbricks.language)
} else {
jsonObject.addProperty("languageCode", "default")
}
val hasCustomStyling = environmentDataHolder.data?.data?.surveys?.first { it.id == surveyId }?.styling != null
val enabled = environmentDataHolder.data?.data?.project?.styling?.allowStyleOverwrite ?: false
if (hasCustomStyling && enabled) {
@@ -1,11 +1,13 @@
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
import com.formbricks.formbrickssdk.model.javascript.FileUploadData
import com.google.gson.JsonParseException
import java.lang.RuntimeException
class WebAppInterface(private val callback: WebAppCallback?) {
@@ -34,13 +36,16 @@ class WebAppInterface(private val callback: WebAppCallback?) {
EventType.ON_SURVEY_LIBRARY_LOAD_ERROR -> { callback?.onSurveyLibraryLoadError() }
}
} catch (e: Exception) {
Logger.e(e.message)
Formbricks.callback?.onError(e)
Logger.e(RuntimeException(e.message))
} catch (e: JsonParseException) {
Logger.e("Failed to parse JSON message: $data")
Logger.e(RuntimeException("Failed to parse JSON message: $data"))
} catch (e: IllegalArgumentException) {
Logger.e("Invalid message format: $data")
Formbricks.callback?.onError(e)
Logger.e(RuntimeException("Invalid message format: $data"))
} catch (e: Exception) {
Logger.e("Unexpected error processing message: $data")
Formbricks.callback?.onError(e)
Logger.e(RuntimeException("Unexpected error processing message: $data"))
}
}