In my Android App built using Jetpack Compose and Kotlin, I am using Android Webview to load Mollie checkout url for payments. The URL (https://www.mollie.com/checkout/select-method/SOMECODESPECIFICTOPAYMENT) shows content like this
Initially everything was working and Android Webview was loading Mollie URLs properly, but now the users on live app are facing issue of empty page display, So the Webvie loads the URL but the content is not displayed.
Below is the BookingPaymentScreen which loads the URL
@RequiresApi(Build.VERSION_CODES.O)
@SuppressLint("SetJavaScriptEnabled")
@Composable
fun BookingPaymentScreen(
navController: NavController,
sharedViewModel: SharedViewModel,
addBookingViewModel: AddBookingViewModel,
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val result = navController.previousBackStackEntry?.savedStateHandle?.get<Boolean>("isFromRetry")
val resultUrl = navController.previousBackStackEntry?.savedStateHandle?.get<String>("url")
val errorToastState = remember { mutableStateOf(false) }
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult(),
onResult = { activityResult ->
addBookingViewModel.retryCallMade = false
val bookingDetail = BookingDetail(
bookingId = addBookingViewModel.bookingFinalUiState.bookingId,
ownerToken = addBookingViewModel.bookingFinalUiState.ownerToken,
url = addBookingViewModel.bookingFinalUiState.paymentUrl
)
navController.currentBackStackEntry?.savedStateHandle?.set(
key = "bookingDetail", value = bookingDetail
)
navController.navigate(BOOKING_DETAIL_ROUTE)
})
BackHandler {
goBackBookingPaymentScreen(addBookingViewModel, navController)
}
// Ensure that retry logic is only executed once and after the Composable is composed
LaunchedEffect(result) {
if (result == true && !addBookingViewModel.retryCallMade) {
addBookingViewModel.onEvent(AddBookingUiEvent.onRetryPayment)
addBookingViewModel.retryCallMade = true
sharedViewModel.logAnalyticsEvent(AnalyticsEvents.mlBookingsRetryPaymentBtnClicked)
}
}
Surface(
modifier = Modifier
.background(
color = PallaAppTheme.colors.primary
)
.fillMaxHeight()
.statusBarsPadding()
) {
if (errorToastState.value) {
Toast.makeText(context, "App not installed or setup", Toast.LENGTH_LONG).show()
errorToastState.value = false
}
if (addBookingViewModel.checkoutLoading) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(AppVariables.AppPading)
.background(PallaAppTheme.AppBackground), contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
color = PallaAppTheme.colors.secondary,
strokeWidth = 2.dp
)
}
} else {
AndroidView(modifier = Modifier
.fillMaxSize()
.statusBarsPadding()
.navigationBarsPadding(),
factory = { context ->
WebView(context).apply {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
webViewClient = object : WebViewClient() {
override fun onPageCommitVisible(view: WebView?, url: String?) {
super.onPageCommitVisible(view, url)
}
override fun onPageStarted(
view: WebView?, url: String?, favicon: Bitmap?
) {
super.onPageStarted(view, url, favicon)
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
}
override fun shouldOverrideUrlLoading(
view: WebView?, request: WebResourceRequest?
): Boolean {
Log.d("shouldOverrideUrlLoading", request?.url.toString())
return handleOverrideUrlLoading(webView = view,
url = request?.url.toString(),
addBookingViewModel,
navController,
context,
launcher,
onBankingAppNotFound = {
errorToastState.value = true
})
}
@Deprecated("Deprecated in Java")
override fun shouldOverrideUrlLoading(
view: WebView?, url: String?
): Boolean {
Log.d("shouldOverrideUrlLoading", "$url")
return handleOverrideUrlLoading(webView = view,
url = url,
addBookingViewModel,
navController,
context,
launcher,
onBankingAppNotFound = {
errorToastState.value = true
})
}
override fun onReceivedError(
view: WebView?,
request: WebResourceRequest?,
error: WebResourceError?
) {
Log.d("onReceivedError", error.toString())
val error1 = error
val abc = "fddg"
super.onReceivedError(view, request, error)
}
override fun onReceivedSslError(
view: WebView?,
handler: SslErrorHandler?,
error: SslError?
) {
Log.d("onReceivedSslError", error.toString())
// ignore ssl error
if (handler != null) {
handler.proceed()
} else {
super.onReceivedSslError(view, null, error)
}
}
}
// WebChromeClient for JavaScript console logs
webChromeClient = object : WebChromeClient() {
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
Log.d("WebView Console", "${consoleMessage.message()} -- from line " +
"${consoleMessage.lineNumber()} of ${consoleMessage.sourceId()}")
return super.onConsoleMessage(consoleMessage)
}
}
var url = addBookingViewModel.bookingFinalUiState.paymentUrl
if (url.isEmpty()) {
url = resultUrl ?: ""
}
loadUrl(url)
}
},
update = { webView ->
// Update the WebView if needed
//CookieManager.getInstance()?.setAcceptThirdPartyCookies(webView, true)
})
}
}
}
fun goBackBookingPaymentScreen(
addBookingViewModel: AddBookingViewModel, navController: NavController
) {
addBookingViewModel.retryCallMade = false
val bookingDetail = BookingDetail(
bookingId = addBookingViewModel.bookingFinalUiState.bookingId,
ownerToken = addBookingViewModel.bookingFinalUiState.ownerToken
)
navController.currentBackStackEntry?.savedStateHandle?.set(
key = "bookingDetail", value = bookingDetail
)
navController.navigate(BOOKING_DETAIL_ROUTE)
}
fun isDeepLink(url: String?): Boolean {
if ((url?.startsWith("nl") == true && url.contains("payloadUri"))) {
return true
}
if (url?.startsWith("http") == false && !url.startsWith("https") && url.contains("://")) {
return true
}
return false
}
//OLD FUNCTION WITH DEEP LINK
private fun handleOverrideUrlLoading(
webView: WebView?,
url: String?,
addBookingViewModel: AddBookingViewModel,
navController: NavController,
context: Context,
launcher: ManagedActivityResultLauncher<Intent, ActivityResult>,
onBankingAppNotFound: () -> Unit
): Boolean {
if (url != null && url.startsWith(AppDeepLinks.COURT_BOOKING)) {
addBookingViewModel.retryCallMade = false
val bookingDetail = BookingDetail(
bookingId = addBookingViewModel.bookingFinalUiState.bookingId,
ownerToken = addBookingViewModel.bookingFinalUiState.ownerToken,
url = addBookingViewModel.bookingFinalUiState.paymentUrl
)
navController.currentBackStackEntry?.savedStateHandle?.set(
key = "bookingDetail", value = bookingDetail
)
navController.navigate(BOOKING_DETAIL_ROUTE)
return true
}
if (isIntentLink(url)) {
handleBankingAppIntent(url ?: "", context, launcher, onBankingAppNotFound = {
onBankingAppNotFound()
})
return true
}
if (isDeepLink(url)) {
try {
val intent = Intent(ACTION_VIEW, Uri.parse(url))
//context.startActivity(intent)
launcher.launch(intent)
} catch (e: ActivityNotFoundException) {
// Only browser apps are available, or a browser is the default.
// So you can open the URL directly in your app, for example in a
e.printStackTrace()
onBankingAppNotFound()
}
return true
}
url?.let {
webView?.loadUrl(it)
return true
}
return true
}
fun isIntentLink(url: String?): Boolean {
if ((url?.startsWith("intent://") == true)) {
return true
}
return false
}
/*// In some cases Mollie creates wrong Android Chrome Intent (lower cased)
// https://developer.chrome.com/multidevice/android/intents
private fun String.fixedIntentName(): String = replace("#intent;", "#Intent;")*/
private fun handleBankingAppIntent(
url: String, context: Context,
launcher: ManagedActivityResultLauncher<Intent, ActivityResult>,
onBankingAppNotFound: () -> Unit
) {
try {
val intent = Intent.parseUri(
url, Intent.URI_INTENT_SCHEME
)
try {
// context.startActivity(intent)
launcher.launch(intent)
} catch (e: ActivityNotFoundException) {
// Only browser apps are available, or a browser is the default.
// So you can open the URL directly in your app, for example in a
e.printStackTrace()
onBankingAppNotFound()
}
} catch (e: URISyntaxException) {
e.printStackTrace()
onBankingAppNotFound()
}
}
I tried Webview settings like initial scale, zoom, scroll, static height/width and different user Agent Strings but nothing seems to be working. The same Mollie payment URLs works on the iOS App.
settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
CookieManager.getInstance().setAcceptThirdPartyCookies(this, true)
settings.userAgentString="Mozilla/5.0 (Linux; Android 6.0.1; SM-J500M Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36"
Any help with this will be appreciated.
You need to sign in to view this answers
Leave feedback about this