gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-taler-android] branch master updated (d7196a0 -> e7e2763)


From: gnunet
Subject: [taler-taler-android] branch master updated (d7196a0 -> e7e2763)
Date: Tue, 26 Sep 2023 18:31:20 +0200

This is an automated email from the git hooks/post-receive script.

torsten-grote pushed a change to branch master
in repository taler-android.

    from d7196a0  [wallet] don't crash when transaction list has no 
bindingAdapterPosition
     new cb6d836  [wallet] Initial version of template support
     new b38d99f  [wallet] Improved templates UX and PoS confirmation codes
     new aa1be46  [wallet] first cleanup of payment template work
     new 6734a0f  [wallet] Improve internal logic of templates
     new 0fee8e1  [wallet] add some potential TODOs for pay templates
     new 68a3c7c  [wallet] Additional refactoring of pay templates
     new 9837f4b  [wallet] fixes for templates parsing and UI
     new ed7f772  [wallet] Refactor amount input into single composable
     new 138ea13  [wallet] Support 0.x fractions in AmountInputField
     new 66d96c5  [wallet] Improved AmountInputField with a VisualTransformation
     new 1624b99  [wallet] fix: AmountInputField reacts to external changes
     new f967d32  [wallet] simplify AmountInputField
     new 559bfbe  [wallet] simplify pay templates
     new e7e2763  [wallet] Fix back navigation after template prompt.

The 14 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../src/main/java/net/taler/common/Amount.kt       |   1 +
 .../src/main/java/net/taler/wallet/MainActivity.kt |   4 +
 .../java/net/taler/wallet/ReceiveFundsFragment.kt  |  27 +---
 .../java/net/taler/wallet/SendFundsFragment.kt     |  35 ++--
 .../net/taler/wallet/compose/AmountInputField.kt   | 169 +++++++++++++++++++
 .../net/taler/wallet/deposit/PayToUriFragment.kt   |  39 +++--
 .../taler/wallet/payment/PayTemplateComposable.kt  | 180 +++++++++++++++++++++
 .../taler/wallet/payment/PayTemplateFragment.kt    | 123 ++++++++++++++
 .../wallet/payment/PayTemplateOrderComposable.kt   | 179 ++++++++++++++++++++
 .../net/taler/wallet/payment/PaymentManager.kt     |  25 +++
 .../wallet/payment/TransactionPaymentComposable.kt |   6 +
 .../net/taler/wallet/transactions/Transactions.kt  |   1 +
 wallet/src/main/res/navigation/nav_graph.xml       |  17 ++
 wallet/src/main/res/values/strings.xml             |   4 +
 14 files changed, 745 insertions(+), 65 deletions(-)
 create mode 100644 
wallet/src/main/java/net/taler/wallet/compose/AmountInputField.kt
 create mode 100644 
wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt
 create mode 100644 
wallet/src/main/java/net/taler/wallet/payment/PayTemplateFragment.kt
 create mode 100644 
wallet/src/main/java/net/taler/wallet/payment/PayTemplateOrderComposable.kt

diff --git a/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt 
b/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt
index 4861568..5fb36fa 100644
--- a/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt
+++ b/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt
@@ -90,6 +90,7 @@ public data class Amount(
         }
 
         public fun isValidAmountStr(str: String): Boolean {
+            if (str.count { it == '.' } > 1) return false
             val split = str.split(".")
             try {
                 checkValue(split[0].toLongOrNull())
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/ReceiveFundsFragment.kt 
b/wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt
index dbff6ae..e560a71 100644
--- a/wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt
@@ -29,12 +29,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.text.KeyboardOptions
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material3.Button
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedTextField
 import androidx.compose.material3.Surface
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
@@ -46,7 +44,6 @@ 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.Companion.Decimal
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
 import androidx.core.os.bundleOf
@@ -55,7 +52,7 @@ import androidx.fragment.app.activityViewModels
 import androidx.lifecycle.lifecycleScope
 import androidx.navigation.fragment.findNavController
 import net.taler.common.Amount
-import net.taler.common.Amount.Companion.isValidAmountStr
+import net.taler.wallet.compose.AmountInputField
 import net.taler.wallet.compose.TalerSurface
 import net.taler.wallet.exchanges.ExchangeItem
 
@@ -127,35 +124,27 @@ private fun ReceiveFundsIntro(
             .fillMaxWidth()
             .verticalScroll(scrollState),
     ) {
-        var text by rememberSaveable { mutableStateOf("") }
+        var text by rememberSaveable { mutableStateOf("0") }
         var isError by rememberSaveable { mutableStateOf(false) }
         Row(
             verticalAlignment = Alignment.CenterVertically,
             modifier = Modifier
                 .padding(16.dp),
         ) {
-            OutlinedTextField(
+            AmountInputField(
                 modifier = Modifier
                     .weight(1f)
                     .padding(end = 16.dp),
                 value = text,
-                keyboardOptions = KeyboardOptions.Default.copy(keyboardType = 
Decimal),
                 onValueChange = { input ->
                     isError = false
-                    val filtered = input.filter { it.isDigit() || it == '.' }
-                    if (filtered.endsWith('.') || isValidAmountStr(filtered)) 
text = filtered
+                    text = input
+                },
+                label = { Text(stringResource(R.string.receive_amount)) },
+                supportingText = {
+                    if (isError) 
Text(stringResource(R.string.receive_amount_invalid))
                 },
                 isError = isError,
-                label = {
-                    if (isError) {
-                        Text(
-                            stringResource(R.string.receive_amount_invalid),
-                            color = MaterialTheme.colorScheme.error,
-                        )
-                    } else {
-                        Text(stringResource(R.string.receive_amount))
-                    }
-                }
             )
             Text(
                 modifier = Modifier,
diff --git a/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt 
b/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt
index c2680d5..b33e53b 100644
--- a/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt
@@ -27,12 +27,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.text.KeyboardOptions
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material3.Button
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedTextField
 import androidx.compose.material3.Surface
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
@@ -44,7 +42,6 @@ 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.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
 import androidx.core.os.bundleOf
@@ -52,7 +49,7 @@ import androidx.fragment.app.Fragment
 import androidx.fragment.app.activityViewModels
 import androidx.navigation.fragment.findNavController
 import net.taler.common.Amount
-import net.taler.common.Amount.Companion.isValidAmountStr
+import net.taler.wallet.compose.AmountInputField
 import net.taler.wallet.compose.TalerSurface
 
 class SendFundsFragment : Fragment() {
@@ -108,7 +105,7 @@ private fun SendFundsIntro(
             .fillMaxWidth()
             .verticalScroll(scrollState),
     ) {
-        var text by rememberSaveable { mutableStateOf("") }
+        var text by rememberSaveable { mutableStateOf("0") }
         var isError by rememberSaveable { mutableStateOf(false) }
         var insufficientBalance by rememberSaveable { mutableStateOf(false) }
         Row(
@@ -116,34 +113,20 @@ private fun SendFundsIntro(
             modifier = Modifier
                 .padding(16.dp),
         ) {
-            OutlinedTextField(
+            AmountInputField(
                 modifier = Modifier
                     .weight(1f)
                     .padding(end = 16.dp),
                 value = text,
-                keyboardOptions = KeyboardOptions.Default.copy(keyboardType = 
KeyboardType.Decimal),
                 onValueChange = { input ->
                     isError = false
-                    insufficientBalance = false
-                    val filtered = input.filter { it.isDigit() || it == '.' }
-                    if (filtered.endsWith('.') || isValidAmountStr(filtered)) 
text = filtered
+                    text = input
                 },
-                isError = isError || insufficientBalance,
-                label = {
-                    if (isError) {
-                        Text(
-                            stringResource(R.string.receive_amount_invalid),
-                            color = MaterialTheme.colorScheme.error,
-                        )
-                    } else if (insufficientBalance) {
-                        Text(
-                            
stringResource(R.string.payment_balance_insufficient),
-                            color = MaterialTheme.colorScheme.error,
-                        )
-                    } else {
-                        Text(stringResource(R.string.send_amount))
-                    }
-                }
+                label = { Text(stringResource(R.string.send_amount)) },
+                supportingText = {
+                    if (isError) 
Text(stringResource(R.string.receive_amount_invalid))
+                },
+                isError = isError,
             )
             Text(
                 modifier = Modifier,
diff --git a/wallet/src/main/java/net/taler/wallet/compose/AmountInputField.kt 
b/wallet/src/main/java/net/taler/wallet/compose/AmountInputField.kt
new file mode 100644
index 0000000..0229ec5
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/compose/AmountInputField.kt
@@ -0,0 +1,169 @@
+/*
+ * 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.compose
+
+import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.LocalTextStyle
+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.Modifier
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.OffsetMapping
+import androidx.compose.ui.text.input.TransformedText
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import java.text.DecimalFormat
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AmountInputField(
+    value: String,
+    onValueChange: (value: String) -> Unit,
+    modifier: Modifier = Modifier,
+    label: @Composable (() -> Unit)? = null,
+    supportingText: @Composable (() -> Unit)? = null,
+    isError: Boolean = false,
+    keyboardActions: KeyboardActions = KeyboardActions.Default,
+) {
+    val decimalSeparator = 
DecimalFormat().decimalFormatSymbols.decimalSeparator
+    var amountInput by remember { mutableStateOf(value) }
+
+    // React to external changes
+    val amountValue = remember(amountInput, value) {
+        transformOutput(amountInput, decimalSeparator, '.').let {
+            if (value != it) value else amountInput
+        }
+    }
+
+    OutlinedTextField(
+        value = amountValue,
+        onValueChange = { input ->
+            val filtered = transformOutput(input, decimalSeparator, '.')
+            if (Amount.isValidAmountStr(filtered)) {
+                amountInput = transformInput(input, decimalSeparator, '.')
+                // tmpIn = input
+                onValueChange(filtered)
+            }
+        },
+        modifier = modifier,
+        textStyle = LocalTextStyle.current.copy(fontFamily = 
FontFamily.Monospace),
+        label = label,
+        supportingText = supportingText,
+        isError = isError,
+        visualTransformation = 
AmountInputVisualTransformation(decimalSeparator),
+        keyboardOptions = KeyboardOptions.Default.copy(keyboardType = 
KeyboardType.Decimal),
+        keyboardActions = keyboardActions,
+        singleLine = true,
+        maxLines = 1,
+    )
+}
+
+private class AmountInputVisualTransformation(
+    private val decimalSeparator: Char,
+) : VisualTransformation {
+
+    override fun filter(text: AnnotatedString): TransformedText {
+        val value = text.text
+        val output = transformOutput(value, '.', decimalSeparator)
+        val newText = AnnotatedString(output)
+        return TransformedText(
+            newText, CursorOffsetMapping(
+                unmaskedText = text.toString(),
+                maskedText = newText.toString().replace(decimalSeparator, '.'),
+            )
+        )
+    }
+
+    private class CursorOffsetMapping(
+        private val unmaskedText: String,
+        private val maskedText: String,
+    ) : OffsetMapping {
+        override fun originalToTransformed(offset: Int) = when {
+            unmaskedText.startsWith('.') -> if (offset == 0) 0 else (offset + 
1) // ".x" -> "0.x"
+            else -> offset
+        }
+
+        override fun transformedToOriginal(offset: Int) = when {
+            unmaskedText == "" -> 0 // "0" -> ""
+            unmaskedText == "." -> if (offset < 1) 0 else 1 // "0.0" -> "."
+            unmaskedText.startsWith('.') -> if (offset < 1) 0 else (offset - 
1) // "0.x" -> ".x"
+            unmaskedText.endsWith('.') && offset == maskedText.length -> 
offset - 1 // "x.0" -> "x."
+            else -> offset // "x" -> "x"
+        }
+    }
+}
+
+private fun transformInput(
+    input: String,
+    inputDecimalSeparator: Char = '.',
+    outputDecimalSeparator: Char = '.',
+) = input.trim().replace(inputDecimalSeparator, outputDecimalSeparator)
+
+private fun transformOutput(
+    input: String,
+    inputDecimalSeparator: Char = '.',
+    outputDecimalSeparator: Char = '.',
+) = transformInput(input, inputDecimalSeparator, outputDecimalSeparator).let {
+    when {
+        it.isEmpty() -> "0"
+        it == "$outputDecimalSeparator" -> "0${outputDecimalSeparator}0"
+        it.startsWith(outputDecimalSeparator) -> "0$it"
+        it.endsWith(outputDecimalSeparator) -> "${it}0"
+        else -> it
+    }
+}
+
+@Preview
+@Composable
+fun AmountInputFieldPreview() {
+    var value by remember { mutableStateOf("0") }
+    TalerSurface {
+        Column(
+            modifier = Modifier.fillMaxWidth().padding(16.dp),
+            verticalArrangement = spacedBy(16.dp),
+        ) {
+            AmountInputField(
+                value = value,
+                onValueChange = { value = it },
+                label = { Text("Amount input:") },
+                supportingText = { Text("This amount is nice.") },
+            )
+            AmountInputField(
+                value = value,
+                onValueChange = { value = it },
+                label = { Text("Error in amount input:") },
+                supportingText = { Text("Amount is invalid.") },
+                isError = true,
+            )
+        }
+    }
+}
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..4bc91e1 100644
--- a/wallet/src/main/java/net/taler/wallet/deposit/PayToUriFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/deposit/PayToUriFragment.kt
@@ -30,7 +30,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.text.KeyboardOptions
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material3.Button
 import androidx.compose.material3.DropdownMenu
@@ -49,13 +48,13 @@ 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
 import androidx.compose.ui.platform.ComposeView
 import androidx.compose.ui.platform.LocalFocusManager
 import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.input.KeyboardType
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
 import androidx.core.os.bundleOf
@@ -66,6 +65,7 @@ import net.taler.common.Amount
 import net.taler.wallet.AmountResult
 import net.taler.wallet.MainViewModel
 import net.taler.wallet.R
+import net.taler.wallet.compose.AmountInputField
 import net.taler.wallet.compose.TalerSurface
 
 class PayToUriFragment : Fragment() {
@@ -131,30 +131,27 @@ private fun PayToComposable(
         horizontalAlignment = Alignment.CenterHorizontally,
         verticalArrangement = Arrangement.spacedBy(16.dp),
     ) {
-        var amountText by rememberSaveable { mutableStateOf("") }
+        var amountText by rememberSaveable { mutableStateOf("0") }
         var amountError by rememberSaveable { mutableStateOf("") }
         var currency by rememberSaveable { mutableStateOf(currencies[0]) }
         val focusRequester = remember { FocusRequester() }
-        OutlinedTextField(
-            modifier = Modifier
-                .focusRequester(focusRequester),
+        AmountInputField(
+            modifier = Modifier.focusRequester(focusRequester),
             value = amountText,
             onValueChange = { input ->
                 amountError = ""
                 amountText = input
             },
-            keyboardOptions = KeyboardOptions.Default.copy(keyboardType = 
KeyboardType.Decimal),
-            singleLine = true,
+            label = { Text(stringResource(R.string.send_amount)) },
+            supportingText = {
+                if (amountError.isNotBlank()) Text(amountError)
+            },
             isError = amountError.isNotBlank(),
-            label = {
-                if (amountError.isBlank()) {
-                    Text(stringResource(R.string.send_amount))
-                } else {
-                    Text(amountError, color = MaterialTheme.colorScheme.error)
-                }
-            }
         )
         CurrencyDropdown(
+            modifier = Modifier
+                .fillMaxSize()
+                .wrapContentSize(Center),
             currencies = currencies,
             onCurrencyChanged = { c -> currency = c },
         )
@@ -189,17 +186,19 @@ private fun PayToComposable(
 fun CurrencyDropdown(
     currencies: List<String>,
     onCurrencyChanged: (String) -> Unit,
+    modifier: Modifier = Modifier,
+    initialCurrency: String? = null,
+    readOnly: Boolean = false,
 ) {
-    var selectedIndex by remember { mutableStateOf(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
-            .fillMaxSize()
-            .wrapContentSize(Alignment.Center),
+        modifier = modifier,
     ) {
         OutlinedTextField(
             modifier = Modifier
-                .clickable(onClick = { expanded = true }),
+                .clickable(onClick = { if (!readOnly) expanded = true }),
             value = currencies[selectedIndex],
             onValueChange = { },
             readOnly = true,
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..815f463
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt
@@ -0,0 +1,180 @@
+/*
+ * 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 androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment.Companion.Center
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import net.taler.common.Amount
+import net.taler.common.ContractTerms
+import net.taler.wallet.AmountResult
+import net.taler.wallet.R
+import net.taler.wallet.compose.TalerSurface
+
+sealed class AmountFieldStatus {
+    object FixedAmount : AmountFieldStatus()
+    class Default(
+        val amountStr: String? = null,
+        val currency: String? = null,
+    ) : AmountFieldStatus()
+
+    object Invalid : AmountFieldStatus()
+}
+
+@Composable
+fun PayTemplateComposable(
+    defaultSummary: String?,
+    amountStatus: AmountFieldStatus,
+    currencies: List<String>,
+    payStatus: PayStatus,
+    onCreateAmount: (String, String) -> AmountResult,
+    onSubmit: (summary: String?, amount: Amount?) -> Unit,
+    onError: (resId: Int) -> Unit,
+) {
+    // If wallet is empty, there's no way the user can pay something
+    if (amountStatus is AmountFieldStatus.Invalid) {
+        PayTemplateError(stringResource(R.string.receive_amount_invalid))
+    } else if (currencies.isEmpty()) {
+        PayTemplateError(stringResource(R.string.payment_balance_insufficient))
+    } else when (payStatus) {
+        is PayStatus.None -> PayTemplateOrderComposable(
+            currencies = currencies,
+            defaultSummary = defaultSummary,
+            amountStatus = amountStatus,
+            onCreateAmount = onCreateAmount,
+            onError = onError,
+            onSubmit = onSubmit,
+        )
+
+        is PayStatus.Loading -> PayTemplateLoading()
+        is PayStatus.AlreadyPaid -> 
PayTemplateError(stringResource(R.string.payment_already_paid))
+        is PayStatus.InsufficientBalance -> 
PayTemplateError(stringResource(R.string.payment_balance_insufficient))
+        is PayStatus.Error -> {} // handled in fragment will show bottom sheet 
FIXME white page?
+        is PayStatus.Prepared -> {} // handled in fragment, will redirect
+        is PayStatus.Success -> {} // handled by other UI flow, no need for 
content here
+    }
+}
+
+@Composable
+fun PayTemplateError(message: String) {
+    Box(
+        modifier = Modifier.fillMaxSize(),
+        contentAlignment = Center,
+    ) {
+        Text(
+            text = message,
+            style = MaterialTheme.typography.titleLarge,
+            color = MaterialTheme.colorScheme.error,
+        )
+    }
+}
+
+@Composable
+fun PayTemplateLoading() {
+    Box(
+        modifier = Modifier.fillMaxSize(),
+        contentAlignment = Center,
+    ) {
+        CircularProgressIndicator()
+    }
+}
+
+@Preview
+@Composable
+fun PayTemplateLoadingPreview() {
+    TalerSurface {
+        PayTemplateComposable(
+            defaultSummary = "Donation",
+            amountStatus = AmountFieldStatus.Default("20", "ARS"),
+            payStatus = PayStatus.Loading,
+            currencies = listOf("KUDOS", "ARS"),
+            onCreateAmount = { text, currency ->
+                AmountResult.Success(amount = Amount.fromString(currency, 
text))
+            },
+            onSubmit = { _, _ -> },
+            onError = { _ -> },
+        )
+    }
+}
+
+@Preview
+@Composable
+fun PayTemplateInsufficientBalancePreview() {
+    TalerSurface {
+        PayTemplateComposable(
+            defaultSummary = "Donation",
+            amountStatus = AmountFieldStatus.Default("20", "ARS"),
+            payStatus = PayStatus.InsufficientBalance(
+                ContractTerms(
+                    "test",
+                    amount = Amount.zero("TESTKUDOS"),
+                    products = emptyList()
+                ), Amount.zero("TESTKUDOS")
+            ),
+            currencies = listOf("KUDOS", "ARS"),
+            onCreateAmount = { text, currency ->
+                AmountResult.Success(amount = Amount.fromString(currency, 
text))
+            },
+            onSubmit = { _, _ -> },
+            onError = { _ -> },
+        )
+    }
+}
+
+@Preview
+@Composable
+fun PayTemplateAlreadyPaidPreview() {
+    TalerSurface {
+        PayTemplateComposable(
+            defaultSummary = "Donation",
+            amountStatus = AmountFieldStatus.Default("20", "ARS"),
+            payStatus = PayStatus.AlreadyPaid,
+            currencies = listOf("KUDOS", "ARS"),
+            onCreateAmount = { text, currency ->
+                AmountResult.Success(amount = Amount.fromString(currency, 
text))
+            },
+            onSubmit = { _, _ -> },
+            onError = { _ -> },
+        )
+    }
+}
+
+
+@Preview
+@Composable
+fun PayTemplateNoCurrenciesPreview() {
+    TalerSurface {
+        PayTemplateComposable(
+            defaultSummary = "Donation",
+            amountStatus = AmountFieldStatus.Default("20", "ARS"),
+            payStatus = PayStatus.None,
+            currencies = emptyList(),
+            onCreateAmount = { text, currency ->
+                AmountResult.Success(amount = Amount.fromString(currency, 
text))
+            },
+            onSubmit = { _, _ -> },
+            onError = { _ -> },
+        )
+    }
+}
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..64cb2c1
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateFragment.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.ui.platform.ComposeView
+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.showError
+import net.taler.wallet.MainViewModel
+import net.taler.wallet.R
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.compose.collectAsStateLifecycleAware
+import net.taler.wallet.showError
+
+class PayTemplateFragment : Fragment() {
+
+    private val model: MainViewModel by activityViewModels()
+    private lateinit var uriString: String
+    private lateinit var uri: Uri
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?,
+    ): View {
+        uriString = arguments?.getString("uri") ?: error("no amount passed")
+        uri = Uri.parse(uriString)
+
+        val defaultSummary = uri.getQueryParameter("summary")
+        val defaultAmount = uri.getQueryParameter("amount")
+        val amountFieldStatus = getAmountFieldStatus(defaultAmount)
+
+        val payStatusFlow = model.paymentManager.payStatus.asFlow()
+
+        return ComposeView(requireContext()).apply {
+            setContent {
+                val payStatus = 
payStatusFlow.collectAsStateLifecycleAware(initial = PayStatus.None)
+                TalerSurface {
+                    PayTemplateComposable(
+                        currencies = model.getCurrencies(),
+                        defaultSummary = defaultSummary,
+                        amountStatus = amountFieldStatus,
+                        payStatus = payStatus.value,
+                        onCreateAmount = model::createAmount,
+                        onSubmit = this@PayTemplateFragment::createOrder,
+                        onError = { this@PayTemplateFragment.showError(it) },
+                    )
+                }
+            }
+        }
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        if (uri.queryParameterNames?.isEmpty() == true) {
+            createOrder(null, null)
+        }
+
+        model.paymentManager.payStatus.observe(viewLifecycleOwner) { payStatus 
->
+            when (payStatus) {
+                is PayStatus.Prepared -> {
+                    val navOptions = NavOptions.Builder()
+                        .setPopUpTo(R.id.nav_main, true)
+                        .build()
+                    
findNavController().navigate(R.id.action_promptPayTemplate_to_promptPayment, 
null, navOptions)
+                }
+
+                is PayStatus.Error -> {
+                    if (model.devMode.value == true) {
+                        showError(payStatus.error)
+                    } else {
+                        showError(R.string.payment_template_error, 
payStatus.error.userFacingMsg)
+                    }
+                }
+
+                else -> {}
+            }
+        }
+    }
+
+    private fun getAmountFieldStatus(defaultAmount: String?): 
AmountFieldStatus {
+        return if (defaultAmount == null) {
+            AmountFieldStatus.FixedAmount
+        } else if (defaultAmount.isBlank()) {
+            AmountFieldStatus.Default()
+        } else {
+            val parts = defaultAmount.split(":")
+            when (parts.size) {
+                0 -> AmountFieldStatus.Default()
+                1 -> AmountFieldStatus.Default(currency = parts[0])
+                2 -> AmountFieldStatus.Default(parts[1], parts[0])
+                else -> AmountFieldStatus.Invalid
+            }
+        }
+    }
+
+    private fun createOrder(summary: String?, amount: Amount?) {
+        model.paymentManager.preparePayForTemplate(uriString, summary, amount)
+    }
+}
diff --git 
a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateOrderComposable.kt 
b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateOrderComposable.kt
new file mode 100644
index 0000000..1524faf
--- /dev/null
+++ 
b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateOrderComposable.kt
@@ -0,0 +1,179 @@
+/*
+ * 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 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.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.Companion.End
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.wallet.AmountResult
+import net.taler.wallet.R
+import net.taler.wallet.compose.AmountInputField
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.deposit.CurrencyDropdown
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun PayTemplateOrderComposable(
+    currencies: List<String>, // assumed to have size > 0
+    defaultSummary: String? = null,
+    amountStatus: AmountFieldStatus,
+    onCreateAmount: (String, String) -> AmountResult,
+    onError: (msgRes: Int) -> Unit,
+    onSubmit: (summary: String?, amount: Amount?) -> Unit,
+) {
+    val amountDefault = amountStatus as? AmountFieldStatus.Default
+
+    var summary by remember { mutableStateOf(defaultSummary) }
+    var currency by remember { mutableStateOf(amountDefault?.currency ?: 
currencies[0]) }
+    var amount by remember { mutableStateOf(amountDefault?.amountStr ?: "0") }
+
+    Column(horizontalAlignment = End) {
+        if (defaultSummary != null) OutlinedTextField(
+            modifier = Modifier
+                .padding(horizontal = 16.dp)
+                .fillMaxWidth(),
+            value = summary ?: "",
+            isError = summary.isNullOrBlank(),
+            onValueChange = { summary = it },
+            singleLine = true,
+            label = { 
Text(stringResource(R.string.withdraw_manual_ready_subject)) },
+        )
+        if (amountDefault != null) AmountField(
+            modifier = Modifier
+                .padding(16.dp)
+                .fillMaxWidth(),
+            amount = amount,
+            currency = currency,
+            currencies = currencies,
+            fixedCurrency = (amountStatus as? 
AmountFieldStatus.Default)?.currency != null,
+            onAmountChosen = { a, c ->
+                amount = a
+                currency = c
+            },
+        )
+        Button(
+            modifier = Modifier.padding(16.dp),
+            enabled = defaultSummary == null || !summary.isNullOrBlank(),
+            onClick = {
+                when (val res = onCreateAmount(amount, currency)) {
+                    is AmountResult.InsufficientBalance -> 
onError(R.string.payment_balance_insufficient)
+                    is AmountResult.InvalidAmount -> 
onError(R.string.receive_amount_invalid)
+                    is AmountResult.Success -> onSubmit(summary, res.amount)
+                }
+            },
+        ) {
+            Text(stringResource(R.string.payment_create_order))
+        }
+    }
+}
+
+@Composable
+private fun AmountField(
+    modifier: Modifier = Modifier,
+    currencies: List<String>,
+    fixedCurrency: Boolean,
+    amount: String,
+    currency: String,
+    onAmountChosen: (amount: String, currency: String) -> Unit,
+) {
+    Row(
+        modifier = modifier,
+    ) {
+        AmountInputField(
+            modifier = Modifier
+                .padding(end = 16.dp)
+                .weight(1f),
+            value = amount,
+            onValueChange = { onAmountChosen(it, currency) },
+            label = { Text(stringResource(R.string.send_amount)) }
+        )
+        CurrencyDropdown(
+            modifier = Modifier.weight(1f),
+            initialCurrency = currency,
+            currencies = currencies,
+            onCurrencyChanged = { onAmountChosen(amount, it) },
+            readOnly = fixedCurrency,
+        )
+    }
+}
+
+@Preview
+@Composable
+fun PayTemplateDefaultPreview() {
+    TalerSurface {
+        PayTemplateOrderComposable(
+            defaultSummary = "Donation",
+            amountStatus = AmountFieldStatus.Default("20", "ARS"),
+            currencies = listOf("KUDOS", "ARS"),
+            onCreateAmount = { text, currency ->
+                AmountResult.Success(amount = Amount.fromString(currency, 
text))
+            },
+            onSubmit = { _, _ -> },
+            onError = { },
+        )
+    }
+}
+
+@Preview
+@Composable
+fun PayTemplateFixedAmountPreview() {
+    TalerSurface {
+        PayTemplateOrderComposable(
+            defaultSummary = "default summary",
+            amountStatus = AmountFieldStatus.FixedAmount,
+            currencies = listOf("KUDOS", "ARS"),
+            onCreateAmount = { text, currency ->
+                AmountResult.Success(amount = Amount.fromString(currency, 
text))
+            },
+            onSubmit = { _, _ -> },
+            onError = { },
+        )
+    }
+}
+
+@Preview
+@Composable
+fun PayTemplateBlankSubjectPreview() {
+    TalerSurface {
+        PayTemplateOrderComposable(
+            defaultSummary = "",
+            amountStatus = AmountFieldStatus.FixedAmount,
+            currencies = listOf("KUDOS", "ARS"),
+            onCreateAmount = { text, currency ->
+                AmountResult.Success(amount = Amount.fromString(currency, 
text))
+            },
+            onSubmit = { _, _ -> },
+            onError = { },
+        )
+    }
+}
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..3a3069c 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+/=]+)$")
 
@@ -78,6 +79,7 @@ class PaymentManager(
                     response.contractTerms,
                     response.amountRaw
                 )
+
                 is AlreadyConfirmedResponse -> AlreadyPaid
             }
         }
@@ -102,6 +104,29 @@ class PaymentManager(
         resetPayStatus()
     }
 
+    fun preparePayForTemplate(url: String, summary: String?, amount: Amount?) 
= scope.launch {
+        mPayStatus.value = PayStatus.Loading
+        api.request("preparePayForTemplate", PreparePayResponse.serializer()) {
+            put("talerPayTemplateUri", url)
+            put("templateParams", JSONObject().apply {
+                summary?.let { put("summary", it) }
+                amount?.let { put("amount", it.toJSONString()) }
+            })
+        }.onError {
+            handleError("preparePayForTemplate", it)
+        }.onSuccess { response ->
+            mPayStatus.value = when (response) {
+                is PaymentPossibleResponse -> response.toPayStatusPrepared()
+                is InsufficientBalanceResponse -> InsufficientBalance(
+                    contractTerms = response.contractTerms,
+                    amountRaw = 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/java/net/taler/wallet/payment/TransactionPaymentComposable.kt 
b/wallet/src/main/java/net/taler/wallet/payment/TransactionPaymentComposable.kt
index e6a65d1..c08bc76 100644
--- 
a/wallet/src/main/java/net/taler/wallet/payment/TransactionPaymentComposable.kt
+++ 
b/wallet/src/main/java/net/taler/wallet/payment/TransactionPaymentComposable.kt
@@ -88,6 +88,12 @@ fun TransactionPaymentComposable(
             amount = t.amountEffective - t.amountRaw,
             amountType = AmountType.Negative,
         )
+        if (t.posConfirmation != null) {
+            TransactionInfoComposable(
+                label = stringResource(id = 
R.string.payment_confirmation_code),
+                info = t.posConfirmation,
+            )
+        }
         PurchaseDetails(info = t.info) {
             onFulfill(t.info.fulfillmentUrl ?: "")
         }
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt 
b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt
index c6be73a..536d433 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt
@@ -218,6 +218,7 @@ class TransactionPayment(
     override val error: TalerErrorInfo? = null,
     override val amountRaw: Amount,
     override val amountEffective: Amount,
+    val posConfirmation: String? = null,
 ) : Transaction() {
     override val icon = R.drawable.ic_cash_usd_outline
     override val detailPageNav = R.id.action_nav_transactions_detail_payment
diff --git a/wallet/src/main/res/navigation/nav_graph.xml 
b/wallet/src/main/res/navigation/nav_graph.xml
index bc35f34..99f4895 100644
--- a/wallet/src/main/res/navigation/nav_graph.xml
+++ b/wallet/src/main/res/navigation/nav_graph.xml
@@ -214,6 +214,19 @@
             app:popUpTo="@id/nav_main" />
     </fragment>
 
+    <fragment
+        android:id="@+id/promptPayTemplate"
+        android:name="net.taler.wallet.payment.PayTemplateFragment"
+        android:label="@string/payment_pay_template_title">
+        <action
+            android:id="@+id/action_promptPayTemplate_to_promptPayment"
+            app:destination="@+id/promptPayment"
+            app:popUpTo="@id/nav_main" />
+        <argument
+            android:name="uri"
+            app:argType="string" />
+    </fragment>
+
     <fragment
         android:id="@+id/nav_transactions"
         android:name="net.taler.wallet.transactions.TransactionsFragment"
@@ -371,6 +384,10 @@
         android:id="@+id/action_global_prompt_push_payment"
         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..e037055 100644
--- a/wallet/src/main/res/values/strings.xml
+++ b/wallet/src/main/res/values/strings.xml
@@ -131,6 +131,10 @@ 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="payment_confirmation_code">Confirmation code</string>
+    <string name="payment_template_error">Error creating order</string>
 
     <string name="receive_amount">Amount to receive</string>
     <string name="receive_amount_invalid">Amount invalid</string>

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]