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()
}
}
}