[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-taler-android] branch master updated (699367b -> 4fe0476)
From: |
gnunet |
Subject: |
[taler-taler-android] branch master updated (699367b -> 4fe0476) |
Date: |
Tue, 06 Sep 2022 23:19:11 +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 699367b [wallet] Fix lint error
new 5a0d679 [wallet] Upgrade to core v0.9.0-dev.16
new 3d3108d [wallet] Improve rendering of transaction list error message
new 4fe0476 [wallet] implement prototype for outgoing peer transactions
The 3 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:
wallet/build.gradle | 4 +-
.../main/java/net/taler/wallet/MainViewModel.kt | 2 +
.../java/net/taler/wallet/ReceiveFundsFragment.kt | 198 +++++++++++++++++++++
.../java/net/taler/wallet/SendFundsFragment.kt | 75 ++++++++
wallet/src/main/java/net/taler/wallet/Utils.kt | 10 ++
.../taler/wallet/compose/QrCodeUriComposable.kt | 124 +++++++++++++
.../main/java/net/taler/wallet/compose/Utils.kt | 53 ++++++
.../net/taler/wallet/exchanges/ExchangeManager.kt | 16 +-
.../main/java/net/taler/wallet/peer/PeerManager.kt | 117 ++++++++++++
.../net/taler/wallet/peer/PeerPullComposable.kt | 129 ++++++++++++++
.../java/net/taler/wallet/peer/PeerPullFragment.kt | 86 +++++++++
.../taler/wallet/peer/PeerPullResultComposable.kt | 185 +++++++++++++++++++
.../net/taler/wallet/peer/PeerPushComposable.kt | 139 +++++++++++++++
.../taler/wallet/peer/PeerPushResultComposable.kt | 185 +++++++++++++++++++
.../taler/wallet/peer/TransactionPeerPullCredit.kt | 98 ++++++++++
.../taler/wallet/peer/TransactionPeerPushDebit.kt | 96 ++++++++++
.../wallet/transactions/TransactionPeerFragment.kt | 148 +++++++++++++++
.../net/taler/wallet/transactions/Transactions.kt | 113 ++++++++++++
.../wallet/transactions/TransactionsFragment.kt | 6 +
.../withdraw/manual/ManualWithdrawFragment.kt | 5 +
.../manual/ManualWithdrawSuccessFragment.kt | 10 --
.../taler/wallet/withdraw/manual/ScreenBitcoin.kt | 4 +-
.../net/taler/wallet/withdraw/manual/ScreenIBAN.kt | 1 +
.../src/main/res/layout/fragment_transactions.xml | 5 +-
wallet/src/main/res/navigation/nav_graph.xml | 48 ++++-
wallet/src/main/res/values/strings.xml | 18 ++
26 files changed, 1858 insertions(+), 17 deletions(-)
create mode 100644
wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt
create mode 100644 wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt
create mode 100644
wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt
create mode 100644 wallet/src/main/java/net/taler/wallet/compose/Utils.kt
create mode 100644 wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt
create mode 100644
wallet/src/main/java/net/taler/wallet/peer/PeerPullComposable.kt
create mode 100644
wallet/src/main/java/net/taler/wallet/peer/PeerPullFragment.kt
create mode 100644
wallet/src/main/java/net/taler/wallet/peer/PeerPullResultComposable.kt
create mode 100644
wallet/src/main/java/net/taler/wallet/peer/PeerPushComposable.kt
create mode 100644
wallet/src/main/java/net/taler/wallet/peer/PeerPushResultComposable.kt
create mode 100644
wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullCredit.kt
create mode 100644
wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushDebit.kt
create mode 100644
wallet/src/main/java/net/taler/wallet/transactions/TransactionPeerFragment.kt
diff --git a/wallet/build.gradle b/wallet/build.gradle
index f9397ba..b80bfe4 100644
--- a/wallet/build.gradle
+++ b/wallet/build.gradle
@@ -23,8 +23,8 @@ plugins {
id "de.undercouch.download"
}
-def walletCoreVersion = "v0.9.0-dev.12"
-def walletCoreSha256 =
"647a82384a57ec23777439259b030da17101a8e754a85bd6c4b32e60a4d158ef"
+def walletCoreVersion = "v0.9.0-dev.16"
+def walletCoreSha256 =
"541c417bcbd282a65159a32fb0b9f5702f83f2d2222bee3619e60af92a1b6781"
static def versionCodeEpoch() {
return (new Date().getTime() / 1000).toInteger()
diff --git a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
index 92113aa..99ac1f9 100644
--- a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
+++ b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
@@ -34,6 +34,7 @@ import net.taler.wallet.balances.BalanceItem
import net.taler.wallet.balances.BalanceResponse
import net.taler.wallet.exchanges.ExchangeManager
import net.taler.wallet.payment.PaymentManager
+import net.taler.wallet.peer.PeerManager
import net.taler.wallet.pending.PendingOperationsManager
import net.taler.wallet.refund.RefundManager
import net.taler.wallet.tip.TipManager
@@ -93,6 +94,7 @@ class MainViewModel(val app: Application) :
AndroidViewModel(app) {
val transactionManager: TransactionManager = TransactionManager(api,
viewModelScope)
val refundManager = RefundManager(api, viewModelScope)
val exchangeManager: ExchangeManager = ExchangeManager(api, viewModelScope)
+ val peerManager: PeerManager = PeerManager(api, viewModelScope)
private val mTransactionsEvent = MutableLiveData<Event<String>>()
val transactionsEvent: LiveData<Event<String>> = mTransactionsEvent
diff --git a/wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt
b/wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt
new file mode 100644
index 0000000..31228a4
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt
@@ -0,0 +1,198 @@
+/*
+ * 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
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+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.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
+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.Companion.Decimal
+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.lifecycle.lifecycleScope
+import androidx.navigation.fragment.findNavController
+import com.google.android.material.composethemeadapter.MdcTheme
+import net.taler.common.Amount
+import net.taler.wallet.exchanges.ExchangeItem
+
+class ReceiveFundsFragment : Fragment() {
+ private val model: MainViewModel by activityViewModels()
+ private val exchangeManager get() = model.exchangeManager
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View = ComposeView(requireContext()).apply {
+ setContent {
+ MdcTheme {
+ Surface {
+ ReceiveFundsIntro(
+ model.transactionManager.selectedCurrency ?: error("No
currency selected"),
+ this@ReceiveFundsFragment::onManualWithdraw,
+ this@ReceiveFundsFragment::onPeerPull,
+ )
+ }
+ }
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ activity?.setTitle(R.string.transactions_receive_funds)
+ }
+
+ private fun onManualWithdraw(amount: Amount) {
+ // TODO give some UI feedback while we wait for exchanges to load
(quick enough for now)
+ lifecycleScope.launchWhenResumed {
+ // we need to set the exchange first, we want to withdraw from
+ exchangeManager.findExchangeForCurrency(amount.currency).collect {
exchange ->
+ onExchangeRetrieved(exchange, amount)
+ }
+ }
+ }
+
+ private fun onExchangeRetrieved(exchange: ExchangeItem?, amount: Amount) {
+ if (exchange == null) {
+ Toast.makeText(requireContext(), "No exchange available",
LENGTH_LONG).show()
+ return
+ }
+ exchangeManager.withdrawalExchange = exchange
+ // now that we have the exchange, we can navigate
+ val bundle = bundleOf("amount" to amount.toJSONString())
+ findNavController().navigate(
+ R.id.action_receiveFunds_to_nav_exchange_manual_withdrawal, bundle)
+ }
+
+ private fun onPeerPull(amount: Amount) {
+ val bundle = bundleOf("amount" to amount.toJSONString())
+
findNavController().navigate(R.id.action_receiveFunds_to_nav_peer_pull, bundle)
+ }
+}
+
+@Composable
+private fun ReceiveFundsIntro(
+ currency: String,
+ onManualWithdraw: (Amount) -> Unit,
+ onPeerPull: (Amount) -> Unit,
+) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(scrollState),
+ ) {
+ var text 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 = text,
+ keyboardOptions = KeyboardOptions.Default.copy(keyboardType =
Decimal),
+ onValueChange = { input ->
+ isError = false
+ text = 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.receive_amount))
+ }
+ }
+ )
+ Text(
+ modifier = Modifier,
+ text = currency,
+ softWrap = false,
+ style = MaterialTheme.typography.h6,
+ )
+ }
+ Text(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ text = stringResource(R.string.receive_intro),
+ style = MaterialTheme.typography.h6,
+ )
+ Row(modifier = Modifier.padding(16.dp)) {
+ Button(
+ modifier = Modifier
+ .padding(end = 16.dp)
+ .weight(1f),
+ onClick = {
+ val amount = getAmount(currency, text)
+ if (amount == null) isError = true
+ else onManualWithdraw(amount)
+ }) {
+ Text(text = stringResource(R.string.receive_withdraw))
+ }
+ Button(
+ modifier = Modifier.weight(1f),
+ onClick = {
+ val amount = getAmount(currency, text)
+ if (amount == null) isError = true
+ else onPeerPull(amount)
+ },
+ ) {
+ Text(text = stringResource(R.string.receive_peer))
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+fun PreviewReceiveFundsIntro() {
+ Surface {
+ ReceiveFundsIntro("TESTKUDOS", {}) {}
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt
b/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt
new file mode 100644
index 0000000..c67b345
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt
@@ -0,0 +1,75 @@
+/*
+ * 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
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.material.Surface
+import androidx.compose.ui.platform.ComposeView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.navigation.findNavController
+import com.google.android.material.composethemeadapter.MdcTheme
+import net.taler.common.Amount
+import net.taler.wallet.compose.collectAsStateLifecycleAware
+import net.taler.wallet.peer.PeerPaymentIntro
+import net.taler.wallet.peer.PeerPushIntroComposable
+import net.taler.wallet.peer.PeerPushResultComposable
+
+class SendFundsFragment : Fragment() {
+ private val model: MainViewModel by activityViewModels()
+ private val transactionManager get() = model.transactionManager
+ private val peerManager get() = model.peerManager
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View = ComposeView(requireContext()).apply {
+ setContent {
+ MdcTheme {
+ Surface {
+ val state =
peerManager.pushState.collectAsStateLifecycleAware()
+ if (state.value is PeerPaymentIntro) {
+ val currency = transactionManager.selectedCurrency
+ ?: error("No currency selected")
+ PeerPushIntroComposable(currency,
this@SendFundsFragment::onSend)
+ } else {
+ PeerPushResultComposable(state.value) {
+ findNavController().popBackStack()
+ }
+ }
+ }
+ }
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ activity?.setTitle(R.string.transactions_send_funds)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ if (!requireActivity().isChangingConfigurations)
peerManager.resetPushPayment()
+ }
+
+ private fun onSend(amount: Amount, summary: String) {
+ peerManager.initiatePeerPushPayment(amount, summary)
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/Utils.kt
b/wallet/src/main/java/net/taler/wallet/Utils.kt
index 1b5af64..67bc72a 100644
--- a/wallet/src/main/java/net/taler/wallet/Utils.kt
+++ b/wallet/src/main/java/net/taler/wallet/Utils.kt
@@ -29,6 +29,8 @@ import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
+import net.taler.common.Amount
+import net.taler.common.AmountParserException
fun connectToWifi(context: Context, ssid: String) {
if (SDK_INT >= 29) {
@@ -84,3 +86,11 @@ private fun connectToWifiDeprecated(context: Context, ssid:
String) {
fun cleanExchange(exchange: String) = exchange.let {
if (it.startsWith("https://")) it.substring(8) else it
}.trimEnd('/')
+
+fun getAmount(currency: String, text: String): Amount? {
+ return try {
+ Amount.fromString(currency, text)
+ } catch (e: AmountParserException) {
+ null
+ }
+}
diff --git
a/wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt
b/wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt
new file mode 100644
index 0000000..3f8ecd1
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.compose
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.material.Button
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ContentCopy
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.produceState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.min
+import androidx.core.content.getSystemService
+import net.taler.common.QrCodeManager
+import net.taler.wallet.R
+
+@Composable
+fun ColumnScope.QrCodeUriComposable(
+ talerUri: String,
+ clipBoardLabel: String,
+ buttonText: String = stringResource(R.string.copy),
+ inBetween: (@Composable ColumnScope.() -> Unit)? = null,
+) {
+ val qrCodeSize = getQrCodeSize()
+ val qrState = produceState<ImageBitmap?>(null) {
+ value = QrCodeManager.makeQrCode(talerUri,
qrCodeSize.value.toInt()).asImageBitmap()
+ }
+ qrState.value?.let { qrCode ->
+ Image(
+ modifier = Modifier.size(qrCodeSize),
+ bitmap = qrCode,
+ contentDescription = stringResource(id =
R.string.button_scan_qr_code),
+ )
+ }
+ if (inBetween != null) inBetween()
+ val scrollState = rememberScrollState()
+ Box(modifier = Modifier.padding(16.dp)) {
+ Text(
+ modifier = Modifier.horizontalScroll(scrollState),
+ fontFamily = FontFamily.Monospace,
+ style = MaterialTheme.typography.body1,
+ text = talerUri,
+ )
+ }
+ CopyToClipboardButton(
+ modifier = Modifier,
+ label = clipBoardLabel,
+ content = talerUri,
+ buttonText = buttonText,
+ )
+}
+
+@Composable
+fun getQrCodeSize(): Dp {
+ val configuration = LocalConfiguration.current
+ val screenHeight = configuration.screenHeightDp.dp
+ val screenWidth = configuration.screenWidthDp.dp
+ return min(screenHeight, screenWidth)
+}
+
+@Composable
+fun CopyToClipboardButton(
+ label: String,
+ content: String,
+ modifier: Modifier = Modifier,
+ buttonText: String = stringResource(R.string.copy),
+) {
+ val context = LocalContext.current
+ Button(
+ modifier = modifier,
+ onClick = { copyToClipBoard(context, label, content) },
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(Icons.Default.ContentCopy, stringResource(R.string.copy))
+ Text(
+ modifier = Modifier.padding(start = 8.dp),
+ text = buttonText,
+ style = MaterialTheme.typography.body1,
+ )
+ }
+ }
+}
+
+fun copyToClipBoard(context: Context, label: String, str: String) {
+ val clipboard = context.getSystemService<ClipboardManager>()
+ val clip = ClipData.newPlainText(label, str)
+ clipboard?.setPrimaryClip(clip)
+}
diff --git a/wallet/src/main/java/net/taler/wallet/compose/Utils.kt
b/wallet/src/main/java/net/taler/wallet/compose/Utils.kt
new file mode 100644
index 0000000..21b04ed
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/compose/Utils.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.compose
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.flowWithLifecycle
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+
+@Composable
+fun <T> rememberFlow(
+ flow: Flow<T>,
+ lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
+): Flow<T> = remember(key1 = flow, key2 = lifecycleOwner) {
+ flow.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED)
+}
+
+@Composable
+fun <T : R, R> Flow<T>.collectAsStateLifecycleAware(
+ initial: R,
+ context: CoroutineContext = EmptyCoroutineContext,
+): State<R> {
+ val lifecycleAwareFlow = rememberFlow(flow = this)
+ return lifecycleAwareFlow.collectAsState(initial = initial, context =
context)
+}
+
+@Suppress("StateFlowValueCalledInComposition")
+@Composable
+fun <T> StateFlow<T>.collectAsStateLifecycleAware(
+ context: CoroutineContext = EmptyCoroutineContext,
+): State<T> = collectAsStateLifecycleAware(initial = value, context = context)
diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt
b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt
index 8205eb7..36b5017 100644
--- a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt
+++ b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt
@@ -20,6 +20,8 @@ import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import net.taler.common.Event
@@ -29,12 +31,12 @@ import net.taler.wallet.backend.WalletBackendApi
@Serializable
data class ExchangeListResponse(
- val exchanges: List<ExchangeItem>
+ val exchanges: List<ExchangeItem>,
)
class ExchangeManager(
private val api: WalletBackendApi,
- private val scope: CoroutineScope
+ private val scope: CoroutineScope,
) {
private val mProgress = MutableLiveData<Boolean>()
@@ -78,4 +80,14 @@ class ExchangeManager(
}
}
+ fun findExchangeForCurrency(currency: String): Flow<ExchangeItem?> = flow {
+ val response = api.request("listExchanges",
ExchangeListResponse.serializer())
+ var exchange: ExchangeItem? = null
+ response.onSuccess { exchangeListResponse ->
+ // just pick the first for now
+ exchange = exchangeListResponse.exchanges.find { it.currency ==
currency }
+ }
+ emit(exchange)
+ }
+
}
diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt
b/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt
new file mode 100644
index 0000000..898dcfd
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt
@@ -0,0 +1,117 @@
+/*
+ * 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.peer
+
+import android.graphics.Bitmap
+import android.util.Log
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import kotlinx.serialization.Serializable
+import net.taler.common.Amount
+import net.taler.common.QrCodeManager
+import net.taler.wallet.TAG
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.backend.WalletBackendApi
+import net.taler.wallet.exchanges.ExchangeItem
+import org.json.JSONObject
+
+class PeerManager(
+ private val api: WalletBackendApi,
+ private val scope: CoroutineScope,
+) {
+
+ private val _pullState =
MutableStateFlow<PeerPaymentState>(PeerPaymentIntro)
+ val pullState: StateFlow<PeerPaymentState> = _pullState
+
+ private val _pushState =
MutableStateFlow<PeerPaymentState>(PeerPaymentIntro)
+ val pushState: StateFlow<PeerPaymentState> = _pushState
+
+ fun initiatePullPayment(amount: Amount, exchange: ExchangeItem) {
+ _pullState.value = PeerPaymentCreating
+ scope.launch(Dispatchers.IO) {
+ api.request("initiatePeerPullPayment",
InitiatePeerPullPaymentResponse.serializer()) {
+ put("exchangeBaseUrl", exchange.exchangeBaseUrl)
+ put("amount", amount.toJSONString())
+ put("partialContractTerms", JSONObject().apply {
+ put("summary", "test")
+ })
+ }.onSuccess {
+ val qrCode = QrCodeManager.makeQrCode(it.talerUri)
+ _pullState.value = PeerPaymentResponse(it.talerUri, qrCode)
+ }.onError { error ->
+ Log.e(TAG, "got initiatePeerPullPayment error result $error")
+ _pullState.value = PeerPaymentError(error)
+ }
+ }
+ }
+
+ fun resetPullPayment() {
+ _pullState.value = PeerPaymentIntro
+ }
+
+ fun initiatePeerPushPayment(amount: Amount, summary: String) {
+ _pushState.value = PeerPaymentCreating
+ scope.launch(Dispatchers.IO) {
+ api.request("initiatePeerPushPayment",
InitiatePeerPushPaymentResponse.serializer()) {
+ put("amount", amount.toJSONString())
+ put("partialContractTerms", JSONObject().apply {
+ put("summary", summary)
+ })
+ }.onSuccess { response ->
+ val qrCode = QrCodeManager.makeQrCode(response.talerUri)
+ _pushState.value = PeerPaymentResponse(response.talerUri,
qrCode)
+ }.onError { error ->
+ Log.e(TAG, "got initiatePeerPushPayment error result $error")
+ _pushState.value = PeerPaymentError(error)
+ }
+ }
+ }
+
+ fun resetPushPayment() {
+ _pushState.value = PeerPaymentIntro
+ }
+
+}
+
+sealed class PeerPaymentState
+object PeerPaymentIntro : PeerPaymentState()
+object PeerPaymentCreating : PeerPaymentState()
+data class PeerPaymentResponse(
+ val talerUri: String,
+ val qrCode: Bitmap,
+) : PeerPaymentState()
+
+data class PeerPaymentError(
+ val info: TalerErrorInfo,
+) : PeerPaymentState()
+
+@Serializable
+data class InitiatePeerPullPaymentResponse(
+ /**
+ * Taler URI for the other party to make the payment that was requested.
+ */
+ val talerUri: String,
+)
+
+@Serializable
+data class InitiatePeerPushPaymentResponse(
+ val exchangeBaseUrl: String,
+ val talerUri: String,
+)
diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerPullComposable.kt
b/wallet/src/main/java/net/taler/wallet/peer/PeerPullComposable.kt
new file mode 100644
index 0000000..02f2c7c
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/peer/PeerPullComposable.kt
@@ -0,0 +1,129 @@
+/*
+ * 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.peer
+
+import android.annotation.SuppressLint
+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.State
+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.Companion.CenterHorizontally
+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.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 net.taler.common.Amount
+import net.taler.wallet.R
+import net.taler.wallet.cleanExchange
+import net.taler.wallet.exchanges.ExchangeItem
+
+@Composable
+fun PeerPullIntroComposable(
+ amount: Amount,
+ exchangeState: State<ExchangeItem?>,
+ onCreateInvoice: (amount: Amount, exchange: ExchangeItem) -> Unit,
+) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(scrollState),
+ horizontalAlignment = CenterHorizontally,
+ ) {
+ var subject by rememberSaveable { mutableStateOf("") }
+ val focusRequester = remember { FocusRequester() }
+ val exchangeItem = exchangeState.value
+ OutlinedTextField(
+ modifier = Modifier
+ .padding(16.dp)
+ .focusRequester(focusRequester),
+ value = subject,
+ onValueChange = { input ->
+ subject = input
+ },
+ isError = subject.isBlank(),
+ label = {
+ Text(
+ stringResource(R.string.withdraw_manual_ready_subject),
+ color = if (subject.isBlank()) {
+ colorResource(R.color.red)
+ } else Color.Unspecified,
+ )
+ }
+ )
+ LaunchedEffect(Unit) {
+ focusRequester.requestFocus()
+ }
+ 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(),
+ )
+ Text(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ text = stringResource(R.string.withdraw_exchange),
+ )
+ Text(
+ modifier = Modifier.padding(16.dp),
+ fontSize = 24.sp,
+ text = if (exchangeItem == null) "" else
cleanExchange(exchangeItem.exchangeBaseUrl),
+ )
+ Button(
+ modifier = Modifier.padding(16.dp),
+ enabled = subject.isNotBlank() && exchangeItem != null,
+ onClick = {
+ onCreateInvoice(amount, exchangeItem ?: error("clickable
without exchange"))
+ },
+ ) {
+ Text(text = stringResource(R.string.receive_peer_create_button))
+ }
+ }
+}
+
+@Preview
+@Composable
+fun PreviewReceiveFundsIntro() {
+ Surface {
+ @SuppressLint("UnrememberedMutableState")
+ val exchangeFlow =
+ mutableStateOf(ExchangeItem("https://example.org", "TESTKUDOS",
emptyList()))
+ PeerPullIntroComposable(Amount.fromDouble("TESTKUDOS", 42.23),
exchangeFlow) { _, _ -> }
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerPullFragment.kt
b/wallet/src/main/java/net/taler/wallet/peer/PeerPullFragment.kt
new file mode 100644
index 0000000..d38ae34
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/peer/PeerPullFragment.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.peer
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.material.Surface
+import androidx.compose.ui.platform.ComposeView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.navigation.findNavController
+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
+import net.taler.wallet.exchanges.ExchangeItem
+
+class PeerPullFragment : Fragment() {
+ private val model: MainViewModel by activityViewModels()
+ private val exchangeManager get() = model.exchangeManager
+ private val peerManager get() = model.peerManager
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View {
+ 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()
+ if (state.value is PeerPaymentIntro) {
+ val exchangeState =
+
exchangeFlow.collectAsStateLifecycleAware(initial = null)
+ PeerPullIntroComposable(
+ amount = amount,
+ exchangeState = exchangeState,
+ onCreateInvoice =
this@PeerPullFragment::onCreateInvoice,
+ )
+ } else {
+ PeerPullResultComposable(state.value) {
+ findNavController().popBackStack()
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ activity?.setTitle(R.string.receive_peer_title)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ if (!requireActivity().isChangingConfigurations)
peerManager.resetPullPayment()
+ }
+
+ private fun onCreateInvoice(amount: Amount, exchange: ExchangeItem) {
+ peerManager.initiatePullPayment(amount, exchange)
+ }
+}
diff --git
a/wallet/src/main/java/net/taler/wallet/peer/PeerPullResultComposable.kt
b/wallet/src/main/java/net/taler/wallet/peer/PeerPullResultComposable.kt
new file mode 100644
index 0000000..0b9b546
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/peer/PeerPullResultComposable.kt
@@ -0,0 +1,185 @@
+/*
+ * 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.peer
+
+import android.content.res.Configuration.UI_MODE_NIGHT_YES
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Button
+import androidx.compose.material.CircularProgressIndicator
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ContentCopy
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.taler.common.QrCodeManager
+import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.compose.copyToClipBoard
+import net.taler.wallet.compose.getQrCodeSize
+import org.json.JSONObject
+
+@Composable
+fun PeerPullResultComposable(state: PeerPaymentState, onClose: () -> Unit) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(scrollState),
+ ) {
+ Text(
+ modifier = Modifier.padding(top = 16.dp, start = 16.dp, end =
16.dp),
+ style = MaterialTheme.typography.h6,
+ text = stringResource(id =
R.string.receive_peer_invoice_instruction),
+ )
+ when (state) {
+ PeerPaymentIntro -> error("Result composable with
PullPaymentIntro")
+ is PeerPaymentCreating -> PeerPullCreatingComposable()
+ is PeerPaymentResponse -> PeerPullResponseComposable(state)
+ is PeerPaymentError -> PeerPullErrorComposable(state)
+ }
+ Button(modifier = Modifier
+ .padding(16.dp)
+ .align(CenterHorizontally),
+ onClick = onClose) {
+ Text(text = stringResource(R.string.close))
+ }
+ }
+}
+
+@Composable
+private fun ColumnScope.PeerPullCreatingComposable() {
+ val qrCodeSize = getQrCodeSize()
+ CircularProgressIndicator(
+ modifier = Modifier
+ .padding(32.dp)
+ .size(qrCodeSize)
+ .align(CenterHorizontally),
+ )
+}
+
+@Composable
+private fun ColumnScope.PeerPullResponseComposable(state: PeerPaymentResponse)
{
+ val qrCodeSize = getQrCodeSize()
+ Image(
+ modifier = Modifier
+ .size(qrCodeSize)
+ .align(CenterHorizontally),
+ bitmap = state.qrCode.asImageBitmap(),
+ contentDescription = stringResource(id = R.string.button_scan_qr_code),
+ )
+ Text(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ style = MaterialTheme.typography.body1,
+ text = stringResource(id = R.string.receive_peer_invoice_uri),
+ )
+ val scrollState = rememberScrollState()
+ Text(
+ modifier = Modifier
+ .horizontalScroll(scrollState)
+ .padding(16.dp),
+ fontFamily = FontFamily.Monospace,
+ style = MaterialTheme.typography.body1,
+ text = state.talerUri,
+ )
+ val context = LocalContext.current
+ IconButton(
+ modifier = Modifier
+ .align(CenterHorizontally),
+ onClick = { copyToClipBoard(context, "Invoice", state.talerUri) },
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(Icons.Default.ContentCopy, stringResource(R.string.copy))
+ Text(
+ modifier = Modifier.padding(start = 8.dp),
+ text = stringResource(R.string.copy),
+ style = MaterialTheme.typography.body1,
+ )
+ }
+ }
+}
+
+@Composable
+private fun ColumnScope.PeerPullErrorComposable(state: PeerPaymentError) {
+ Text(
+ modifier = Modifier
+ .align(CenterHorizontally)
+ .padding(16.dp),
+ color = colorResource(R.color.red),
+ style = MaterialTheme.typography.body1,
+ text = state.info.userFacingMsg,
+ )
+}
+
+@Preview
+@Composable
+fun PeerPullCreatingPreview() {
+ Surface {
+ PeerPullResultComposable(PeerPaymentCreating) {}
+ }
+}
+
+@Preview
+@Composable
+fun PeerPullResponsePreview() {
+ Surface {
+ val talerUri =
"https://example.org/foo/bar/can/be/very/long/url/so/fit/it/on/screen"
+ val response = PeerPaymentResponse(talerUri,
QrCodeManager.makeQrCode(talerUri))
+ PeerPullResultComposable(response) {}
+ }
+}
+
+@Preview(widthDp = 720, uiMode = UI_MODE_NIGHT_YES)
+@Composable
+fun PeerPullResponseLandscapePreview() {
+ Surface {
+ val talerUri =
"https://example.org/foo/bar/can/be/very/long/url/so/fit/it/on/screen"
+ val response = PeerPaymentResponse(talerUri,
QrCodeManager.makeQrCode(talerUri))
+ PeerPullResultComposable(response) {}
+ }
+}
+
+@Preview
+@Composable
+fun PeerPullErrorPreview() {
+ Surface {
+ val json = JSONObject().apply { put("foo", "bar") }
+ val response = PeerPaymentError(TalerErrorInfo(42, "hint", "message",
json))
+ PeerPullResultComposable(response) {}
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerPushComposable.kt
b/wallet/src/main/java/net/taler/wallet/peer/PeerPushComposable.kt
new file mode 100644
index 0000000..1399fbb
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/peer/PeerPushComposable.kt
@@ -0,0 +1,139 @@
+/*
+ * 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.peer
+
+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.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.Alignment.Companion.CenterHorizontally
+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 PeerPushIntroComposable(
+ currency: String,
+ onSend: (amount: Amount, summary: String) -> Unit,
+) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .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,
+ softWrap = false,
+ style = MaterialTheme.typography.h6,
+ )
+ }
+
+ var subject by rememberSaveable { mutableStateOf("") }
+ OutlinedTextField(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ value = subject,
+ onValueChange = { input ->
+ subject = input
+ },
+ isError = subject.isBlank(),
+ label = {
+ Text(
+ stringResource(R.string.withdraw_manual_ready_subject),
+ color = if (subject.isBlank()) {
+ colorResource(R.color.red)
+ } else Color.Unspecified,
+ )
+ }
+ )
+ Text(
+ modifier = Modifier.padding(top = 16.dp, start = 16.dp, end =
16.dp),
+ text = stringResource(R.string.send_peer_warning),
+ )
+ Button(
+ modifier = Modifier.padding(16.dp),
+ enabled = subject.isNotBlank() && amountText.isNotBlank(),
+ onClick = {
+ val amount = getAmount(currency, amountText)
+ if (amount == null) isError = true
+ else onSend(amount, subject)
+ },
+ ) {
+ Text(text = stringResource(R.string.send_peer_create_button))
+ }
+ }
+}
+
+@Preview
+@Composable
+fun PeerPushIntroComposablePreview() {
+ Surface {
+ PeerPushIntroComposable("TESTKUDOS") { _, _ -> }
+ }
+}
diff --git
a/wallet/src/main/java/net/taler/wallet/peer/PeerPushResultComposable.kt
b/wallet/src/main/java/net/taler/wallet/peer/PeerPushResultComposable.kt
new file mode 100644
index 0000000..f3d1a79
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/peer/PeerPushResultComposable.kt
@@ -0,0 +1,185 @@
+/*
+ * 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.peer
+
+import android.content.res.Configuration.UI_MODE_NIGHT_YES
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Button
+import androidx.compose.material.CircularProgressIndicator
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ContentCopy
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.taler.common.QrCodeManager
+import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.compose.copyToClipBoard
+import net.taler.wallet.compose.getQrCodeSize
+import org.json.JSONObject
+
+@Composable
+fun PeerPushResultComposable(state: PeerPaymentState, onClose: () -> Unit) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(scrollState),
+ ) {
+ Text(
+ modifier = Modifier.padding(top = 16.dp, start = 16.dp, end =
16.dp),
+ style = MaterialTheme.typography.h6,
+ text = stringResource(id = R.string.send_peer_payment_instruction),
+ )
+ when (state) {
+ PeerPaymentIntro -> error("Result composable with
PullPaymentIntro")
+ is PeerPaymentCreating -> PeerPushCreatingComposable()
+ is PeerPaymentResponse -> PeerPushResponseComposable(state)
+ is PeerPaymentError -> PeerPushErrorComposable(state)
+ }
+ Button(modifier = Modifier
+ .padding(16.dp)
+ .align(CenterHorizontally),
+ onClick = onClose) {
+ Text(text = stringResource(R.string.close))
+ }
+ }
+}
+
+@Composable
+private fun ColumnScope.PeerPushCreatingComposable() {
+ val qrCodeSize = getQrCodeSize()
+ CircularProgressIndicator(
+ modifier = Modifier
+ .padding(32.dp)
+ .size(qrCodeSize)
+ .align(CenterHorizontally),
+ )
+}
+
+@Composable
+private fun ColumnScope.PeerPushResponseComposable(state: PeerPaymentResponse)
{
+ val qrCodeSize = getQrCodeSize()
+ Image(
+ modifier = Modifier
+ .size(qrCodeSize)
+ .align(CenterHorizontally),
+ bitmap = state.qrCode.asImageBitmap(),
+ contentDescription = stringResource(id = R.string.button_scan_qr_code),
+ )
+ Text(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ style = MaterialTheme.typography.body1,
+ text = stringResource(id = R.string.receive_peer_invoice_uri),
+ )
+ val scrollState = rememberScrollState()
+ Text(
+ modifier = Modifier
+ .horizontalScroll(scrollState)
+ .padding(16.dp),
+ fontFamily = FontFamily.Monospace,
+ style = MaterialTheme.typography.body1,
+ text = state.talerUri,
+ )
+ val context = LocalContext.current
+ IconButton(
+ modifier = Modifier
+ .align(CenterHorizontally),
+ onClick = { copyToClipBoard(context, "Invoice", state.talerUri) },
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(Icons.Default.ContentCopy, stringResource(R.string.copy))
+ Text(
+ modifier = Modifier.padding(start = 8.dp),
+ text = stringResource(R.string.copy),
+ style = MaterialTheme.typography.body1,
+ )
+ }
+ }
+}
+
+@Composable
+private fun ColumnScope.PeerPushErrorComposable(state: PeerPaymentError) {
+ Text(
+ modifier = Modifier
+ .align(CenterHorizontally)
+ .padding(16.dp),
+ color = colorResource(R.color.red),
+ style = MaterialTheme.typography.body1,
+ text = state.info.userFacingMsg,
+ )
+}
+
+@Preview
+@Composable
+fun PeerPushCreatingPreview() {
+ Surface {
+ PeerPushResultComposable(PeerPaymentCreating) {}
+ }
+}
+
+@Preview
+@Composable
+fun PeerPushResponsePreview() {
+ Surface {
+ val talerUri =
"https://example.org/foo/bar/can/be/very/long/url/so/fit/it/on/screen"
+ val response = PeerPaymentResponse(talerUri,
QrCodeManager.makeQrCode(talerUri))
+ PeerPushResultComposable(response) {}
+ }
+}
+
+@Preview(widthDp = 720, uiMode = UI_MODE_NIGHT_YES)
+@Composable
+fun PeerPushResponseLandscapePreview() {
+ Surface {
+ val talerUri =
"https://example.org/foo/bar/can/be/very/long/url/so/fit/it/on/screen"
+ val response = PeerPaymentResponse(talerUri,
QrCodeManager.makeQrCode(talerUri))
+ PeerPushResultComposable(response) {}
+ }
+}
+
+@Preview
+@Composable
+fun PeerPushErrorPreview() {
+ Surface {
+ val json = JSONObject().apply { put("foo", "bar") }
+ val response = PeerPaymentError(TalerErrorInfo(42, "hint", "message",
json))
+ PeerPushResultComposable(response) {}
+ }
+}
diff --git
a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullCredit.kt
b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullCredit.kt
new file mode 100644
index 0000000..3179024
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullCredit.kt
@@ -0,0 +1,98 @@
+/*
+ * 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.peer
+
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.common.Timestamp
+import net.taler.wallet.R
+import net.taler.wallet.compose.QrCodeUriComposable
+import net.taler.wallet.transactions.AmountType
+import net.taler.wallet.transactions.PeerInfoShort
+import net.taler.wallet.transactions.TransactionAmountComposable
+import net.taler.wallet.transactions.TransactionInfoComposable
+import net.taler.wallet.transactions.TransactionPeerComposable
+import net.taler.wallet.transactions.TransactionPeerPullCredit
+
+@Composable
+fun ColumnScope.TransactionPeerPullCreditComposable(t:
TransactionPeerPullCredit) {
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.receive_amount),
+ amount = t.amountEffective,
+ amountType = AmountType.Positive,
+ )
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.amount_chosen),
+ amount = t.amountRaw,
+ amountType = AmountType.Neutral,
+ )
+ val fee = t.amountRaw - t.amountEffective
+ if (!fee.isZero()) {
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.withdraw_fees),
+ amount = fee,
+ amountType = AmountType.Negative,
+ )
+ }
+ TransactionInfoComposable(
+ label = stringResource(id = R.string.withdraw_manual_ready_subject),
+ info = t.info.summary ?: "",
+ )
+ if (t.pending) {
+ QrCodeUriComposable(
+ talerUri = t.talerUri,
+ clipBoardLabel = "Invoice",
+ buttonText = stringResource(id = R.string.copy_uri),
+ ) {
+ Text(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ style = MaterialTheme.typography.body1,
+ text = stringResource(id = R.string.receive_peer_invoice_uri),
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+fun TransactionPeerPullCreditPreview() {
+ val t = TransactionPeerPullCredit(
+ transactionId = "transactionId",
+ timestamp = Timestamp(System.currentTimeMillis() - 360 * 60 * 1000),
+ pending = true,
+ exchangeBaseUrl = "https://exchange.example.org/",
+ amountRaw = Amount.fromDouble("TESTKUDOS", 42.23),
+ amountEffective = Amount.fromDouble("TESTKUDOS", 42.1337),
+ info = PeerInfoShort(
+ expiration = Timestamp(System.currentTimeMillis() + 60 * 60 *
1000),
+ summary = "test invoice",
+ ),
+ talerUri = "https://exchange.example.org/peer/pull/credit",
+ )
+ Surface {
+ TransactionPeerComposable(t) {}
+ }
+}
diff --git
a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushDebit.kt
b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushDebit.kt
new file mode 100644
index 0000000..18528f9
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushDebit.kt
@@ -0,0 +1,96 @@
+/*
+ * 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.peer
+
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.common.Timestamp
+import net.taler.wallet.R
+import net.taler.wallet.compose.QrCodeUriComposable
+import net.taler.wallet.transactions.AmountType
+import net.taler.wallet.transactions.PeerInfoShort
+import net.taler.wallet.transactions.TransactionAmountComposable
+import net.taler.wallet.transactions.TransactionInfoComposable
+import net.taler.wallet.transactions.TransactionPeerComposable
+import net.taler.wallet.transactions.TransactionPeerPushDebit
+
+@Composable
+fun ColumnScope.TransactionPeerPushDebitComposable(t:
TransactionPeerPushDebit) {
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.transaction_paid),
+ amount = t.amountEffective,
+ amountType = AmountType.Negative,
+ )
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.transaction_order_total),
+ amount = t.amountRaw,
+ amountType = AmountType.Neutral,
+ )
+ val fee = t.amountEffective - t.amountRaw
+ if (!fee.isZero()) {
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.withdraw_fees),
+ amount = fee,
+ amountType = AmountType.Negative,
+ )
+ }
+ TransactionInfoComposable(
+ label = stringResource(id = R.string.withdraw_manual_ready_subject),
+ info = t.info.summary ?: "",
+ )
+ QrCodeUriComposable(
+ talerUri = t.talerUri,
+ clipBoardLabel = "Push payment",
+ buttonText = stringResource(id = R.string.copy_uri),
+ ) {
+ Text(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ style = MaterialTheme.typography.body1,
+ text = stringResource(id = R.string.receive_peer_invoice_uri),
+ )
+ }
+}
+
+@Preview
+@Composable
+fun TransactionPeerPushDebitPreview() {
+ val t = TransactionPeerPushDebit(
+ transactionId = "transactionId",
+ timestamp = Timestamp(System.currentTimeMillis() - 360 * 60 * 1000),
+ pending = true,
+ exchangeBaseUrl = "https://exchange.example.org/",
+ amountRaw = Amount.fromDouble("TESTKUDOS", 42.1337),
+ amountEffective = Amount.fromDouble("TESTKUDOS", 42.23),
+ info = PeerInfoShort(
+ expiration = Timestamp(System.currentTimeMillis() + 60 * 60 *
1000),
+ summary = "test invoice",
+ ),
+ talerUri = "https://exchange.example.org/peer/pull/credit",
+ )
+ Surface {
+ TransactionPeerComposable(t) {}
+ }
+}
diff --git
a/wallet/src/main/java/net/taler/wallet/transactions/TransactionPeerFragment.kt
b/wallet/src/main/java/net/taler/wallet/transactions/TransactionPeerFragment.kt
new file mode 100644
index 0000000..f1afb41
--- /dev/null
+++
b/wallet/src/main/java/net/taler/wallet/transactions/TransactionPeerFragment.kt
@@ -0,0 +1,148 @@
+/*
+ * 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.transactions
+
+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.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Button
+import androidx.compose.material.ButtonDefaults
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Alignment.Companion.CenterVertically
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.google.android.material.composethemeadapter.MdcTheme
+import net.taler.common.Amount
+import net.taler.common.toAbsoluteTime
+import net.taler.wallet.R
+import net.taler.wallet.peer.TransactionPeerPullCreditComposable
+import net.taler.wallet.peer.TransactionPeerPushDebitComposable
+
+class TransactionPeerFragment : TransactionDetailFragment() {
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View = ComposeView(requireContext()).apply {
+ setContent {
+ MdcTheme {
+ Surface {
+ val t = transaction ?: error("No transaction")
+ TransactionPeerComposable(t) {
+ onDeleteButtonClicked(t)
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun TransactionPeerComposable(t: Transaction, onDelete: () -> Unit) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(scrollState),
+ horizontalAlignment = CenterHorizontally,
+ ) {
+ val context = LocalContext.current
+ Text(
+ modifier = Modifier.padding(16.dp),
+ text = t.timestamp.ms.toAbsoluteTime(context).toString(),
+ style = MaterialTheme.typography.body1,
+ )
+ when (t) {
+ is TransactionPeerPullCredit ->
TransactionPeerPullCreditComposable(t)
+ is TransactionPeerPushCredit -> TODO()
+ is TransactionPeerPullDebit -> TODO()
+ is TransactionPeerPushDebit ->
TransactionPeerPushDebitComposable(t)
+ else -> error("unexpected transaction: ${t::class.simpleName}")
+ }
+ Button(
+ modifier = Modifier.padding(16.dp),
+ colors = ButtonDefaults.buttonColors(backgroundColor =
colorResource(R.color.red)),
+ onClick = onDelete,
+ ) {
+ Row(verticalAlignment = CenterVertically) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_delete),
+ contentDescription = null,
+ tint = Color.White,
+ )
+ Text(
+ modifier = Modifier.padding(start = 8.dp),
+ text = stringResource(R.string.transactions_delete),
+ color = Color.White,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun TransactionAmountComposable(label: String, amount: Amount, amountType:
AmountType) {
+ Text(
+ modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp),
+ text = label,
+ style = MaterialTheme.typography.body2,
+ )
+ Text(
+ modifier = Modifier.padding(top = 8.dp, start = 16.dp, end = 16.dp,
bottom = 16.dp),
+ text = if (amountType == AmountType.Negative) "-$amount" else
amount.toString(),
+ fontSize = 24.sp,
+ color = when (amountType) {
+ AmountType.Positive -> colorResource(R.color.green)
+ AmountType.Negative -> colorResource(R.color.red)
+ AmountType.Neutral -> Color.Unspecified
+ },
+ )
+}
+
+@Composable
+fun TransactionInfoComposable(label: String, info: String) {
+ Text(
+ modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp),
+ text = label,
+ style = MaterialTheme.typography.body2,
+ )
+ Text(
+ modifier = Modifier.padding(top = 8.dp, start = 16.dp, end = 16.dp,
bottom = 16.dp),
+ text = info,
+ fontSize = 24.sp,
+ )
+}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt
b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt
index ca01501..6f72567 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt
@@ -252,3 +252,116 @@ class TransactionRefresh(
override val generalTitleRes = R.string.transaction_refresh
}
+
+@Serializable
+data class PeerInfoShort(
+ val expiration: Timestamp? = null,
+ val summary: String? = null,
+)
+
+/**
+ * Debit because we paid someone's invoice.
+ */
+@Serializable
+@SerialName("peer-pull-debit")
+class TransactionPeerPullDebit(
+ override val transactionId: String,
+ override val timestamp: Timestamp,
+ override val pending: Boolean,
+ val exchangeBaseUrl: String,
+ override val error: TalerErrorInfo? = null,
+ override val amountRaw: Amount,
+ override val amountEffective: Amount,
+ val info: PeerInfoShort,
+) : Transaction() {
+ override val icon = R.drawable.ic_cash_usd_outline
+ override val detailPageNav = R.id.nav_transactions_detail_peer
+
+ @Transient
+ override val amountType = AmountType.Negative
+ override fun getTitle(context: Context): String {
+ return context.getString(R.string.transaction_peer_push_debit)
+ }
+ override val generalTitleRes = R.string.payment_title
+}
+
+/**
+ * Credit because someone paid for an invoice we created.
+ */
+@Serializable
+@SerialName("peer-pull-credit")
+class TransactionPeerPullCredit(
+ override val transactionId: String,
+ override val timestamp: Timestamp,
+ override val pending: Boolean,
+ val exchangeBaseUrl: String,
+ override val error: TalerErrorInfo? = null,
+ override val amountRaw: Amount,
+ override val amountEffective: Amount,
+ val info: PeerInfoShort,
+ val talerUri: String,
+ // val completed: Boolean, maybe
+) : Transaction() {
+ override val icon = R.drawable.transaction_withdrawal
+ override val detailPageNav = R.id.nav_transactions_detail_peer
+
+ override val amountType get() = AmountType.Positive
+ override fun getTitle(context: Context): String {
+ return context.getString(R.string.transaction_peer_pull_credit)
+ }
+ override val generalTitleRes = R.string.transaction_peer_pull_credit
+}
+
+/**
+ * Debit because we sent money to someone.
+ */
+@Serializable
+@SerialName("peer-push-debit")
+class TransactionPeerPushDebit(
+ override val transactionId: String,
+ override val timestamp: Timestamp,
+ override val pending: Boolean,
+ val exchangeBaseUrl: String,
+ override val error: TalerErrorInfo? = null,
+ override val amountRaw: Amount,
+ override val amountEffective: Amount,
+ val info: PeerInfoShort,
+ val talerUri: String,
+ // val completed: Boolean, definitely
+) : Transaction() {
+ override val icon = R.drawable.ic_cash_usd_outline
+ override val detailPageNav = R.id.nav_transactions_detail_peer
+
+ @Transient
+ override val amountType = AmountType.Negative
+ override fun getTitle(context: Context): String {
+ return context.getString(R.string.transaction_peer_push_debit)
+ }
+ override val generalTitleRes = R.string.payment_title
+}
+
+/**
+ * We received money via a peer payment.
+ */
+@Serializable
+@SerialName("peer-push-credit")
+class TransactionPeerPushCredit(
+ override val transactionId: String,
+ override val timestamp: Timestamp,
+ override val pending: Boolean,
+ val exchangeBaseUrl: String,
+ override val error: TalerErrorInfo? = null,
+ override val amountRaw: Amount,
+ override val amountEffective: Amount,
+ val info: PeerInfoShort,
+) : Transaction() {
+ override val icon = R.drawable.transaction_withdrawal
+ override val detailPageNav = R.id.nav_transactions_detail_peer
+
+ @Transient
+ override val amountType = AmountType.Positive
+ override fun getTitle(context: Context): String {
+ return context.getString(R.string.transaction_peer_push_debit)
+ }
+ override val generalTitleRes = R.string.withdraw_title
+}
diff --git
a/wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt
b/wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt
index f5840ab..50f95c0 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt
@@ -115,6 +115,12 @@ class TransactionsFragment : Fragment(),
OnTransactionClickListener, ActionMode.
transactionManager.transactions.observe(viewLifecycleOwner) { result ->
onTransactionsResult(result)
}
+ ui.sendButton.setOnClickListener {
+ findNavController().navigate(R.id.sendFunds)
+ }
+ ui.receiveButton.setOnClickListener {
+ findNavController().navigate(R.id.receiveFunds)
+ }
ui.mainFab.setOnClickListener {
model.scanCode()
}
diff --git
a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawFragment.kt
b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawFragment.kt
index eb1f133..148b8c0 100644
---
a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawFragment.kt
+++
b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawFragment.kt
@@ -49,6 +49,11 @@ class ManualWithdrawFragment : Fragment() {
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ arguments?.getString("amount")?.let {
+ val amount = Amount.fromJSONString(it)
+ ui.amountView.setText(amount.amountStr)
+ }
+
ui.qrCodeButton.setOnClickListener {
model.scanCode()
}
diff --git
a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawSuccessFragment.kt
b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawSuccessFragment.kt
index e40036d..f019a5b 100644
---
a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawSuccessFragment.kt
+++
b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawSuccessFragment.kt
@@ -16,9 +16,6 @@
package net.taler.wallet.withdraw.manual
-import android.content.ClipData
-import android.content.ClipboardManager
-import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
@@ -26,7 +23,6 @@ import android.view.View
import android.view.ViewGroup
import androidx.compose.material.Surface
import androidx.compose.ui.platform.ComposeView
-import androidx.core.content.getSystemService
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
@@ -82,9 +78,3 @@ class ManualWithdrawSuccessFragment : Fragment() {
activity?.setTitle(R.string.withdraw_title)
}
}
-
-fun copyToClipBoard(context: Context, label: String, str: String) {
- val clipboard = context.getSystemService<ClipboardManager>()
- val clip = ClipData.newPlainText(label, str)
- clipboard?.setPrimaryClip(clip)
-}
diff --git
a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenBitcoin.kt
b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenBitcoin.kt
index 9ae2418..cc271eb 100644
--- a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenBitcoin.kt
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenBitcoin.kt
@@ -37,6 +37,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
+import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
@@ -47,6 +48,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import net.taler.common.Amount
import net.taler.wallet.R
+import net.taler.wallet.compose.copyToClipBoard
import net.taler.wallet.withdraw.WithdrawStatus
@Composable
@@ -189,7 +191,7 @@ $sr
IconButton(
onClick = { copyToClipBoard(context, "Bitcoin", copyText) },
) {
- Row {
+ Row(verticalAlignment = CenterVertically) {
Icon(Icons.Default.ContentCopy, stringResource(R.string.copy))
Text(
modifier = Modifier.padding(start = 8.dp),
diff --git
a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenIBAN.kt
b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenIBAN.kt
index 9dc5d5e..4cf7941 100644
--- a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenIBAN.kt
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenIBAN.kt
@@ -47,6 +47,7 @@ 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.compose.copyToClipBoard
import net.taler.wallet.withdraw.WithdrawStatus
@Composable
diff --git a/wallet/src/main/res/layout/fragment_transactions.xml
b/wallet/src/main/res/layout/fragment_transactions.xml
index 60675bb..bad79ea 100644
--- a/wallet/src/main/res/layout/fragment_transactions.xml
+++ b/wallet/src/main/res/layout/fragment_transactions.xml
@@ -104,10 +104,13 @@
<TextView
android:id="@+id/emptyState"
- android:layout_width="wrap_content"
+ android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
+ android:layout_margin="16dp"
+ android:gravity="center"
android:text="@string/transactions_empty"
+ android:textSize="16sp"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
diff --git a/wallet/src/main/res/navigation/nav_graph.xml
b/wallet/src/main/res/navigation/nav_graph.xml
index 871ba53..e3d526e 100644
--- a/wallet/src/main/res/navigation/nav_graph.xml
+++ b/wallet/src/main/res/navigation/nav_graph.xml
@@ -33,6 +33,23 @@
app:destination="@id/nav_uri_input" />
</fragment>
+ <fragment
+ android:id="@+id/receiveFunds"
+ android:name="net.taler.wallet.ReceiveFundsFragment"
+ android:label="@string/transactions_receive_funds">
+ <action
+
android:id="@+id/action_receiveFunds_to_nav_exchange_manual_withdrawal"
+ app:destination="@id/nav_exchange_manual_withdrawal" />
+ <action
+ android:id="@+id/action_receiveFunds_to_nav_peer_pull"
+ app:destination="@id/nav_peer_pull" />
+ </fragment>
+
+ <fragment
+ android:id="@+id/sendFunds"
+ android:name="net.taler.wallet.SendFundsFragment"
+ android:label="@string/transactions_send_funds" />
+
<fragment
android:id="@+id/promptTip"
android:name="net.taler.wallet.tip.PromptTipFragment"
@@ -91,6 +108,10 @@
<action
android:id="@+id/action_nav_exchange_manual_withdrawal_to_promptWithdraw"
app:destination="@id/promptWithdraw" />
+ <argument
+ android:name="amount"
+ app:argType="string"
+ app:nullable="false" />
</fragment>
<fragment
@@ -108,11 +129,22 @@
android:name="net.taler.wallet.settings.BackupSettingsFragment"
android:label="@string/nav_settings_backup" />
+ <fragment
+ android:id="@+id/nav_peer_pull"
+ android:name="net.taler.wallet.peer.PeerPullFragment"
+ android:label="@string/receive_peer_title">
+ <argument
+ android:name="amount"
+ android:defaultValue="@null"
+ app:argType="string"
+ app:nullable="true" />
+ </fragment>
+
<fragment
android:id="@+id/nav_transactions"
android:name="net.taler.wallet.transactions.TransactionsFragment"
android:label="@string/transactions_title"
- tools:layout="@layout/fragment_transactions" >
+ tools:layout="@layout/fragment_transactions">
<action
android:id="@+id/action_nav_transactions_to_nav_uri_input"
app:destination="@id/nav_uri_input" />
@@ -146,6 +178,12 @@
android:label="@string/transactions_detail_title"
tools:layout="@layout/fragment_transaction_withdrawal" />
+ <fragment
+ android:id="@+id/nav_transactions_detail_peer"
+ android:name="net.taler.wallet.transactions.TransactionPeerFragment"
+ android:label="@string/transactions_detail_title"
+ tools:layout="@layout/fragment_transaction_payment" />
+
<fragment
android:id="@+id/alreadyAccepted"
android:name="net.taler.wallet.tip.AlreadyAcceptedFragment"
@@ -217,6 +255,14 @@
android:label="@string/nav_error"
tools:layout="@layout/fragment_error" />
+ <action
+ android:id="@+id/action_global_receiveFunds"
+ app:destination="@id/receiveFunds" />
+
+ <action
+ android:id="@+id/action_global_sendFunds"
+ app:destination="@id/sendFunds" />
+
<action
android:id="@+id/action_global_promptWithdraw"
app:destination="@id/promptWithdraw" />
diff --git a/wallet/src/main/res/values/strings.xml
b/wallet/src/main/res/values/strings.xml
index 4fdfd4f..96a3453 100644
--- a/wallet/src/main/res/values/strings.xml
+++ b/wallet/src/main/res/values/strings.xml
@@ -46,6 +46,7 @@ GNU Taler is immune against many types of fraud, such as
phishing of credit card
<string name="button_scan_qr_code">Scan Taler QR Code</string>
<string name="enter_uri">Enter Taler URI</string>
<string name="copy" tools:override="true">Copy</string>
+ <string name="copy_uri">Copy Taler URI</string>
<string name="paste">Paste</string>
<string name="paste_invalid">Clipboard contains an invalid data
type</string>
<string name="uri_invalid">Not a valid Taler URI</string>
@@ -95,6 +96,8 @@ GNU Taler is immune against many types of fraud, such as
phishing of credit card
<string name="transaction_refund_from">Refund from %s</string>
<string name="transaction_pending">PENDING</string>
<string name="transaction_refresh">Coin expiry change fee</string>
+ <string name="transaction_peer_push_debit">Push payment</string>
+ <string name="transaction_peer_pull_credit">Invoice</string>
<string name="payment_title">Payment</string>
<string name="payment_fee">+%s payment fee</string>
@@ -109,6 +112,21 @@ GNU Taler is immune against many types of fraud, such as
phishing of credit card
<string name="payment_already_paid_title">Already paid</string>
<string name="payment_already_paid">You\'ve already paid for this
purchase.</string>
+ <string name="receive_amount">Amount to receive</string>
+ <string name="receive_amount_invalid">Amount invalid</string>
+ <string name="receive_intro">Choose where to receive money from:</string>
+ <string name="receive_withdraw">Withdraw from bank account</string>
+ <string name="receive_peer">Invoice another wallet</string>
+ <string name="receive_peer_title">Request payment</string>
+ <string name="receive_peer_create_button">Create invoice</string>
+ <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_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>
+
<string name="withdraw_initiated">Withdrawal initiated</string>
<string name="withdraw_title">Withdrawal</string>
<string name="withdraw_total">Withdraw</string>
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
- [taler-taler-android] branch master updated (699367b -> 4fe0476),
gnunet <=