[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.
- [taler-taler-android] branch master updated (2398d0f -> 431e326),
gnunet <=