Skip to content

Commit f67bc53

Browse files
longvantruongpandeymanggmattinanntgithub-advanced-security[bot]
authored
add cancel support for ongoing retrofit calls (And Merge Main into Mobile-SDK-Custom) (#26)
* chore: custom sdk tcb * chore: custom sdk tcb * chore: custom sdk tcb * chore: custom sdk tcb * chore: custom sdk tcb * fix: blackduck issues (#22) * fixes blackduck issues * fix: dokka version * fix: Workflow does not contain permissions (#20) Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * chore: handle back press survey * chore: handle back press survey * fix: fixes the pixel fold issue (#24) * fixes the pixel fold issue * adds errors * fixes log * add hidden field * handle back press survey * fixes responses stuck error (#25) * feat(api): add cancel support for ongoing retrofit calls --------- Co-authored-by: Anshuman Pandey <54475686+pandeymangg@users.noreply.github.com> Co-authored-by: Matti Nannt <mail@matti.sh> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
1 parent cd3596d commit f67bc53

6 files changed

Lines changed: 108 additions & 34 deletions

File tree

android/src/main/java/com/formbricks/android/Formbricks.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ object Formbricks {
8888
isInitialized = true
8989
}
9090

91+
fun cancelCallApi(){
92+
FormbricksApi.cancelCallApi()
93+
}
94+
9195
/**
9296
* Sets the user id for the current user with the given [String].
9397
* The SDK must be initialized before calling this method.

android/src/main/java/com/formbricks/android/api/FormbricksApi.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import com.formbricks.android.model.environment.EnvironmentDataHolder
55
import com.formbricks.android.model.user.PostUserBody
66
import com.formbricks.android.model.user.UserResponse
77
import com.formbricks.android.network.FormbricksApiService
8+
import kotlinx.coroutines.CoroutineScope
89
import kotlinx.coroutines.Dispatchers
910
import kotlinx.coroutines.delay
11+
import kotlinx.coroutines.launch
1012
import kotlinx.coroutines.withContext
1113

1214
object FormbricksApi {
@@ -33,6 +35,12 @@ object FormbricksApi {
3335
)
3436
}
3537

38+
fun cancelCallApi(){
39+
CoroutineScope(Dispatchers.IO).launch {
40+
service.cancelCallApi()
41+
}
42+
}
43+
3644
suspend fun getEnvironmentState(): Result<EnvironmentDataHolder> = withContext(Dispatchers.IO) {
3745
retryApiCall {
3846
try {

android/src/main/java/com/formbricks/android/model/error/SDKError.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,7 @@ object SDKError {
2424
val surveyNotFoundError = RuntimeException("No survey found matching the action class.")
2525
val noUserIdSetError = RuntimeException("No userId is set, please set a userId first using the setUserId function")
2626

27+
val couldNotCreateDisplayError = RuntimeException("Something went wrong while creating a display. Please try again later")
28+
val couldNotCreateResponseError = RuntimeException("Something went wrong while creating a response. Please try again later")
29+
val somethingWentWrongError = RuntimeException("Something went wrong. Please try again later")
2730
}

android/src/main/java/com/formbricks/android/network/FormbricksApiService.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import retrofit2.Retrofit
1616
open class FormbricksApiService {
1717

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

2021
fun initialize(appUrl: String, isLoggingEnabled: Boolean) {
2122
retrofit = FormbricksRetrofitBuilder(appUrl, isLoggingEnabled)
@@ -48,7 +49,9 @@ open class FormbricksApiService {
4849
}
4950

5051
private inline fun <T> execute(apiCall: () -> Call<T>): Result<T> {
51-
val call = apiCall().execute()
52+
val callInstance = apiCall()
53+
callProvider.add(callInstance)
54+
val call = callInstance.execute()
5255
return if (call.isSuccessful) {
5356
val body = call.body()
5457
if (body == null) {
@@ -67,4 +70,9 @@ open class FormbricksApiService {
6770
}
6871
}
6972
}
73+
74+
fun cancelCallApi() {
75+
callProvider.map { it.cancel() }
76+
callProvider.clear()
77+
}
7078
}

android/src/main/java/com/formbricks/android/webview/FormbricksFragment.kt

Lines changed: 73 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import android.view.LayoutInflater
1616
import android.view.View
1717
import android.view.ViewGroup
1818
import android.view.WindowManager
19+
import android.view.animation.AccelerateInterpolator
1920
import android.webkit.ConsoleMessage
2021
import android.webkit.WebChromeClient
2122
import android.webkit.WebResourceError
@@ -39,28 +40,38 @@ import com.google.gson.JsonObject
3940
import java.io.ByteArrayOutputStream
4041
import java.io.InputStream
4142

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

4545
private lateinit var binding: FragmentFormbricksBinding
4646
private lateinit var surveyId: String
4747
private val viewModel: FormbricksViewModel by viewModels()
48+
private var isDismissing = false
4849

4950
private var webAppInterface = WebAppInterface(object : WebAppInterface.WebAppCallback {
5051
override fun onClose() {
5152
Handler(Looper.getMainLooper()).post {
5253
Formbricks.callback?.onSurveyClosed()
53-
dismissAllowingStateLoss()
54+
safeDismiss()
5455
}
5556
}
5657

5758
override fun onDisplayCreated() {
58-
Formbricks.callback?.onSurveyStarted()
59-
SurveyManager.onNewDisplay(surveyId)
59+
try {
60+
Formbricks.callback?.onSurveyStarted()
61+
SurveyManager.onNewDisplay(surveyId)
62+
} catch (e: Exception) {
63+
val error = SDKError.couldNotCreateDisplayError
64+
Logger.e(error)
65+
}
6066
}
6167

6268
override fun onResponseCreated() {
63-
SurveyManager.postResponse(surveyId)
69+
try {
70+
SurveyManager.postResponse(surveyId)
71+
} catch (e: Exception) {
72+
val error = SDKError.couldNotCreateResponseError
73+
Logger.e(error)
74+
}
6475
}
6576

6677
override fun onFilePick(data: FileUploadData) {
@@ -76,7 +87,7 @@ class FormbricksFragment(val hiddenFields: Map<String, Any>? = null) : BottomShe
7687
val error = SDKError.unableToLoadFormbicksJs
7788
Formbricks.callback?.onError(error)
7889
Logger.e(error)
79-
dismissAllowingStateLoss()
90+
safeDismiss()
8091
}
8192
})
8293

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

129+
override fun onCreate(savedInstanceState: Bundle?) {
130+
super.onCreate(savedInstanceState)
131+
arguments?.let {
132+
surveyId = it.getString(ARG_SURVEY_ID) ?: throw IllegalArgumentException("Survey ID is required")
133+
}
134+
}
135+
118136
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
119137
binding = FragmentFormbricksBinding.inflate(inflater).apply {
120138
lifecycleOwner = viewLifecycleOwner
@@ -158,6 +176,14 @@ class FormbricksFragment(val hiddenFields: Map<String, Any>? = null) : BottomShe
158176
dialog?.window?.setDimAmount(0.0f)
159177
binding.formbricksWebview.setBackgroundColor(Color.TRANSPARENT)
160178
binding.formbricksWebview.let {
179+
// First configure the WebView
180+
it.settings.apply {
181+
javaScriptEnabled = true
182+
domStorageEnabled = true
183+
loadWithOverviewMode = true
184+
useWideViewPort = true
185+
}
186+
161187
it.webChromeClient = object : WebChromeClient() {
162188
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
163189
consoleMessage?.let { cm ->
@@ -174,13 +200,6 @@ class FormbricksFragment(val hiddenFields: Map<String, Any>? = null) : BottomShe
174200
}
175201
}
176202

177-
it.settings.apply {
178-
javaScriptEnabled = true
179-
domStorageEnabled = true
180-
loadWithOverviewMode = true
181-
useWideViewPort = true
182-
}
183-
184203
it.webViewClient = object : WebViewClient() {
185204
override fun onReceivedError(
186205
view: WebView?,
@@ -205,20 +224,25 @@ class FormbricksFragment(val hiddenFields: Map<String, Any>? = null) : BottomShe
205224
}
206225

207226
it.setInitialScale(1)
208-
209227
it.addJavascriptInterface(webAppInterface, WebAppInterface.INTERFACE_NAME)
228+
viewModel.loadHtml(surveyId, hiddenFields = hiddenFields)
210229
}
211-
212-
viewModel.loadHtml(surveyId = surveyId, hiddenFields = hiddenFields)
213230
handleBackPressIfEnable()
214231
}
215232

216233
private fun handleBackPressIfEnable() {
217234
if (Formbricks.isBackPressEnable) {
218235
dialog?.setOnKeyListener { _, keyCode, event ->
219236
if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
220-
dismissAllowingStateLoss()
221-
Formbricks.callback?.onSurveyDismissByBack()
237+
binding.formbricksWebview.animate()
238+
.translationY(binding.formbricksWebview.height.toFloat())
239+
.alpha(ALPHA_TRANSPARENT).setDuration(ANIMATION_TRANSPARENT_MS)
240+
.setInterpolator(
241+
AccelerateInterpolator()
242+
).withEndAction {
243+
dismissAllowingStateLoss()
244+
Formbricks.callback?.onSurveyDismissByBack()
245+
}.start()
222246
true
223247
} else {
224248
false
@@ -259,6 +283,29 @@ class FormbricksFragment(val hiddenFields: Map<String, Any>? = null) : BottomShe
259283
}
260284
}
261285

286+
private fun safeDismiss() {
287+
if (isDismissing) return
288+
isDismissing = true
289+
290+
try {
291+
if (isAdded && !isStateSaved) {
292+
dismiss()
293+
} else {
294+
// If we can't dismiss safely, just finish the activity
295+
activity?.finish()
296+
}
297+
} catch (e: Exception) {
298+
val error = SDKError.somethingWentWrongError
299+
Logger.e(error)
300+
activity?.finish()
301+
}
302+
}
303+
304+
override fun onDestroy() {
305+
super.onDestroy()
306+
isDismissing = false
307+
}
308+
262309
override fun onDestroyView() {
263310
super.onDestroyView()
264311
binding.formbricksWebview.removeJavascriptInterface(WebAppInterface.INTERFACE_NAME)
@@ -269,17 +316,21 @@ class FormbricksFragment(val hiddenFields: Map<String, Any>? = null) : BottomShe
269316

270317
companion object {
271318
private val TAG: String by lazy { FormbricksFragment::class.java.simpleName }
319+
private const val ARG_SURVEY_ID = "survey_id"
320+
private const val ALPHA_TRANSPARENT = 0f
321+
private const val ANIMATION_TRANSPARENT_MS = 250L
272322

273323
fun show(
274324
childFragmentManager: FragmentManager,
275325
surveyId: String,
276326
hiddenFields: Map<String, Any>? = null
277327
) {
278-
val fragment = FormbricksFragment(hiddenFields)
279-
fragment.surveyId = surveyId
328+
val fragment = FormbricksFragment(hiddenFields).apply {
329+
arguments = Bundle().apply {
330+
putString(ARG_SURVEY_ID, surveyId)
331+
}
332+
}
280333
fragment.show(childFragmentManager, TAG)
281334
}
282-
283-
private const val CLOSING_TIMEOUT_IN_SECONDS = 5L
284335
}
285336
}

android/src/main/java/com/formbricks/android/webview/FormbricksViewModel.kt

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ class FormbricksViewModel : ViewModel() {
3535
</head>
3636
3737
<body style="overflow: hidden; height: 100vh; display: flex; flex-direction: column; justify-content: flex-end;">
38-
<div id="formbricks-react-native" style="width: 100%;"></div>
38+
<div id="formbricks-android" style="width: 100%;"></div>
3939
</body>
4040
4141
<script type="text/javascript">
42-
var json = `{{WEBVIEW_DATA}}`
42+
const json = `{{WEBVIEW_DATA}}`;
4343
4444
function onClose() {
4545
FormbricksJavascript.message(JSON.stringify({ event: "onClose" }));
@@ -52,20 +52,25 @@ class FormbricksViewModel : ViewModel() {
5252
function onResponseCreated() {
5353
FormbricksJavascript.message(JSON.stringify({ event: "onResponseCreated" }));
5454
};
55+
56+
let setResponseFinished = null;
57+
function getSetIsResponseSendingFinished(callback) {
58+
setResponseFinished = callback;
59+
}
5560
5661
function loadSurvey() {
5762
const options = JSON.parse(json);
5863
const surveyProps = {
5964
...options,
65+
getSetIsResponseSendingFinished,
6066
onDisplayCreated,
6167
onResponseCreated,
6268
onClose,
6369
};
6470
6571
window.formbricksSurveys.renderSurvey(surveyProps);
66-
}
72+
};
6773
68-
// Function to attach click listener to file inputs
6974
function attachFilePickerOverride() {
7075
const inputs = document.querySelectorAll('input[type="file"]');
7176
inputs.forEach(input => {
@@ -82,23 +87,20 @@ class FormbricksViewModel : ViewModel() {
8287
fileUploadParams: {
8388
allowedFileExtensions: allowedFileExtensions,
8489
allowMultipleFiles: allowMultipleFiles === "true",
85-
},
90+
}
8691
}));
8792
});
8893
}
8994
});
90-
}
95+
};
9196
92-
// Initially attach the override
9397
attachFilePickerOverride();
9498
95-
// Set up a MutationObserver to catch dynamically added file inputs
9699
const observer = new MutationObserver(function (mutations) {
97100
attachFilePickerOverride();
98101
});
99102
100103
observer.observe(document.body, { childList: true, subtree: true });
101-
102104
const script = document.createElement("script");
103105
script.src = "${Formbricks.appUrl}/js/surveys.umd.cjs";
104106
script.async = true;
@@ -159,8 +161,6 @@ class FormbricksViewModel : ViewModel() {
159161
.replace("#", "%23") // Hex color code's # breaks the JSON
160162
.replace("\\\"","'") // " is replaced to ' in the html codes in the JSON
161163
}
162-
163-
164164
}
165165

166166
@BindingAdapter("htmlText")

0 commit comments

Comments
 (0)