HandleUriScreen.kt

package net.taler.wallet

import android.net.Uri
import android.util.Log
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.core.net.toUri
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.compose.GlobalScaffold
import net.taler.wallet.compose.LoadingScreen
import net.taler.wallet.compose.RetryScreen
import net.taler.wallet.main.MainViewModel
import net.taler.wallet.main.TAG
import net.taler.wallet.payment.PayStatus
import net.taler.wallet.refund.RefundStatus
import net.taler.wallet.transactions.TransactionMajorState
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL
import java.util.Locale

@Composable
fun HandleUriScreen(
    model: MainViewModel,
    uriString: String,
    onNavigate: NavigateCallback,
    onNavigateBack: () -> Unit,
    onShowError: (error: TalerErrorInfo) -> Unit,
) {
    var processing by remember { mutableStateOf(false) }
    var errorInfo by remember { mutableStateOf<TalerErrorInfo?>(null) }
    val networkStatus by model.networkManager.networkStatus.observeAsState()
    val payStatus by model.paymentManager.payStatus.observeAsState(PayStatus.None)
    val scope = rememberCoroutineScope()

    fun processTalerUri() {
        if (processing) return
        processing = true
        scope.launch {
            try {
                val u = getTalerAction(model, uriString.trim().toUri(), 3)
                Log.v(TAG, "found action $u")

                if (u.startsWith("payto://", ignoreCase = true)) {
                    onNavigate(WalletDestination.PaytoUri(u), true)
                    return@launch
                }

                val normalizedURL = u.lowercase(Locale.ROOT)
                var ext = false
                val action = normalizedURL.substring(
                    if (normalizedURL.startsWith("taler://", ignoreCase = true)) {
                        "taler://".length
                    } else if (normalizedURL.startsWith("ext+taler://", ignoreCase = true)) {
                        ext = true
                        "ext+taler://".length
                    } else if (normalizedURL.startsWith("taler+http://", ignoreCase = true) &&
                        model.devMode.value == true
                    ) {
                        "taler+http://".length
                    } else {
                        normalizedURL.length
                    }
                )

                val u2 = if (ext) {
                    "taler://" + u.substring("ext+taler://".length)
                } else u

                when {
                    action.startsWith("pay/", ignoreCase = true) -> {
                        model.paymentManager.preparePay(u2)?.let { transactionId ->
                            model.paymentManager.waitForTransaction(
                                transactionId,
                                TransactionMajorState.Dialog,
                            )
                            if (model.transactionManager.selectTransaction(transactionId)) {
                                onNavigate(WalletDestination.TransactionPayment(promptMode = true), true)
                            }
                        }
                    }
                    action.startsWith("withdraw/", ignoreCase = true) -> {
                        model.withdrawManager.resetWithdrawal()
                        onNavigate(WalletDestination.PromptWithdraw(
                            withdrawUri = u2,
                            editableCurrency = false
                        ), true)
                    }
                    action.startsWith("withdraw-exchange/", ignoreCase = true) -> {
                        model.withdrawManager.resetWithdrawal()
                        onNavigate(WalletDestination.PromptWithdraw(
                            withdrawExchangeUri = u2,
                            editableCurrency = false
                        ), true)
                    }
                    action.startsWith("refund/", ignoreCase = true) -> {
                        model.showProgressBar.value = true
                        model.refundManager.refund(u2).observeForever { status ->
                            model.showProgressBar.value = false
                            when (status) {
                                is RefundStatus.Error -> {
                                    errorInfo = status.error
                                }
                                is RefundStatus.Success -> {
                                    onNavigateBack()
                                }
                            }
                        }
                    }
                    action.startsWith("pay-pull/", ignoreCase = true) -> {
                        model.peerManager.preparePeerPullDebit(u2)
                        onNavigate(WalletDestination.PromptPullPayment, true)
                    }
                    action.startsWith("pay-push/", ignoreCase = true) -> {
                        model.peerManager.preparePeerPushCredit(u2)
                        onNavigate(WalletDestination.PromptPushPayment, true)
                    }
                    action.startsWith("pay-template/", ignoreCase = true) -> {
                        onNavigate(WalletDestination.PromptPayTemplate(u2), true)
                    }
                    action.startsWith("dev-experiment/", ignoreCase = true) -> {
                        model.applyDevExperiment(u2,
                            onSuccess = {
                                onNavigateBack()
                            },
                            onError = { error ->
                                errorInfo = error
                            },
                        )
                    }
                    action.startsWith("add-contact/", ignoreCase = true) -> {
                        onNavigate(WalletDestination.Contacts(addContactUri = u2), true)
                    }
                    else -> {
                        errorInfo = TalerErrorInfo.makeCustomError("Unsupported URI: $u2")
                    }
                }
            } catch (_: CancellationException) {
            }
            processing = false
        }
    }

    var autoProcessed by remember { mutableStateOf(false) }

    LaunchedEffect(networkStatus) {
        if (networkStatus == true && !autoProcessed) {
            autoProcessed = true
            processTalerUri()
        }
    }

    LaunchedEffect(payStatus) {
        val error = (payStatus as? PayStatus.Error)?.error ?: return@LaunchedEffect
        errorInfo = error
    }

    LaunchedEffect(errorInfo) {
        val currentError = errorInfo
        if (currentError != null) {
            onShowError(currentError)
            onNavigateBack()
        }
    }

    GlobalScaffold(
        model = model,
        modifier = Modifier.fillMaxSize(),
        onNavigateBack = onNavigateBack,
    ) { paddingValues ->
        Box(
            Modifier
                .fillMaxSize()
                .padding(paddingValues),
        ) {
            if (networkStatus == true) {
                LoadingScreen()
            } else {
                RetryScreen {
                    processTalerUri()
                }
            }
        }
    }
}

private suspend fun getTalerAction(
    model: MainViewModel,
    uri: Uri,
    maxRedirects: Int,
): String {
    val scheme = uri.scheme ?: return uri.toString()
    if (scheme != "http" && scheme != "https") return uri.toString()

    return withContext(Dispatchers.IO) {
        try {
            val conn = URL(uri.toString()).openConnection() as HttpURLConnection
            conn.setRequestProperty("Accept", "text/html")
            conn.connectTimeout = 5000
            conn.requestMethod = "HEAD"
            conn.connect()
            when (conn.responseCode) {
                HttpURLConnection.HTTP_OK,
                HttpURLConnection.HTTP_PAYMENT_REQUIRED -> {
                    val talerHeader = conn.headerFields["Taler"]
                    if (talerHeader != null && talerHeader[0] != null) {
                        getTalerAction(model, talerHeader[0].toUri(), 0)
                    } else {
                        uri.toString()
                    }
                }
                HttpURLConnection.HTTP_MOVED_TEMP,
                HttpURLConnection.HTTP_MOVED_PERM,
                HttpURLConnection.HTTP_SEE_OTHER -> {
                    val location = conn.headerFields["Location"]
                    if (location != null && location[0] != null) {
                        getTalerAction(model, location[0].toUri(), maxRedirects - 1)
                    } else {
                        uri.toString()
                    }
                }
                else -> uri.toString()
            }
        } catch (_: IOException) {
            uri.toString()
        }
    }
}