diff --git a/wallet/src/main/java/net/taler/wallet/WalletNavigation.kt b/wallet/src/main/java/net/taler/wallet/WalletNavigation.kt index 3d4c2eaa..bf1df1fb 100644 --- a/wallet/src/main/java/net/taler/wallet/WalletNavigation.kt +++ b/wallet/src/main/java/net/taler/wallet/WalletNavigation.kt @@ -80,11 +80,8 @@ sealed interface WalletDestination { @Serializable data class Deposit( - val amount: String? = null, - val receiverName: String? = null, - val receiverPostalCode: String? = null, - val receiverTown: String? = null, - val IBAN: String? = null + /** Destination given by a payto:// link; the user still picks the amount and confirms. */ + val paytoUri: String? = null, ) : WalletDestination @Serializable diff --git a/wallet/src/main/java/net/taler/wallet/deposit/DepositManager.kt b/wallet/src/main/java/net/taler/wallet/deposit/DepositManager.kt index 9bbf9d22..aa7ff2ce 100644 --- a/wallet/src/main/java/net/taler/wallet/deposit/DepositManager.kt +++ b/wallet/src/main/java/net/taler/wallet/deposit/DepositManager.kt @@ -30,6 +30,7 @@ import net.taler.common.Amount import net.taler.common.CurrencySpecification import net.taler.wallet.main.TAG import net.taler.wallet.accounts.KnownBankAccountInfo +import net.taler.wallet.accounts.PaytoUri import net.taler.wallet.accounts.PaytoUriBitcoin import net.taler.wallet.accounts.PaytoUriIban import net.taler.wallet.accounts.PaytoUriTalerBank @@ -42,7 +43,6 @@ import net.taler.wallet.balances.BalanceManager import net.taler.wallet.balances.ScopeInfo import net.taler.wallet.exchanges.ExchangeManager import org.json.JSONObject -import androidx.core.net.toUri class DepositManager( private val api: WalletBackendApi, @@ -58,12 +58,8 @@ class DepositManager( exchangeManager.getCachedSpecForCurrency(currency) ?: exchangeManager.getSpecForScopeInfo(ScopeInfo.Global(currency)) - fun isSupportedPayToUri(uriString: String): Boolean { - if (!uriString.startsWith("payto://")) return false - val u = uriString.toUri() - if (!u.authority.equals("iban", ignoreCase = true)) return false - return u.pathSegments.isNotEmpty() - } + fun isSupportedPayToUri(uriString: String): Boolean = + PaytoUri.parse(uriString) != null @UiThread fun selectAccount(account: KnownBankAccountInfo) = scope.launch { diff --git a/wallet/src/main/java/net/taler/wallet/deposit/DepositScreen.kt b/wallet/src/main/java/net/taler/wallet/deposit/DepositScreen.kt index 605298a5..63f78718 100644 --- a/wallet/src/main/java/net/taler/wallet/deposit/DepositScreen.kt +++ b/wallet/src/main/java/net/taler/wallet/deposit/DepositScreen.kt @@ -28,11 +28,12 @@ import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.lifecycle.map -import net.taler.common.Amount import net.taler.wallet.NavigateCallback import net.taler.wallet.R import net.taler.wallet.WalletDestination +import net.taler.wallet.accounts.KnownBankAccountInfo import net.taler.wallet.accounts.ListBankAccountsResult +import net.taler.wallet.accounts.PaytoUri import net.taler.wallet.balances.ScopeInfo.ExchangeLegacyKeys import net.taler.wallet.compose.ErrorComposable import net.taler.wallet.compose.GlobalScaffold @@ -51,16 +52,19 @@ fun DepositScreen( val accountManager = model.accountManager val exchangeManager = model.exchangeManager - val presetAmount = dest.amount?.let { Amount.fromJSONString(it) } - val receiverName = dest.receiverName - val receiverPostalCode = dest.receiverPostalCode - val receiverTown = dest.receiverTown - val iban = dest.IBAN - LaunchedEffect(Unit) { - if (presetAmount != null && receiverName != null && iban != null) { - val paytoUri = getIbanPayto(receiverName, receiverPostalCode, receiverTown, iban) - depositManager.makeDeposit(presetAmount, paytoUri) + // A payto:// link only names the destination: continue with the amount + // step, which shows fees and the maximum, and deposit when the user confirms. + dest.paytoUri?.let { paytoUri -> + depositManager.selectAccount( + KnownBankAccountInfo( + bankAccountId = "", + paytoUri = paytoUri, + kycCompleted = false, + currencies = null, + label = PaytoUri.parse(paytoUri)?.receiverName, + ) + ) } accountManager.listBankAccounts() } diff --git a/wallet/src/main/java/net/taler/wallet/deposit/PayToUriScreen.kt b/wallet/src/main/java/net/taler/wallet/deposit/PayToUriScreen.kt index 019fc840..39210fdf 100644 --- a/wallet/src/main/java/net/taler/wallet/deposit/PayToUriScreen.kt +++ b/wallet/src/main/java/net/taler/wallet/deposit/PayToUriScreen.kt @@ -16,40 +16,21 @@ package net.taler.wallet.deposit -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.core.net.toUri -import net.taler.common.Amount -import net.taler.common.CurrencySpecification -import net.taler.wallet.BottomInsetsSpacer import net.taler.wallet.NavigateCallback import net.taler.wallet.R import net.taler.wallet.WalletDestination -import net.taler.wallet.compose.AmountCurrencyField import net.taler.wallet.compose.GlobalScaffold +import net.taler.wallet.compose.LoadingScreen import net.taler.wallet.compose.TalerSurface -import net.taler.wallet.main.AmountResult import net.taler.wallet.main.MainViewModel @Composable @@ -59,11 +40,14 @@ fun PayToUriScreen( onNavigate: NavigateCallback, onNavigateBack: () -> Unit, ) { - val u = uri.toUri() - val receiverName = u.getQueryParameter("receiver-name")?.replace('+', ' ') ?: "" - val iban = u.pathSegments.lastOrNull() ?: "" - val currencies = model.balanceManager.getCurrencies() + val supported = model.depositManager.isSupportedPayToUri(uri) + + // The link names the destination; amount, fees and confirmation are + // handled by the regular deposit screen, as for a saved bank account. + if (currencies.isNotEmpty() && supported) LaunchedEffect(uri) { + onNavigate(WalletDestination.Deposit(paytoUri = uri), true) + } TalerSurface { GlobalScaffold( @@ -78,21 +62,8 @@ fun PayToUriScreen( text = stringResource(id = R.string.payment_balance_insufficient), color = MaterialTheme.colorScheme.error, ) - } else if (model.depositManager.isSupportedPayToUri(uri)) { - PayToComposable( - currencies = currencies, - getAmount = model::createAmount, - onAmountChosen = { amount -> - onNavigate( - WalletDestination.Deposit( - amount = amount.toJSONString(), - receiverName = receiverName, - IBAN = iban - ), true, - ) - }, - getCurrencySpec = model.exchangeManager::getSpecForCurrency, - ) + } else if (supported) { + LoadingScreen() } else { Text( modifier = Modifier.padding(16.dp), @@ -104,77 +75,3 @@ fun PayToUriScreen( } } } - -@Composable -private fun PayToComposable( - currencies: List, - getAmount: (String, String) -> AmountResult, - getCurrencySpec: (String) -> CurrencySpecification?, - onAmountChosen: (Amount) -> Unit, -) { - val scrollState = rememberScrollState() - Column( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp) - .verticalScroll(scrollState), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - var amount by remember { mutableStateOf(Amount.zero(currencies[0])) } - val currencySpec = remember(amount.currency) { getCurrencySpec(amount.currency) } - var amountError by rememberSaveable { mutableStateOf("") } - - AmountCurrencyField( - modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxWidth(), - amount = amount.withSpec(currencySpec), - currencies = currencies, - readOnly = false, - onAmountChanged = { amount = it }, - label = { Text(stringResource(R.string.amount_send)) }, - isError = amountError.isNotBlank(), - supportingText = { - if (amountError.isNotBlank()) { - Text(amountError) - } - } - ) - - val focusManager = LocalFocusManager.current - val errorStrInvalidAmount = stringResource(id = R.string.amount_invalid) - val errorStrInsufficientBalance = stringResource(id = R.string.payment_balance_insufficient) - Button( - modifier = Modifier.padding(16.dp), - enabled = !amount.isZero(), - onClick = { - when (val amountResult = getAmount(amount.amountStr, amount.currency)) { - is AmountResult.Success -> { - focusManager.clearFocus() - onAmountChosen(amountResult.amount) - } - is AmountResult.InvalidAmount -> amountError = errorStrInvalidAmount - is AmountResult.InsufficientBalance -> amountError = errorStrInsufficientBalance - } - }, - ) { - Text(text = stringResource(R.string.send_deposit_check_fees_button)) - } - - BottomInsetsSpacer() - } -} - -@Preview -@Composable -fun PreviewPayToComposable() { - Surface { - PayToComposable( - currencies = listOf("KUDOS", "TESTKUDOS", "BTCBITCOIN"), - getAmount = { _, _ -> AmountResult.InvalidAmount }, - onAmountChosen = {}, - getCurrencySpec = { null } - ) - } -}