mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-24 19:48:23 -05:00
fix: android sdk callbacks, tweaks and fixes (#5487)
This commit is contained in:
@@ -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
@@ -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 { *; }
|
||||
+57
-10
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+30
-11
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+77
-7
@@ -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
|
||||
}
|
||||
}
|
||||
+28
-1
@@ -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() {
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package com.formbricks.formbrickssdk.model.enums
|
||||
|
||||
enum class SuccessType {
|
||||
SET_USER_SUCCESS,
|
||||
GET_ENVIRONMENT_SUCCESS,
|
||||
LOGOUT_SUCCESS
|
||||
}
|
||||
+16
@@ -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>?
|
||||
)
|
||||
+21
-1
@@ -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")
|
||||
|
||||
}
|
||||
|
||||
+2
-1
@@ -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?
|
||||
)
|
||||
|
||||
-2
@@ -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"
|
||||
}
|
||||
|
||||
}
|
||||
+19
-7
@@ -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 {
|
||||
|
||||
+33
-4
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-1
@@ -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) {
|
||||
|
||||
+9
-4
@@ -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"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user