From cb6d8362746481b383d559280c8cfadbed082231 Mon Sep 17 00:00:00 2001 From: Iván Ávalos Date: Fri, 11 Aug 2023 23:08:46 +0200 Subject: [wallet] Initial version of template support --- .../taler/wallet/payment/PayTemplateFragment.kt | 226 +++++++++++++++++++++ .../net/taler/wallet/payment/PaymentManager.kt | 21 ++ 2 files changed, 247 insertions(+) create mode 100644 wallet/src/main/java/net/taler/wallet/payment/PayTemplateFragment.kt (limited to 'wallet/src/main/java/net/taler/wallet/payment') 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 + */ + +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) { + 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, + 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, + ) } + + 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)) + } + } +} + +@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 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) = 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("abortProposal") { -- cgit v1.2.3