From aa1be463c2a79d673f1dd2dd31538649a1cfb83c Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 18 Aug 2023 10:36:04 +0200 Subject: [wallet] first cleanup of payment template work the PayTemplateComposable still needs refactoring --- .../net/taler/wallet/deposit/PayToUriFragment.kt | 12 +- .../taler/wallet/payment/PayTemplateComposable.kt | 228 +++++++++++++++++++++ .../taler/wallet/payment/PayTemplateFragment.kt | 219 ++------------------ .../net/taler/wallet/payment/PaymentManager.kt | 4 +- 4 files changed, 248 insertions(+), 215 deletions(-) create mode 100644 wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt (limited to 'wallet/src/main/java') diff --git a/wallet/src/main/java/net/taler/wallet/deposit/PayToUriFragment.kt b/wallet/src/main/java/net/taler/wallet/deposit/PayToUriFragment.kt index 243f589..81f3617 100644 --- a/wallet/src/main/java/net/taler/wallet/deposit/PayToUriFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/deposit/PayToUriFragment.kt @@ -49,6 +49,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.Center import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester @@ -157,7 +158,7 @@ private fun PayToComposable( CurrencyDropdown( modifier = Modifier .fillMaxSize() - .wrapContentSize(Alignment.Center), + .wrapContentSize(Center), currencies = currencies, onCurrencyChanged = { c -> currency = c }, ) @@ -190,14 +191,13 @@ private fun PayToComposable( @OptIn(ExperimentalMaterial3Api::class) @Composable fun CurrencyDropdown( - modifier: Modifier = Modifier, - initialCurrency: String? = null, currencies: List, onCurrencyChanged: (String) -> Unit, + modifier: Modifier = Modifier, + initialCurrency: String? = null, ) { - val initialIndex = currencies.indexOf(initialCurrency) - .let { if (it < 0) null else it } - var selectedIndex by remember { mutableStateOf(initialIndex ?: 0) } + val initialIndex = currencies.indexOf(initialCurrency).let { if (it < 0) 0 else it } + var selectedIndex by remember { mutableStateOf(initialIndex) } var expanded by remember { mutableStateOf(false) } Box( modifier = modifier, diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt new file mode 100644 index 0000000..3279c71 --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt @@ -0,0 +1,228 @@ +/* + * This file is part of GNU Taler + * (C) 2023 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see + */ + +package net.taler.wallet.payment + +import android.net.Uri +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.Center +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.fragment.app.Fragment +import androidx.lifecycle.asFlow +import net.taler.common.Amount +import net.taler.common.AmountParserException +import net.taler.common.showError +import net.taler.wallet.AmountResult +import net.taler.wallet.MainViewModel +import net.taler.wallet.R +import net.taler.wallet.deposit.CurrencyDropdown + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PayTemplateComposable( + uri: Uri, + currencies: List, + fragment: Fragment, + model: MainViewModel, + onSubmit: (Map) -> Unit, +) { + val queryParams = uri.queryParameterNames + + var summary by remember { mutableStateOf( + if ("summary" in queryParams) + uri.getQueryParameter("summary") else null, + ) } + + var amount by remember { mutableStateOf( + if ("amount" in queryParams) { + val amount = uri.getQueryParameter("amount")!! + val parts = amount.split(':') + when (parts.size) { + 1 -> Amount.fromString(parts[0], "0") + 2 -> Amount.fromString(parts[0], parts[1]) + else -> throw AmountParserException("Invalid Amount Format") + } + } else null, + ) } + + val payStatus by model.paymentManager.payStatus.asFlow() + .collectAsState(initial = PayStatus.None) + + // If wallet is empty, there's no way the user can pay something + if (payStatus is PayStatus.InsufficientBalance || currencies.isEmpty()) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Center, + ) { + Text( + text = stringResource(R.string.payment_balance_insufficient), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.error, + ) + } + } else when (payStatus) { + is PayStatus.None -> { + Column(horizontalAlignment = Alignment.End) { + if ("summary" in queryParams) { + OutlinedTextField( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + value = summary!!, + isError = summary!!.isBlank(), + onValueChange = { summary = it }, + singleLine = true, + label = { Text(stringResource(R.string.withdraw_manual_ready_subject)) }, + ) + } + + if ("amount" in queryParams) { + AmountField( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + amount = amount!!, + currencies = currencies, + onAmountChosen = { amount = it }, + ) + } + + Button( + modifier = Modifier.padding(16.dp), + enabled = summary == null || summary!!.isNotBlank(), + onClick = { + if (amount != null) { + val result = model.createAmount( + amount!!.amountStr, + amount!!.currency, + ) + when (result) { + AmountResult.InsufficientBalance -> { + fragment.showError(R.string.payment_balance_insufficient) + } + AmountResult.InvalidAmount -> { + fragment.showError(R.string.receive_amount_invalid) + } + else -> { + onSubmit( + mutableMapOf().apply { + if (summary != null) put("summary", summary!!) + if (amount != null) put("amount", amount!!.toJSONString()) + } + ) + } + } + } + }, + ) { + Text(stringResource(R.string.payment_create_order)) + } + } + } + is PayStatus.Loading -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Center, + ) { CircularProgressIndicator() } + } + is PayStatus.AlreadyPaid -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Center, + ) { + Text( + stringResource(R.string.payment_already_paid), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.error, + ) + } + } + else -> {} + } +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun AmountField( + modifier: Modifier = Modifier, + currencies: List, + amount: Amount, + onAmountChosen: (Amount) -> Unit, +) { + Row( + modifier = modifier, + ) { + val amountText = if (amount.value == 0L) "" else amount.value.toString() + val currency = currencies.find { amount.currency == it } ?: currencies[0] + OutlinedTextField( + modifier = Modifier + .padding(end = 16.dp) + .weight(1f), + value = amountText, + placeholder = { Text("0") }, + onValueChange = { input -> + if (input.isNotBlank()) { + onAmountChosen(Amount.fromString(currency, input)) + } else { + onAmountChosen(Amount.zero(currency)) + } + }, + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Decimal), + singleLine = true, + label = { Text(stringResource(R.string.send_amount)) }, + ) + CurrencyDropdown( + modifier = Modifier.weight(1f), + initialCurrency = currency, + currencies = currencies, + onCurrencyChanged = { c -> + onAmountChosen(Amount.fromString(c, amount.amountStr)) + }, + ) + } +} + +// TODO cleanup composable +//@Preview +//@Composable +//fun PayTemplateComposablePreview() { +// TalerSurface { +// PayTemplateComposable(Uri.EMPTY, listOf("KUDOS")) +// } +//} diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateFragment.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateFragment.kt index 633ab20..080d319 100644 --- a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateFragment.kt @@ -21,65 +21,35 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Button -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.unit.dp import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.lifecycle.asFlow import androidx.navigation.NavOptions import androidx.navigation.fragment.findNavController -import net.taler.common.Amount -import net.taler.common.AmountParserException -import net.taler.common.showError -import net.taler.wallet.AmountResult import net.taler.wallet.MainViewModel import net.taler.wallet.R import net.taler.wallet.compose.TalerSurface -import net.taler.wallet.deposit.CurrencyDropdown -class PayTemplateFragment: Fragment() { +class PayTemplateFragment : Fragment() { + private val model: MainViewModel by activityViewModels() - private var uriString: String? = null - private var uri: Uri? = null + private lateinit var uriString: String + private lateinit var uri: Uri override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? + savedInstanceState: Bundle?, ): View { uriString = arguments?.getString("uri") ?: error("no amount passed") uri = Uri.parse(uriString) - val currencies = model.getCurrencies() return ComposeView(requireContext()).apply { setContent { TalerSurface { PayTemplateComposable( - uri = uri!!, - currencies = currencies, + uri = uri, + currencies = model.getCurrencies(), fragment = this@PayTemplateFragment, model = model, onSubmit = { createOrder(it) }, @@ -93,186 +63,21 @@ class PayTemplateFragment: Fragment() { super.onViewCreated(view, savedInstanceState) // TODO: this is not ideal, if the template is fixed, the // user shouldn't even have to go through this fragment. - if (uri?.queryParameterNames?.isEmpty() == true) { + if (uri.queryParameterNames?.isEmpty() == true) { createOrder(emptyMap()) } } private fun createOrder(params: Map) { - uriString ?: return - model.paymentManager.preparePayForTemplate(uriString!!, params,).invokeOnCompletion { + model.paymentManager.preparePayForTemplate(uriString, params).invokeOnCompletion { + // TODO maybe better to observe/collect payStatus instead of invokeOnCompletion + // and then only reacting to one of the possible payStatus values if (model.paymentManager.payStatus.value is PayStatus.Prepared) { val navOptions = NavOptions.Builder() .setPopUpTo(R.id.nav_main, true) .build() - findNavController() - .navigate(R.id.action_global_promptPayment, null, navOptions) + findNavController().navigate(R.id.action_global_promptPayment, null, navOptions) } } } } - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun PayTemplateComposable( - uri: Uri, - currencies: List, - fragment: Fragment, - model: MainViewModel, - onSubmit: (Map) -> Unit, -) { - val queryParams = uri.queryParameterNames - - var summary by remember { mutableStateOf( - if ("summary" in queryParams) - uri.getQueryParameter("summary") else null, - ) } - - var amount by remember { mutableStateOf( - if ("amount" in queryParams) { - val amount = uri.getQueryParameter("amount")!! - val parts = amount.split(':') - when (parts.size) { - 1 -> Amount.fromString(parts[0], "0") - 2 -> Amount.fromString(parts[0], parts[1]) - else -> throw AmountParserException("Invalid Amount Format") - } - } else null, - ) } - - val payStatus by model.paymentManager.payStatus.asFlow() - .collectAsState(initial = PayStatus.None) - - // If wallet is empty, there's no way the user can pay something - if (payStatus is PayStatus.InsufficientBalance || currencies.isEmpty()) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - Text( - stringResource(R.string.payment_balance_insufficient), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.error, - ) - } - } else when (payStatus) { - is PayStatus.None -> { - Column(horizontalAlignment = Alignment.End) { - if ("summary" in queryParams) { - OutlinedTextField( - modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxWidth(), - value = summary!!, - isError = summary!!.isBlank(), - onValueChange = { summary = it }, - singleLine = true, - label = { Text(stringResource(R.string.withdraw_manual_ready_subject)) }, - ) - } - - if ("amount" in queryParams) { - AmountField( - modifier = Modifier - .padding(16.dp) - .fillMaxWidth(), - amount = amount!!, - currencies = currencies, - onAmountChosen = { amount = it }, - ) - } - - Button( - modifier = Modifier.padding(16.dp), - enabled = summary == null || summary!!.isNotBlank(), - onClick = { - if (amount != null) { - val result = model.createAmount( - amount!!.amountStr, - amount!!.currency, - ) - when (result) { - AmountResult.InsufficientBalance -> { - fragment.showError(R.string.payment_balance_insufficient) - } - AmountResult.InvalidAmount -> { - fragment.showError(R.string.receive_amount_invalid) - } - else -> { - onSubmit( - mutableMapOf().apply { - if (summary != null) put("summary", summary!!) - if (amount != null) put("amount", amount!!.toJSONString()) - } - ) - } - } - } - }, - ) { - Text(stringResource(R.string.payment_create_order)) - } - } - } - is PayStatus.Loading -> { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { CircularProgressIndicator() } - } - is PayStatus.AlreadyPaid -> { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - Text( - stringResource(R.string.payment_already_paid), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.error, - ) - } - } - else -> {} - } -} - -@Composable -@OptIn(ExperimentalMaterial3Api::class) -private fun AmountField( - modifier: Modifier = Modifier, - currencies: List, - amount: Amount, - onAmountChosen: (Amount) -> Unit, -) { - Row( - modifier = modifier, - ) { - val amountText = if (amount.value == 0L) "" else amount.value.toString() - val currency = currencies.find { amount.currency == it } ?: currencies[0] - OutlinedTextField( - modifier = Modifier - .padding(end = 16.dp) - .weight(1f), - value = amountText, - placeholder = { Text("0") }, - onValueChange = { input -> - if (input.isNotBlank()) { - onAmountChosen(Amount.fromString(currency, input)) - } else { - onAmountChosen(Amount.zero(currency)) - } - }, - keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Decimal), - singleLine = true, - label = { Text(stringResource(R.string.send_amount)) }, - ) - CurrencyDropdown( - modifier = Modifier.weight(1f), - initialCurrency = currency, - currencies = currencies, - onCurrencyChanged = { c -> - onAmountChosen(Amount.fromString(c, amount.amountStr)) - }, - ) - } -} \ No newline at end of file diff --git a/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt b/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt index c98e0b2..627c05d 100644 --- a/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt +++ b/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt @@ -116,8 +116,8 @@ class PaymentManager( mPayStatus.value = when (response) { is PaymentPossibleResponse -> response.toPayStatusPrepared() is InsufficientBalanceResponse -> InsufficientBalance( - response.contractTerms, - response.amountRaw + contractTerms = response.contractTerms, + amountRaw = response.amountRaw, ) is AlreadyConfirmedResponse -> AlreadyPaid } -- cgit v1.2.3