How to handle Redirect URLs in Android Jetpack Compose Webview

187 Views Asked by At

I am creating an Android application with Kotlin and Jetpack Compose, I have a composable screen which shows a Webview.

Screen Instructions

  • Webview loads a Mollie Payment URL Which Shows Payment Option from different banks Each option have a deep link which redirects to bank app if the app is installed The payment process from bank app returns a Redirect URL

What is required?

  • The Redirect URL from deep link should be opened in my App's webview instead to opening in Web browser (Google Chrome in this case)

This is the pattern of Redirect Url

https://www.mollie.com/checkout/ideal/return/SOMETOKEN?trxid=SOMEVALUE=SOMEVALUE

Below is the code for AndroidManifest.xml

<activity
        android:name=".presentation.feature.dashboard.MainActivity"
        android:exported="true"
        android:launchMode="singleTask"
        android:windowSoftInputMode="adjustResize">
        <intent-filter>
             <action android:name="android.intent.action.MAIN" />

             <category android:name="android.intent.category.LAUNCHER" />
         </intent-filter>
        
        <intent-filter>
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.intent.category.DEFAULT" />
            <category android:name="android.intent.category.BROWSABLE" />
            <data
                android:host="ideal"
                android:scheme="intent" />
            <data
                android:host="x-bunq-app"
                android:scheme="bunq" />
            <data
                android:host="payment"
                android:scheme="nl-asnbank-ideal" />
            <data
                android:host="ideal.ing.nl"
                android:scheme="intent" />
            <data
                android:host="authenticate"
                android:scheme="intent" />
            <data
                android:host="ideal2.knab.nl"
                android:scheme="knabapp" />
            <data
                android:host="payment"
                android:scheme="nl-regiobank-ideal" />
            <data
                android:host="payment"
                android:scheme="nl-snsbank-ideal" />

        </intent-filter>


    </activity>

Composable Screen Code

val USER_AGENT_MOZILLA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:101.0) AppleWebKit/605.1.15 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/605.1.15"
@RequiresApi(Build.VERSION_CODES.O)
@SuppressLint("SetJavaScriptEnabled")
@Composable
fun BookingPaymentScreen(
    navController: NavController,
    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 ->
        }
    )
    BackHandler {
        goBackBookingPaymentScreen(addBookingViewModel, navController)
    }
    //First time
    if (result == true) {
        if (!addBookingViewModel.retryCallMade) {
            addBookingViewModel.onEvent(AddBookingUiEvent.onRetryPayment)
            addBookingViewModel.retryCallMade = true
        }
    }
    Surface(
        modifier = Modifier
            .background(
                color = PallaAppTheme.colors.primary
            )
            .fillMaxHeight()
            .statusBarsPadding()
    ) {
        if (errorToastState.value) {
            Toast.makeText(context, "App not installed", 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.userAgentString = USER_AGENT_MOZILLA
                        settings.domStorageEnabled = true
                        settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
                        settings.allowContentAccess = true
                        settings.loadWithOverviewMode = 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)
                            }

                            @Deprecated("Deprecated in Java")
                            override fun shouldOverrideUrlLoading(
                                view: WebView?,
                                url: String?
                            ): 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 (isDeepLink(url)
                                ) {
                                    try {
                                        val intent = Intent(ACTION_VIEW, Uri.parse(url))
                                        context.startActivity(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
                                        errorToastState.value = true
                                    }
                                    return true
                                }
                                // return super.shouldOverrideUrlLoading(view, url)
                                return false
                            }
                        }
                        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
}

Any help what i am missing here, which is not allowing my app to receive redirect url instead the redirect url is opened in User's web browser.

1

There are 1 best solutions below

0
Chirag Thummar On

You have not setup the Deeplink Properly in your app and website as well.

If you don't want your link to open in external web browser you need to put https://yourdomain/.well-known/assetlinks.json with the content below.

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "your_com.example",
    "sha256_cert_fingerprints":
    ["YOUR_CERTIFICATE"]
  }
}]

Note : You can create above file from this URL : https://developers.google.com/digital-asset-links/tools/generator

You can generate your app's SHA256 fingerprint with this command:

keytool -list -v -keystore <keystore path> -alias <key alias> -storepass <store password> -keypass <key password>