gnunet-svn
[Top][All Lists]
Advanced

[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.



reply via email to

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