gnunet-svn
[Top][All Lists]
Advanced

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

[taler-taler-android] 03/03: [wallet] implement prototype for outgoing p


From: gnunet
Subject: [taler-taler-android] 03/03: [wallet] implement prototype for outgoing peer transactions
Date: Tue, 06 Sep 2022 23:19:14 +0200

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

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

commit 4fe04766fbf5328d0816f7cd862228a71690fd1c
Author: Torsten Grote <t@grobox.de>
AuthorDate: Tue Sep 6 18:18:47 2022 -0300

    [wallet] implement prototype for outgoing peer transactions
---
 .../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 +
 wallet/src/main/res/navigation/nav_graph.xml       |  48 ++++-
 wallet/src/main/res/values/strings.xml             |  18 ++
 24 files changed, 1852 insertions(+), 14 deletions(-)

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