gnunet-svn
[Top][All Lists]
Advanced

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

[taler-taler-android] 01/14: [wallet] Initial version of template suppor


From: gnunet
Subject: [taler-taler-android] 01/14: [wallet] Initial version of template support
Date: Tue, 26 Sep 2023 18:31:21 +0200

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

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

commit cb6d8362746481b383d559280c8cfadbed082231
Author: Iván Ávalos <avalos@disroot.org>
AuthorDate: Fri Aug 11 23:08:46 2023 +0200

    [wallet] Initial version of template support
---
 .../src/main/java/net/taler/wallet/MainActivity.kt |   4 +
 .../net/taler/wallet/deposit/PayToUriFragment.kt   |  13 +-
 .../taler/wallet/payment/PayTemplateFragment.kt    | 226 +++++++++++++++++++++
 .../net/taler/wallet/payment/PaymentManager.kt     |  21 ++
 wallet/src/main/res/navigation/nav_graph.xml       |  13 ++
 wallet/src/main/res/values/strings.xml             |   2 +
 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
@@ -214,6 +214,15 @@
             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">
+        <argument
+            android:name="uri"
+            app:argType="string" />
+    </fragment>
+
     <fragment
         android:id="@+id/nav_transactions"
         android:name="net.taler.wallet.transactions.TransactionsFragment"
@@ -371,6 +380,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..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>

-- 
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]