diff options
author | Iván Ávalos <avalos@disroot.org> | 2023-08-11 23:08:46 +0200 |
---|---|---|
committer | Torsten Grote <t@grobox.de> | 2023-09-26 18:30:51 +0200 |
commit | cb6d8362746481b383d559280c8cfadbed082231 (patch) | |
tree | f5f3d5c0349a646e1148f06a270fe18aac62e4a6 /wallet/src | |
parent | d7196a07eaaf5ca52906a8c987144c7b9814dfef (diff) | |
download | taler-android-cb6d8362746481b383d559280c8cfadbed082231.tar.gz taler-android-cb6d8362746481b383d559280c8cfadbed082231.tar.bz2 taler-android-cb6d8362746481b383d559280c8cfadbed082231.zip |
[wallet] Initial version of template support
Diffstat (limited to 'wallet/src')
6 files changed, 275 insertions, 4 deletions
diff --git a/wallet/src/main/java/net/taler/wallet/MainActivity.kt b/wallet/src/main/java/net/taler/wallet/MainActivity.kt index a49890e..cfeeb31 100644 --- a/wallet/src/main/java/net/taler/wallet/MainActivity.kt +++ b/wallet/src/main/java/net/taler/wallet/MainActivity.kt @@ -290,6 +290,10 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener, nav.navigate(R.id.action_global_prompt_push_payment) model.peerManager.preparePeerPushCredit(u2) } + action.startsWith("pay-template/", ignoreCase = true) -> { + val bundle = bundleOf("uri" to u2) + nav.navigate(R.id.action_global_prompt_pay_template, bundle) + } else -> { showError(R.string.error_unsupported_uri, "From: $from\nURI: $u2") } 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 c8b5b6e..243f589 100644 --- a/wallet/src/main/java/net/taler/wallet/deposit/PayToUriFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/deposit/PayToUriFragment.kt @@ -155,6 +155,9 @@ private fun PayToComposable( } ) CurrencyDropdown( + modifier = Modifier + .fillMaxSize() + .wrapContentSize(Alignment.Center), currencies = currencies, onCurrencyChanged = { c -> currency = c }, ) @@ -187,15 +190,17 @@ private fun PayToComposable( @OptIn(ExperimentalMaterial3Api::class) @Composable fun CurrencyDropdown( + modifier: Modifier = Modifier, + initialCurrency: String? = null, currencies: List<String>, onCurrencyChanged: (String) -> Unit, ) { - var selectedIndex by remember { mutableStateOf(0) } + val initialIndex = currencies.indexOf(initialCurrency) + .let { if (it < 0) null else it } + var selectedIndex by remember { mutableStateOf(initialIndex ?: 0) } var expanded by remember { mutableStateOf(false) } Box( - modifier = Modifier - .fillMaxSize() - .wrapContentSize(Alignment.Center), + modifier = modifier, ) { OutlinedTextField( modifier = Modifier diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateFragment.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateFragment.kt new file mode 100644 index 0000000..ab6dada --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateFragment.kt @@ -0,0 +1,226 @@ +/* + * 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 <http://www.gnu.org/licenses/> + */ + +package net.taler.wallet.payment + +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.ExperimentalMaterial3Api +import androidx.compose.material3.OutlinedTextField +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.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.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() { + private val model: MainViewModel by activityViewModels() + private var uriString: String? = null + private var uri: Uri? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + 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, + fragment = this@PayTemplateFragment, + model = model, + onSubmit = { createOrder(it) }, + ) + } + } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + if (uri?.queryParameterNames?.isEmpty() == true) { + createOrder(emptyMap()) + } + } + + fun createOrder(params: Map<String, String>) { + uriString?.let { + model.paymentManager.preparePayForTemplate(it, params,).invokeOnCompletion { + findNavController().navigate(R.id.action_global_promptPayment) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PayTemplateComposable( + uri: Uri, + currencies: List<String>, + fragment: Fragment, + model: MainViewModel, + onSubmit: (Map<String, String>) -> 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, + ) } + + 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<String, String>().apply { + if (summary != null) put("summary", summary!!) + if (amount != null) put("amount", amount!!.toJSONString()) + } + ) + } + } + } + }, + ) { + Text(stringResource(R.string.payment_create_order)) + } + } +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun AmountField( + modifier: Modifier = Modifier, + currencies: List<String>, + 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 c280304..538f2e1 100644 --- a/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt +++ b/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt @@ -32,6 +32,7 @@ import net.taler.wallet.payment.PayStatus.InsufficientBalance import net.taler.wallet.payment.PreparePayResponse.AlreadyConfirmedResponse import net.taler.wallet.payment.PreparePayResponse.InsufficientBalanceResponse import net.taler.wallet.payment.PreparePayResponse.PaymentPossibleResponse +import org.json.JSONObject val REGEX_PRODUCT_IMAGE = Regex("^data:image/(jpeg|png);base64,([A-Za-z0-9+/=]+)$") @@ -102,6 +103,26 @@ class PaymentManager( resetPayStatus() } + fun preparePayForTemplate(url: String, params: Map<String, String>) = scope.launch { + api.request("preparePayForTemplate", PreparePayResponse.serializer()) { + put("talerPayTemplateUri", url) + put("templateParams", JSONObject().apply { + params.forEach { put(it.key, it.value) } + }) + }.onError { + handleError("preparePayForTemplate", it) + }.onSuccess { response -> + mPayStatus.value = when (response) { + is PaymentPossibleResponse -> response.toPayStatusPrepared() + is InsufficientBalanceResponse -> InsufficientBalance( + response.contractTerms, + response.amountRaw + ) + is AlreadyConfirmedResponse -> AlreadyPaid + } + } + } + internal fun abortProposal(proposalId: String) = scope.launch { Log.i(TAG, "aborting proposal") api.request<Unit>("abortProposal") { diff --git a/wallet/src/main/res/navigation/nav_graph.xml b/wallet/src/main/res/navigation/nav_graph.xml index bc35f34..8f94f8d 100644 --- a/wallet/src/main/res/navigation/nav_graph.xml +++ b/wallet/src/main/res/navigation/nav_graph.xml @@ -215,6 +215,15 @@ </fragment> <fragment + android:id="@+id/promptPayTemplate" + android:name="net.taler.wallet.payment.PayTemplateFragment" + android:label="@string/payment_pay_template_title"> + <argument + android:name="uri" + app:argType="string" /> + </fragment> + + <fragment android:id="@+id/nav_transactions" android:name="net.taler.wallet.transactions.TransactionsFragment" android:label="@string/transactions_title" @@ -372,6 +381,10 @@ app:destination="@id/promptPushPayment" /> <action + android:id="@+id/action_global_prompt_pay_template" + app:destination="@id/promptPayTemplate" /> + + <action android:id="@+id/action_global_pending_operations" app:destination="@id/nav_pending_operations" /> diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml index 17e4e24..824c922 100644 --- a/wallet/src/main/res/values/strings.xml +++ b/wallet/src/main/res/values/strings.xml @@ -131,6 +131,8 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card <string name="payment_initiated">Payment initiated</string> <string name="payment_already_paid_title">Already paid</string> <string name="payment_already_paid">You\'ve already paid for this purchase.</string> + <string name="payment_pay_template_title">Customize your order</string> + <string name="payment_create_order">Create order</string> <string name="receive_amount">Amount to receive</string> <string name="receive_amount_invalid">Amount invalid</string> |