gnunet-svn
[Top][All Lists]
Advanced

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

[taler-taler-android] branch master updated (2398d0f -> 431e326)


From: gnunet
Subject: [taler-taler-android] branch master updated (2398d0f -> 431e326)
Date: Thu, 27 Oct 2022 15:38:00 +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 2398d0f  [wallet] Remove old anastasis prototype
     new 725562a  [wallet] Implement making deposits (not fully functional)
     new 431e326  [wallet] Check for sufficient balance when sending funds

The 2 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/AndroidUtils.kt |  12 +-
 .../main/java/net/taler/wallet/MainViewModel.kt    |  11 +
 .../java/net/taler/wallet/ReceiveFundsFragment.kt  |   5 +-
 .../java/net/taler/wallet/SendFundsFragment.kt     | 158 +++++++++++--
 .../net/taler/wallet/accounts/KnownBankAccounts.kt |  19 +-
 .../net/taler/wallet/payment/DepositFragment.kt    | 262 +++++++++++++++++++++
 .../java/net/taler/wallet/payment/DepositState.kt  |  30 ++-
 .../net/taler/wallet/payment/PaymentManager.kt     |  97 ++++++--
 ...oingPullFragment.kt => OutgoingPushFragment.kt} |  22 +-
 .../wallet/peer/OutgoingPushIntroComposable.kt     |  39 +--
 wallet/src/main/res/navigation/nav_graph.xml       |  25 +-
 wallet/src/main/res/values/strings.xml             |  13 +-
 12 files changed, 598 insertions(+), 95 deletions(-)
 create mode 100644 
wallet/src/main/java/net/taler/wallet/payment/DepositFragment.kt
 copy cashier/src/main/java/net/taler/cashier/SignedAmount.kt => 
wallet/src/main/java/net/taler/wallet/payment/DepositState.kt (53%)
 copy wallet/src/main/java/net/taler/wallet/peer/{OutgoingPullFragment.kt => 
OutgoingPushFragment.kt} (73%)

diff --git 
a/taler-kotlin-android/src/main/java/net/taler/common/AndroidUtils.kt 
b/taler-kotlin-android/src/main/java/net/taler/common/AndroidUtils.kt
index 7dde872..c6d34e9 100644
--- a/taler-kotlin-android/src/main/java/net/taler/common/AndroidUtils.kt
+++ b/taler-kotlin-android/src/main/java/net/taler/common/AndroidUtils.kt
@@ -97,13 +97,21 @@ fun Context.isOnline(): Boolean {
 }
 
 fun FragmentActivity.showError(mainText: String, detailText: String = "") = 
ErrorBottomSheet
-        .newInstance(mainText, detailText)
-        .show(supportFragmentManager, "ERROR_BOTTOM_SHEET")
+    .newInstance(mainText, detailText)
+    .show(supportFragmentManager, "ERROR_BOTTOM_SHEET")
 
 fun FragmentActivity.showError(@StringRes mainId: Int, detailText: String = 
"") {
     showError(getString(mainId), detailText)
 }
 
+fun Fragment.showError(mainText: String, detailText: String = "") = 
ErrorBottomSheet
+    .newInstance(mainText, detailText)
+    .show(parentFragmentManager, "ERROR_BOTTOM_SHEET")
+
+fun Fragment.showError(@StringRes mainId: Int, detailText: String = "") {
+    showError(getString(mainId), detailText)
+}
+
 fun Fragment.startActivitySafe(intent: Intent) {
     try {
         startActivity(intent)
diff --git a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt 
b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
index 50438c4..aa9b0f1 100644
--- a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
+++ b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
@@ -26,6 +26,7 @@ import androidx.lifecycle.distinctUntilChanged
 import androidx.lifecycle.viewModelScope
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.launch
+import net.taler.common.Amount
 import net.taler.common.Event
 import net.taler.common.assertUiThread
 import net.taler.common.toEvent
@@ -140,6 +141,16 @@ class MainViewModel(val app: Application) : 
AndroidViewModel(app) {
         mTransactionsEvent.value = currency.toEvent()
     }
 
+    @UiThread
+    fun hasSufficientBalance(amount: Amount): Boolean {
+        balances.value?.forEach { balanceItem ->
+            if (balanceItem.currency == amount.currency) {
+                return balanceItem.available >= amount
+            }
+        }
+        return false
+    }
+
     @UiThread
     fun dangerouslyReset() {
         api.sendRequest("reset")
diff --git a/wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt 
b/wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt
index 31228a4..cf01e59 100644
--- a/wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt
@@ -23,8 +23,10 @@ import android.view.ViewGroup
 import android.widget.Toast
 import android.widget.Toast.LENGTH_LONG
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
 import androidx.compose.foundation.layout.Row
 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
@@ -167,6 +169,7 @@ private fun ReceiveFundsIntro(
             Button(
                 modifier = Modifier
                     .padding(end = 16.dp)
+                    .height(IntrinsicSize.Max)
                     .weight(1f),
                 onClick = {
                     val amount = getAmount(currency, text)
@@ -176,7 +179,7 @@ private fun ReceiveFundsIntro(
                 Text(text = stringResource(R.string.receive_withdraw))
             }
             Button(
-                modifier = Modifier.weight(1f),
+                modifier = Modifier.weight(1f).height(IntrinsicSize.Max),
                 onClick = {
                     val amount = getAmount(currency, text)
                     if (amount == null) isError = true
diff --git a/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt 
b/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt
index 290c91b..d6f7280 100644
--- a/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt
@@ -20,21 +20,42 @@ 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.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+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.material.Button
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.OutlinedTextField
 import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
 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
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.activityViewModels
-import androidx.navigation.findNavController
+import androidx.navigation.fragment.findNavController
 import com.google.android.material.composethemeadapter.MdcTheme
 import net.taler.common.Amount
-import net.taler.wallet.compose.collectAsStateLifecycleAware
-import net.taler.wallet.peer.OutgoingIntro
-import net.taler.wallet.peer.OutgoingPushIntroComposable
-import net.taler.wallet.peer.OutgoingPushResultComposable
 
 class SendFundsFragment : Fragment() {
     private val model: MainViewModel by activityViewModels()
-    private val transactionManager get() = model.transactionManager
     private val peerManager get() = model.peerManager
 
     override fun onCreateView(
@@ -44,16 +65,12 @@ class SendFundsFragment : Fragment() {
         setContent {
             MdcTheme {
                 Surface {
-                    val state = 
peerManager.pushState.collectAsStateLifecycleAware()
-                    if (state.value is OutgoingIntro) {
-                        val currency = transactionManager.selectedCurrency
-                            ?: error("No currency selected")
-                        OutgoingPushIntroComposable(currency, 
this@SendFundsFragment::onSend)
-                    } else {
-                        OutgoingPushResultComposable(state.value) {
-                            findNavController().popBackStack()
-                        }
-                    }
+                    SendFundsIntro(
+                        model.transactionManager.selectedCurrency ?: error("No 
currency selected"),
+                        model::hasSufficientBalance,
+                        this@SendFundsFragment::onDeposit,
+                        this@SendFundsFragment::onPeerPush,
+                    )
                 }
             }
         }
@@ -69,7 +86,112 @@ class SendFundsFragment : Fragment() {
         if (!requireActivity().isChangingConfigurations) 
peerManager.resetPushPayment()
     }
 
-    private fun onSend(amount: Amount, summary: String) {
-        peerManager.initiatePeerPushPayment(amount, summary)
+    fun onDeposit(amount: Amount) {
+        val bundle = bundleOf("amount" to amount.toJSONString())
+        findNavController().navigate(R.id.action_sendFunds_to_nav_deposit, 
bundle)
+    }
+
+    fun onPeerPush(amount: Amount) {
+        val bundle = bundleOf("amount" to amount.toJSONString())
+        findNavController().navigate(R.id.action_sendFunds_to_nav_peer_push, 
bundle)
+    }
+}
+
+@Composable
+private fun SendFundsIntro(
+    currency: String,
+    hasSufficientBalance: (Amount) -> Boolean,
+    onDeposit: (Amount) -> Unit,
+    onPeerPush: (Amount) -> Unit,
+) {
+    val scrollState = rememberScrollState()
+    Column(
+        modifier = Modifier
+            .fillMaxWidth()
+            .verticalScroll(scrollState),
+    ) {
+        var text by rememberSaveable { mutableStateOf("") }
+        var isError by rememberSaveable { mutableStateOf(false) }
+        var insufficientBalance by rememberSaveable { mutableStateOf(false) }
+        Row(
+            verticalAlignment = Alignment.CenterVertically,
+            modifier = Modifier
+                .padding(16.dp),
+        ) {
+            OutlinedTextField(
+                modifier = Modifier
+                    .weight(1f)
+                    .padding(end = 16.dp),
+                value = text,
+                keyboardOptions = KeyboardOptions.Default.copy(keyboardType = 
KeyboardType.Decimal),
+                onValueChange = { input ->
+                    isError = false
+                    insufficientBalance = false
+                    text = input.filter { it.isDigit() || it == '.' }
+                },
+                isError = isError || insufficientBalance,
+                label = {
+                    if (isError) {
+                        Text(
+                            stringResource(R.string.receive_amount_invalid),
+                            color = Color.Red,
+                        )
+                    } else if (insufficientBalance) {
+                        Text(
+                            
stringResource(R.string.payment_balance_insufficient),
+                            color = Color.Red,
+                        )
+                    } else {
+                        Text(stringResource(R.string.send_amount))
+                    }
+                }
+            )
+            Text(
+                modifier = Modifier,
+                text = currency,
+                softWrap = false,
+                style = MaterialTheme.typography.h6,
+            )
+        }
+        Text(
+            modifier = Modifier.padding(horizontal = 16.dp),
+            text = stringResource(R.string.send_intro),
+            style = MaterialTheme.typography.h6,
+        )
+        Row(modifier = Modifier.padding(16.dp)) {
+            fun onClickButton(block: (Amount) -> Unit) {
+                val amount = getAmount(currency, text)
+                if (amount == null) isError = true
+                else if (!hasSufficientBalance(amount)) insufficientBalance = 
true
+                else block(amount)
+            }
+            Button(
+                modifier = Modifier
+                    .padding(end = 16.dp)
+                    .weight(1f),
+                onClick = {
+                    onClickButton { amount -> onDeposit(amount) }
+                }) {
+                Text(text = stringResource(R.string.send_deposit))
+            }
+            Button(
+                modifier = Modifier
+                    .height(IntrinsicSize.Max)
+                    .weight(1f),
+                onClick = {
+                    onClickButton { amount -> onPeerPush(amount) }
+                },
+            ) {
+                Text(text = stringResource(R.string.send_peer))
+            }
+        }
+    }
+}
+
+@Preview
+@Composable
+fun PreviewSendFundsIntro() {
+    Surface {
+        SendFundsIntro("TESTKUDOS", { true }, {}) {}
     }
 }
diff --git 
a/wallet/src/main/java/net/taler/wallet/accounts/KnownBankAccounts.kt 
b/wallet/src/main/java/net/taler/wallet/accounts/KnownBankAccounts.kt
index 0dcb18e..a0ce956 100644
--- a/wallet/src/main/java/net/taler/wallet/accounts/KnownBankAccounts.kt
+++ b/wallet/src/main/java/net/taler/wallet/accounts/KnownBankAccounts.kt
@@ -16,6 +16,7 @@
 
 package net.taler.wallet.accounts
 
+import android.net.Uri
 import kotlinx.serialization.ExperimentalSerializationApi
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
@@ -48,14 +49,28 @@ sealed class PaytoUri(
 
 @Serializable
 @SerialName("iban")
-class PaytoUriIBAN(
+class PaytoUriIban(
     val iban: String,
+    val bic: String? = "SANDBOXX",
     override val targetPath: String,
     override val params: Map<String, String>,
 ) : PaytoUri(
     isKnown = true,
     targetType = "iban",
-)
+) {
+    val paytoUri: String
+        get() = Uri.Builder()
+            .scheme("payto")
+            .appendEncodedPath("/$targetType")
+            .apply { if (bic != null) appendPath(bic) }
+            .appendPath(iban)
+            .apply {
+                params.forEach { (key, value) ->
+                    appendQueryParameter(key, value)
+                }
+            }
+            .build().toString()
+}
 
 @Serializable
 @SerialName("x-taler-bank")
diff --git a/wallet/src/main/java/net/taler/wallet/payment/DepositFragment.kt 
b/wallet/src/main/java/net/taler/wallet/payment/DepositFragment.kt
new file mode 100644
index 0000000..add9467
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/payment/DepositFragment.kt
@@ -0,0 +1,262 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 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.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Button
+import androidx.compose.material.OutlinedTextField
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+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.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import com.google.android.material.composethemeadapter.MdcTheme
+import net.taler.common.Amount
+import net.taler.wallet.MainViewModel
+import net.taler.wallet.R
+import net.taler.wallet.compose.collectAsStateLifecycleAware
+
+class DepositFragment : Fragment() {
+    private val model: MainViewModel by activityViewModels()
+    private val paymentManager get() = model.paymentManager
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?,
+    ): View {
+        val amount = arguments?.getString("amount")?.let {
+            Amount.fromJSONString(it)
+        } ?: error("no amount passed")
+
+        return ComposeView(requireContext()).apply {
+            setContent {
+                MdcTheme {
+                    Surface {
+                        val state = 
paymentManager.depositState.collectAsStateLifecycleAware()
+                        MakeDepositComposable(
+                            state = state.value,
+                            amount = amount,
+                            onMakeDeposit = 
this@DepositFragment::onDepositButtonClicked,
+                        )
+                    }
+                }
+            }
+        }
+    }
+
+    override fun onStart() {
+        super.onStart()
+        activity?.setTitle(R.string.send_deposit_title)
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        if (!requireActivity().isChangingConfigurations) {
+            paymentManager.resetDepositState()
+        }
+    }
+
+    private fun onDepositButtonClicked(
+        amount: Amount,
+        receiverName: String,
+        iban: String,
+        bic: String,
+    ) {
+        paymentManager.onDepositButtonClicked(amount, receiverName, iban, bic)
+    }
+}
+
+@Composable
+private fun MakeDepositComposable(
+    state: DepositState,
+    amount: Amount,
+    onMakeDeposit: (Amount, String, String, String) -> Unit,
+) {
+    val scrollState = rememberScrollState()
+    Column(
+        modifier = Modifier
+            .fillMaxWidth()
+            .verticalScroll(scrollState),
+        horizontalAlignment = Alignment.CenterHorizontally,
+    ) {
+        var name by rememberSaveable { mutableStateOf("") }
+        var iban by rememberSaveable { mutableStateOf("") }
+        var bic by rememberSaveable { mutableStateOf("") }
+        val focusRequester = remember { FocusRequester() }
+        OutlinedTextField(
+            modifier = Modifier
+                .padding(16.dp)
+                .focusRequester(focusRequester),
+            value = name,
+            enabled = !state.showFees,
+            onValueChange = { input ->
+                name = input
+            },
+            isError = name.isBlank(),
+            label = {
+                Text(
+                    stringResource(R.string.send_deposit_name),
+                    color = if (name.isBlank()) {
+                        colorResource(R.color.red)
+                    } else Color.Unspecified,
+                )
+            }
+        )
+        LaunchedEffect(Unit) {
+            focusRequester.requestFocus()
+        }
+        OutlinedTextField(
+            modifier = Modifier
+                .padding(16.dp),
+            value = iban,
+            enabled = !state.showFees,
+            onValueChange = { input ->
+                iban = input
+            },
+            isError = iban.isBlank(),
+            label = {
+                Text(
+                    text = stringResource(R.string.send_deposit_iban),
+                    color = if (iban.isBlank()) {
+                        colorResource(R.color.red)
+                    } else Color.Unspecified,
+                )
+            }
+        )
+        OutlinedTextField(
+            modifier = Modifier
+                .padding(16.dp),
+            value = bic,
+            enabled = !state.showFees,
+            onValueChange = { input ->
+                bic = input
+            },
+            label = {
+                Text(
+                    text = stringResource(R.string.send_deposit_bic),
+                )
+            }
+        )
+        Text(
+            modifier = Modifier.padding(horizontal = 16.dp),
+            text = stringResource(id = R.string.amount_chosen),
+        )
+        Text(
+            modifier = Modifier.padding(16.dp),
+            fontSize = 24.sp,
+            color = colorResource(R.color.green),
+            text = amount.toString(),
+        )
+        AnimatedVisibility(visible = state.showFees) {
+            Column(
+                modifier = Modifier.fillMaxWidth(),
+                horizontalAlignment = Alignment.CenterHorizontally,
+            ) {
+                val effectiveAmount = state.effectiveDepositAmount
+                val fee = amount - (effectiveAmount ?: 
Amount.zero(amount.currency))
+                Text(
+                    modifier = Modifier.padding(horizontal = 16.dp),
+                    text = stringResource(id = R.string.withdraw_fees),
+                )
+                Text(
+                    modifier = Modifier.padding(16.dp),
+                    fontSize = 24.sp,
+                    color = colorResource(if (fee.isZero()) R.color.green else 
R.color.red),
+                    text = if (fee.isZero()) {
+                        fee.toString()
+                    } else {
+                        stringResource(R.string.amount_negative, 
fee.toString())
+                    },
+                )
+                Text(
+                    modifier = Modifier.padding(horizontal = 16.dp),
+                    text = stringResource(id = 
R.string.send_deposit_amount_effective),
+                )
+                Text(
+                    modifier = Modifier.padding(16.dp),
+                    fontSize = 24.sp,
+                    color = colorResource(R.color.green),
+                    text = effectiveAmount.toString(),
+                )
+            }
+        }
+        AnimatedVisibility(visible = state is DepositState.Error) {
+            Text(
+                modifier = Modifier.padding(16.dp),
+                fontSize = 18.sp,
+                color = colorResource(R.color.red),
+                text = (state as? DepositState.Error)?.msg ?: "",
+            )
+        }
+        val focusManager = LocalFocusManager.current
+        Button(
+            modifier = Modifier.padding(16.dp),
+            enabled = iban.isNotBlank(),
+            onClick = {
+                focusManager.clearFocus()
+                onMakeDeposit(amount, name, iban, bic)
+            },
+        ) {
+            Text(text = stringResource(
+                if (state.showFees) R.string.send_deposit_create_button
+                else R.string.send_deposit_check_fees_button
+            ))
+        }
+    }
+}
+
+@Preview
+@Composable
+fun PreviewMakeDepositComposable() {
+    Surface {
+        val state = DepositState.FeesChecked(
+            effectiveDepositAmount = Amount.fromDouble("TESTKUDOS", 42.00),
+        )
+        MakeDepositComposable(
+            state = state,
+            amount = Amount.fromDouble("TESTKUDOS", 42.23)) { _, _, _, _ ->
+        }
+    }
+}
diff --git a/cashier/src/main/java/net/taler/cashier/SignedAmount.kt 
b/wallet/src/main/java/net/taler/wallet/payment/DepositState.kt
similarity index 53%
copy from cashier/src/main/java/net/taler/cashier/SignedAmount.kt
copy to wallet/src/main/java/net/taler/wallet/payment/DepositState.kt
index 4f624ae..8598911 100644
--- a/cashier/src/main/java/net/taler/cashier/SignedAmount.kt
+++ b/wallet/src/main/java/net/taler/wallet/payment/DepositState.kt
@@ -1,6 +1,6 @@
 /*
  * This file is part of GNU Taler
- * (C) 2020 Taler Systems S.A.
+ * (C) 2022 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
@@ -14,17 +14,31 @@
  * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-package net.taler.cashier
+package net.taler.wallet.payment
 
 import net.taler.common.Amount
 
-data class SignedAmount(
-    val positive: Boolean,
-    val amount: Amount
-) {
+sealed class DepositState {
 
-    override fun toString(): String {
-        return if (positive) "$amount" else "-$amount"
+    open val showFees: Boolean = false
+    open val effectiveDepositAmount: Amount? = null
+
+    object Start : DepositState()
+    object CheckingFees : DepositState()
+    class FeesChecked(
+        override val effectiveDepositAmount: Amount,
+    ) : DepositState() {
+        override val showFees = true
+    }
+
+    class MakingDeposit(
+        override val effectiveDepositAmount: Amount,
+    ) : DepositState() {
+        override val showFees = true
     }
 
+    object Success : DepositState()
+
+    class Error(val msg: String) : DepositState()
+
 }
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 dfa14c2..74740ca 100644
--- a/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt
+++ b/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt
@@ -21,11 +21,14 @@ import androidx.annotation.UiThread
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.launch
 import kotlinx.serialization.Serializable
 import net.taler.common.Amount
 import net.taler.common.ContractTerms
 import net.taler.wallet.TAG
+import net.taler.wallet.accounts.PaytoUriIban
 import net.taler.wallet.backend.TalerErrorInfo
 import net.taler.wallet.backend.WalletBackendApi
 import net.taler.wallet.payment.PayStatus.AlreadyPaid
@@ -43,12 +46,12 @@ sealed class PayStatus {
         val contractTerms: ContractTerms,
         val proposalId: String,
         val amountRaw: Amount,
-        val amountEffective: Amount
+        val amountEffective: Amount,
     ) : PayStatus()
 
     data class InsufficientBalance(
         val contractTerms: ContractTerms,
-        val amountRaw: Amount
+        val amountRaw: Amount,
     ) : PayStatus()
 
     // TODO bring user to fulfilment URI
@@ -65,6 +68,9 @@ class PaymentManager(
     private val mPayStatus = MutableLiveData<PayStatus>(PayStatus.None)
     internal val payStatus: LiveData<PayStatus> = mPayStatus
 
+    private val mDepositState = 
MutableStateFlow<DepositState>(DepositState.Start)
+    internal val depositState = mDepositState.asStateFlow()
+
     @UiThread
     fun preparePay(url: String) = scope.launch {
         mPayStatus.value = PayStatus.Loading
@@ -120,28 +126,91 @@ class PaymentManager(
         mPayStatus.value = PayStatus.None
     }
 
+    private fun handleError(operation: String, error: TalerErrorInfo) {
+        Log.e(TAG, "got $operation error result $error")
+        mPayStatus.value = PayStatus.Error(error.userFacingMsg)
+    }
+
+    /* Deposits */
+
     @UiThread
-    fun makeDeposit(url: String, amount: Amount) = scope.launch {
-        // TODO
-        api.request("createDepositGroup", 
CreateDepositGroupResponse.serializer()) {
-            put("depositPaytoUri", url)
-            put("amount", amount.toJSONString())
-        }.onError {
-            Log.e(TAG, "Error createDepositGroup $it")
-        }.onSuccess {
-            Log.e(TAG, "createDepositGroup $it")
+    fun onDepositButtonClicked(amount: Amount, receiverName: String, iban: 
String, bic: String) {
+        val paytoUri: String = PaytoUriIban(
+            iban = iban,
+            bic = bic,
+            targetPath = "",
+            params = mapOf("receiver-name" to receiverName),
+        ).paytoUri
+
+        if (depositState.value.showFees) {
+            val effectiveDepositAmount = 
depositState.value.effectiveDepositAmount
+                ?: Amount.zero(amount.currency)
+            makeDeposit(paytoUri, amount, effectiveDepositAmount)
+        } else {
+            prepareDeposit(paytoUri, amount)
         }
     }
 
-    private fun handleError(operation: String, error: TalerErrorInfo) {
-        Log.e(TAG, "got $operation error result $error")
-        mPayStatus.value = PayStatus.Error(error.userFacingMsg)
+    private fun prepareDeposit(paytoUri: String, amount: Amount) {
+        mDepositState.value = DepositState.CheckingFees
+        scope.launch {
+            api.request("prepareDeposit", PrepareDepositResponse.serializer()) 
{
+                put("depositPaytoUri", paytoUri)
+                put("amount", amount.toJSONString())
+            }.onError {
+                Log.e(TAG, "Error prepareDeposit $it")
+                mDepositState.value = DepositState.Error(it.userFacingMsg)
+            }.onSuccess {
+                mDepositState.value = DepositState.FeesChecked(
+                    effectiveDepositAmount = it.effectiveDepositAmount.amount,
+                )
+            }
+        }
     }
 
+    private fun makeDeposit(
+        paytoUri: String,
+        amount: Amount,
+        effectiveDepositAmount: Amount,
+    ) {
+        mDepositState.value = 
DepositState.MakingDeposit(effectiveDepositAmount)
+        scope.launch {
+            api.request("createDepositGroup", 
CreateDepositGroupResponse.serializer()) {
+                put("depositPaytoUri", paytoUri)
+                put("amount", amount.toJSONString())
+            }.onError {
+                Log.e(TAG, "Error createDepositGroup $it")
+                mDepositState.value = DepositState.Error(it.userFacingMsg)
+            }.onSuccess {
+                mDepositState.value = DepositState.Success
+            }
+        }
+    }
+
+    @UiThread
+    fun resetDepositState() {
+        mDepositState.value = DepositState.Start
+    }
 }
 
+@Serializable
+data class PrepareDepositResponse(
+    val totalDepositCost: AmountJson,
+    val effectiveDepositAmount: AmountJson,
+)
+
 @Serializable
 data class CreateDepositGroupResponse(
     val depositGroupId: String,
     val transactionId: String,
 )
+
+@Serializable
+@Deprecated("no idea why this is now in the API")
+data class AmountJson(
+    val currency: String,
+    val value: Long,
+    val fraction: Int,
+) {
+    val amount = Amount(currency, value, fraction)
+}
diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt 
b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushFragment.kt
similarity index 73%
copy from wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt
copy to wallet/src/main/java/net/taler/wallet/peer/OutgoingPushFragment.kt
index 24bedc4..ae0ef10 100644
--- a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushFragment.kt
@@ -30,11 +30,9 @@ import net.taler.common.Amount
 import net.taler.wallet.MainViewModel
 import net.taler.wallet.R
 import net.taler.wallet.compose.collectAsStateLifecycleAware
-import net.taler.wallet.exchanges.ExchangeItem
 
-class OutgoingPullFragment : Fragment() {
+class OutgoingPushFragment : Fragment() {
     private val model: MainViewModel by activityViewModels()
-    private val exchangeManager get() = model.exchangeManager
     private val peerManager get() = model.peerManager
 
     override fun onCreateView(
@@ -45,22 +43,18 @@ class OutgoingPullFragment : Fragment() {
         val amount = arguments?.getString("amount")?.let {
             Amount.fromJSONString(it)
         } ?: error("no amount passed")
-        val exchangeFlow = 
exchangeManager.findExchangeForCurrency(amount.currency)
         return ComposeView(requireContext()).apply {
             setContent {
                 MdcTheme {
                     Surface {
-                        val state = 
peerManager.pullState.collectAsStateLifecycleAware()
+                        val state = 
peerManager.pushState.collectAsStateLifecycleAware()
                         if (state.value is OutgoingIntro) {
-                            val exchangeState =
-                                
exchangeFlow.collectAsStateLifecycleAware(initial = null)
-                            OutgoingPullIntroComposable(
+                            OutgoingPushIntroComposable(
                                 amount = amount,
-                                exchangeState = exchangeState,
-                                onCreateInvoice = 
this@OutgoingPullFragment::onCreateInvoice,
+                                onSend = this@OutgoingPushFragment::onSend,
                             )
                         } else {
-                            OutgoingPullResultComposable(state.value) {
+                            OutgoingPushResultComposable(state.value) {
                                 findNavController().popBackStack()
                             }
                         }
@@ -77,10 +71,10 @@ class OutgoingPullFragment : Fragment() {
 
     override fun onDestroy() {
         super.onDestroy()
-        if (!requireActivity().isChangingConfigurations) 
peerManager.resetPullPayment()
+        if (!requireActivity().isChangingConfigurations) 
peerManager.resetPushPayment()
     }
 
-    private fun onCreateInvoice(amount: Amount, summary: String, exchange: 
ExchangeItem) {
-        peerManager.initiatePullPayment(amount, summary, exchange)
+    private fun onSend(amount: Amount, summary: String) {
+        peerManager.initiatePeerPushPayment(amount, summary)
     }
 }
diff --git 
a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushIntroComposable.kt 
b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushIntroComposable.kt
index 72c8862..1964ebd 100644
--- a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushIntroComposable.kt
+++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushIntroComposable.kt
@@ -21,7 +21,6 @@ import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxWidth
 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.material.Button
 import androidx.compose.material.MaterialTheme
@@ -39,16 +38,14 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.res.colorResource
 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 net.taler.common.Amount
 import net.taler.wallet.R
-import net.taler.wallet.getAmount
 
 @Composable
 fun OutgoingPushIntroComposable(
-    currency: String,
+    amount: Amount,
     onSend: (amount: Amount, summary: String) -> Unit,
 ) {
     val scrollState = rememberScrollState()
@@ -58,38 +55,14 @@ fun OutgoingPushIntroComposable(
             .verticalScroll(scrollState),
         horizontalAlignment = CenterHorizontally,
     ) {
-        var amountText by rememberSaveable { mutableStateOf("") }
-        var isError by rememberSaveable { mutableStateOf(false) }
         Row(
             verticalAlignment = Alignment.CenterVertically,
             modifier = Modifier
                 .padding(16.dp),
         ) {
-            OutlinedTextField(
-                modifier = Modifier
-                    .weight(1f)
-                    .padding(end = 16.dp),
-                value = amountText,
-                keyboardOptions = KeyboardOptions.Default.copy(keyboardType = 
KeyboardType.Decimal),
-                onValueChange = { input ->
-                    isError = false
-                    amountText = input.filter { it.isDigit() || it == '.' }
-                },
-                isError = isError,
-                label = {
-                    if (isError) {
-                        Text(
-                            stringResource(R.string.receive_amount_invalid),
-                            color = Color.Red,
-                        )
-                    } else {
-                        Text(stringResource(R.string.send_peer_amount))
-                    }
-                }
-            )
             Text(
                 modifier = Modifier,
-                text = currency,
+                text = amount.toString(),
                 softWrap = false,
                 style = MaterialTheme.typography.h6,
             )
@@ -118,11 +91,9 @@ fun OutgoingPushIntroComposable(
         )
         Button(
             modifier = Modifier.padding(16.dp),
-            enabled = subject.isNotBlank() && amountText.isNotBlank(),
+            enabled = subject.isNotBlank(),
             onClick = {
-                val amount = getAmount(currency, amountText)
-                if (amount == null) isError = true
-                else onSend(amount, subject)
+                onSend(amount, subject)
             },
         ) {
             Text(text = stringResource(R.string.send_peer_create_button))
@@ -134,6 +105,6 @@ fun OutgoingPushIntroComposable(
 @Composable
 fun PeerPushIntroComposablePreview() {
     Surface {
-        OutgoingPushIntroComposable("TESTKUDOS") { _, _ -> }
+        OutgoingPushIntroComposable(Amount.fromDouble("TESTKUDOS", 42.23)) { 
_, _ -> }
     }
 }
diff --git a/wallet/src/main/res/navigation/nav_graph.xml 
b/wallet/src/main/res/navigation/nav_graph.xml
index 96ca49f..6feb846 100644
--- a/wallet/src/main/res/navigation/nav_graph.xml
+++ b/wallet/src/main/res/navigation/nav_graph.xml
@@ -48,7 +48,14 @@
     <fragment
         android:id="@+id/sendFunds"
         android:name="net.taler.wallet.SendFundsFragment"
-        android:label="@string/transactions_send_funds" />
+        android:label="@string/transactions_send_funds">
+        <action
+            android:id="@+id/action_sendFunds_to_nav_deposit"
+            app:destination="@id/nav_deposit" />
+        <action
+            android:id="@+id/action_sendFunds_to_nav_peer_push"
+            app:destination="@id/nav_peer_push" />
+    </fragment>
 
     <fragment
         android:id="@+id/promptTip"
@@ -124,6 +131,11 @@
             app:popUpTo="@id/nav_main" />
     </fragment>
 
+    <fragment
+        android:id="@+id/nav_deposit"
+        android:name="net.taler.wallet.payment.DepositFragment"
+        android:label="@string/send_deposit_title" />
+
     <fragment
         android:id="@+id/nav_settings_backup"
         android:name="net.taler.wallet.settings.BackupSettingsFragment"
@@ -140,6 +152,17 @@
             app:nullable="true" />
     </fragment>
 
+    <fragment
+        android:id="@+id/nav_peer_push"
+        android:name="net.taler.wallet.peer.OutgoingPushFragment"
+        android:label="@string/send_peer_title">
+        <argument
+            android:name="amount"
+            android:defaultValue="@null"
+            app:argType="string"
+            app:nullable="true" />
+    </fragment>
+
     <fragment
         android:id="@+id/promptPullPayment"
         android:name="net.taler.wallet.peer.IncomingPullPaymentFragment"
diff --git a/wallet/src/main/res/values/strings.xml 
b/wallet/src/main/res/values/strings.xml
index f72b345..2b81894 100644
--- a/wallet/src/main/res/values/strings.xml
+++ b/wallet/src/main/res/values/strings.xml
@@ -125,7 +125,18 @@ GNU Taler is immune against many types of fraud, such as 
phishing of credit card
     <string name="receive_peer_invoice_instruction">Let the payer scan this QR 
code to pay:</string>
     <string name="receive_peer_invoice_uri">Alternatively, copy and send this 
URI:</string>
 
-    <string name="send_peer_amount">Amount to send</string>
+    <string name="send_amount">Amount to send</string>
+    <string name="send_intro">Choose where to send money to:</string>
+    <string name="send_deposit">To a bank account</string>
+    <string name="send_deposit_title">Deposit to a bank account</string>
+    <string name="send_deposit_iban">IBAN</string>
+    <string name="send_deposit_bic">BIC/SWIFT</string>
+    <string name="send_deposit_name">Account holder</string>
+    <string name="send_deposit_check_fees_button">Check fees</string>
+    <string name="send_deposit_amount_effective">Effective Amount</string>
+    <string name="send_deposit_create_button">Make deposit</string>
+    <string name="send_peer">To another wallet</string>
+    <string name="send_peer_title">Send money to another wallet</string>
     <string name="send_peer_create_button">Send funds now</string>
     <string name="send_peer_warning">Warning: Funds will leave the wallet 
immediately.</string>
     <string name="send_peer_payment_instruction">Let the payee scan this QR 
code to receive:</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]