Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions android/src/main/java/com/formbricks/android/Formbricks.kt
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ object Formbricks {
isInitialized = true
}

fun cancelCallApi(){
FormbricksApi.cancelCallApi()
}

/**
* Sets the user id for the current user with the given [String].
* The SDK must be initialized before calling this method.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import com.formbricks.android.model.environment.EnvironmentDataHolder
import com.formbricks.android.model.user.PostUserBody
import com.formbricks.android.model.user.UserResponse
import com.formbricks.android.network.FormbricksApiService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

object FormbricksApi {
Expand All @@ -33,6 +35,12 @@ object FormbricksApi {
)
}

fun cancelCallApi(){
CoroutineScope(Dispatchers.IO).launch {
service.cancelCallApi()
}
}

suspend fun getEnvironmentState(): Result<EnvironmentDataHolder> = withContext(Dispatchers.IO) {
retryApiCall {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,7 @@ object SDKError {
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")

val couldNotCreateDisplayError = RuntimeException("Something went wrong while creating a display. Please try again later")
val couldNotCreateResponseError = RuntimeException("Something went wrong while creating a response. Please try again later")
val somethingWentWrongError = RuntimeException("Something went wrong. Please try again later")
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import retrofit2.Retrofit
open class FormbricksApiService {

private lateinit var retrofit: Retrofit
private val callProvider = mutableListOf<Call<*>>()

fun initialize(appUrl: String, isLoggingEnabled: Boolean) {
retrofit = FormbricksRetrofitBuilder(appUrl, isLoggingEnabled)
Expand Down Expand Up @@ -48,7 +49,9 @@ open class FormbricksApiService {
}

private inline fun <T> execute(apiCall: () -> Call<T>): Result<T> {
val call = apiCall().execute()
val callInstance = apiCall()
callProvider.add(callInstance)
val call = callInstance.execute()
return if (call.isSuccessful) {
val body = call.body()
if (body == null) {
Expand All @@ -67,4 +70,9 @@ open class FormbricksApiService {
}
}
}

fun cancelCallApi() {
callProvider.map { it.cancel() }
callProvider.clear()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.animation.AccelerateInterpolator
import android.webkit.ConsoleMessage
import android.webkit.WebChromeClient
import android.webkit.WebResourceError
Expand All @@ -39,28 +40,38 @@ import com.google.gson.JsonObject
import java.io.ByteArrayOutputStream
import java.io.InputStream


class FormbricksFragment(val hiddenFields: Map<String, Any>? = null) : BottomSheetDialogFragment() {

private lateinit var binding: FragmentFormbricksBinding
private lateinit var surveyId: String
private val viewModel: FormbricksViewModel by viewModels()
private var isDismissing = false

private var webAppInterface = WebAppInterface(object : WebAppInterface.WebAppCallback {
override fun onClose() {
Handler(Looper.getMainLooper()).post {
Formbricks.callback?.onSurveyClosed()
dismissAllowingStateLoss()
safeDismiss()
}
}

override fun onDisplayCreated() {
Formbricks.callback?.onSurveyStarted()
SurveyManager.onNewDisplay(surveyId)
try {
Formbricks.callback?.onSurveyStarted()
SurveyManager.onNewDisplay(surveyId)
} catch (e: Exception) {
val error = SDKError.couldNotCreateDisplayError
Logger.e(error)
}
}

override fun onResponseCreated() {
SurveyManager.postResponse(surveyId)
try {
SurveyManager.postResponse(surveyId)
} catch (e: Exception) {
val error = SDKError.couldNotCreateResponseError
Logger.e(error)
}
}

override fun onFilePick(data: FileUploadData) {
Expand All @@ -76,7 +87,7 @@ class FormbricksFragment(val hiddenFields: Map<String, Any>? = null) : BottomShe
val error = SDKError.unableToLoadFormbicksJs
Formbricks.callback?.onError(error)
Logger.e(error)
dismissAllowingStateLoss()
safeDismiss()
}
})

Expand Down Expand Up @@ -115,6 +126,13 @@ class FormbricksFragment(val hiddenFields: Map<String, Any>? = null) : BottomShe
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
surveyId = it.getString(ARG_SURVEY_ID) ?: throw IllegalArgumentException("Survey ID is required")
}
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentFormbricksBinding.inflate(inflater).apply {
lifecycleOwner = viewLifecycleOwner
Expand Down Expand Up @@ -158,6 +176,14 @@ class FormbricksFragment(val hiddenFields: Map<String, Any>? = null) : BottomShe
dialog?.window?.setDimAmount(0.0f)
binding.formbricksWebview.setBackgroundColor(Color.TRANSPARENT)
binding.formbricksWebview.let {
// First configure the WebView
it.settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
loadWithOverviewMode = true
useWideViewPort = true
}

it.webChromeClient = object : WebChromeClient() {
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
consoleMessage?.let { cm ->
Expand All @@ -174,13 +200,6 @@ class FormbricksFragment(val hiddenFields: Map<String, Any>? = null) : BottomShe
}
}

it.settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
loadWithOverviewMode = true
useWideViewPort = true
}

it.webViewClient = object : WebViewClient() {
override fun onReceivedError(
view: WebView?,
Expand All @@ -205,20 +224,25 @@ class FormbricksFragment(val hiddenFields: Map<String, Any>? = null) : BottomShe
}

it.setInitialScale(1)

it.addJavascriptInterface(webAppInterface, WebAppInterface.INTERFACE_NAME)
viewModel.loadHtml(surveyId, hiddenFields = hiddenFields)
}

viewModel.loadHtml(surveyId = surveyId, hiddenFields = hiddenFields)
handleBackPressIfEnable()
}

private fun handleBackPressIfEnable() {
if (Formbricks.isBackPressEnable) {
dialog?.setOnKeyListener { _, keyCode, event ->
if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
dismissAllowingStateLoss()
Formbricks.callback?.onSurveyDismissByBack()
binding.formbricksWebview.animate()
.translationY(binding.formbricksWebview.height.toFloat())
.alpha(ALPHA_TRANSPARENT).setDuration(ANIMATION_TRANSPARENT_MS)
.setInterpolator(
AccelerateInterpolator()
).withEndAction {
dismissAllowingStateLoss()
Formbricks.callback?.onSurveyDismissByBack()
}.start()
true
} else {
false
Expand Down Expand Up @@ -259,6 +283,29 @@ class FormbricksFragment(val hiddenFields: Map<String, Any>? = null) : BottomShe
}
}

private fun safeDismiss() {
if (isDismissing) return
isDismissing = true

try {
if (isAdded && !isStateSaved) {
dismiss()
} else {
// If we can't dismiss safely, just finish the activity
activity?.finish()
}
} catch (e: Exception) {
val error = SDKError.somethingWentWrongError
Logger.e(error)
activity?.finish()
}
}

override fun onDestroy() {
super.onDestroy()
isDismissing = false
}

override fun onDestroyView() {
super.onDestroyView()
binding.formbricksWebview.removeJavascriptInterface(WebAppInterface.INTERFACE_NAME)
Expand All @@ -269,17 +316,21 @@ class FormbricksFragment(val hiddenFields: Map<String, Any>? = null) : BottomShe

companion object {
private val TAG: String by lazy { FormbricksFragment::class.java.simpleName }
private const val ARG_SURVEY_ID = "survey_id"
private const val ALPHA_TRANSPARENT = 0f
private const val ANIMATION_TRANSPARENT_MS = 250L

fun show(
childFragmentManager: FragmentManager,
surveyId: String,
hiddenFields: Map<String, Any>? = null
) {
val fragment = FormbricksFragment(hiddenFields)
fragment.surveyId = surveyId
val fragment = FormbricksFragment(hiddenFields).apply {
arguments = Bundle().apply {
putString(ARG_SURVEY_ID, surveyId)
}
}
fragment.show(childFragmentManager, TAG)
}

private const val CLOSING_TIMEOUT_IN_SECONDS = 5L
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ class FormbricksViewModel : ViewModel() {
</head>

<body style="overflow: hidden; height: 100vh; display: flex; flex-direction: column; justify-content: flex-end;">
<div id="formbricks-react-native" style="width: 100%;"></div>
<div id="formbricks-android" style="width: 100%;"></div>
</body>

<script type="text/javascript">
var json = `{{WEBVIEW_DATA}}`
const json = `{{WEBVIEW_DATA}}`;

function onClose() {
FormbricksJavascript.message(JSON.stringify({ event: "onClose" }));
Expand All @@ -52,20 +52,25 @@ class FormbricksViewModel : ViewModel() {
function onResponseCreated() {
FormbricksJavascript.message(JSON.stringify({ event: "onResponseCreated" }));
};

let setResponseFinished = null;
function getSetIsResponseSendingFinished(callback) {
setResponseFinished = callback;
}

function loadSurvey() {
const options = JSON.parse(json);
const surveyProps = {
...options,
getSetIsResponseSendingFinished,
onDisplayCreated,
onResponseCreated,
onClose,
};

window.formbricksSurveys.renderSurvey(surveyProps);
}
};

// Function to attach click listener to file inputs
function attachFilePickerOverride() {
const inputs = document.querySelectorAll('input[type="file"]');
inputs.forEach(input => {
Expand All @@ -82,23 +87,20 @@ class FormbricksViewModel : ViewModel() {
fileUploadParams: {
allowedFileExtensions: allowedFileExtensions,
allowMultipleFiles: allowMultipleFiles === "true",
},
}
}));
});
}
});
}
};

// Initially attach the override
attachFilePickerOverride();

// Set up a MutationObserver to catch dynamically added file inputs
const observer = new MutationObserver(function (mutations) {
attachFilePickerOverride();
});

observer.observe(document.body, { childList: true, subtree: true });

const script = document.createElement("script");
script.src = "${Formbricks.appUrl}/js/surveys.umd.cjs";
script.async = true;
Expand Down Expand Up @@ -159,8 +161,6 @@ class FormbricksViewModel : ViewModel() {
.replace("#", "%23") // Hex color code's # breaks the JSON
.replace("\\\"","'") // " is replaced to ' in the html codes in the JSON
}


}

@BindingAdapter("htmlText")
Expand Down
Loading