summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIván Ávalos <avalos@disroot.org>2023-08-11 23:08:46 +0200
committerIván Ávalos <avalos@disroot.org>2023-09-22 17:01:03 -0600
commit252f2985019fdea16992d110a5f8310c55ee1516 (patch)
tree1a2ab59cdb59812af433cb8f47d2a746e412d401
parent9bfc8b7a4e0cca3cfb052750385f2005c7bc31d7 (diff)
downloadtaler-android-252f2985019fdea16992d110a5f8310c55ee1516.tar.gz
taler-android-252f2985019fdea16992d110a5f8310c55ee1516.tar.bz2
taler-android-252f2985019fdea16992d110a5f8310c55ee1516.zip
[wallet] Initial version of template support
-rw-r--r--wallet/src/main/java/net/taler/wallet/MainActivity.kt4
-rw-r--r--wallet/src/main/java/net/taler/wallet/deposit/PayToUriFragment.kt13
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/PayTemplateFragment.kt226
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt21
-rw-r--r--wallet/src/main/res/navigation/nav_graph.xml13
-rw-r--r--wallet/src/main/res/values/strings.xml2
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>