aboutsummaryrefslogtreecommitdiff
path: root/wallet/src/main/java/net/taler/wallet/peer
diff options
context:
space:
mode:
Diffstat (limited to 'wallet/src/main/java/net/taler/wallet/peer')
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt117
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/PeerPullComposable.kt129
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/PeerPullFragment.kt86
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/PeerPullResultComposable.kt185
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/PeerPushComposable.kt139
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/PeerPushResultComposable.kt185
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullCredit.kt98
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushDebit.kt96
8 files changed, 1035 insertions, 0 deletions
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) {}
+ }
+}