aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTorsten Grote <t@grobox.de>2022-09-06 18:18:47 -0300
committerTorsten Grote <t@grobox.de>2022-09-06 18:18:47 -0300
commit4fe04766fbf5328d0816f7cd862228a71690fd1c (patch)
tree289e2c4fbfbd5462b0d4d2e2067227c68b840efa
parent3d3108dd40cd8d3395434905463a91b4b3a71d5a (diff)
downloadtaler-android-4fe04766fbf5328d0816f7cd862228a71690fd1c.tar.gz
taler-android-4fe04766fbf5328d0816f7cd862228a71690fd1c.tar.bz2
taler-android-4fe04766fbf5328d0816f7cd862228a71690fd1c.zip
[wallet] implement prototype for outgoing peer transactions
-rw-r--r--wallet/src/main/java/net/taler/wallet/MainViewModel.kt2
-rw-r--r--wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt198
-rw-r--r--wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt75
-rw-r--r--wallet/src/main/java/net/taler/wallet/Utils.kt10
-rw-r--r--wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt124
-rw-r--r--wallet/src/main/java/net/taler/wallet/compose/Utils.kt53
-rw-r--r--wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt16
-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
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionPeerFragment.kt148
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt113
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt6
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawFragment.kt5
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawSuccessFragment.kt10
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenBitcoin.kt4
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenIBAN.kt1
-rw-r--r--wallet/src/main/res/navigation/nav_graph.xml48
-rw-r--r--wallet/src/main/res/values/strings.xml18
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
@@ -34,6 +34,23 @@
</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"
android:label="Review Tip"
@@ -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
@@ -109,10 +130,21 @@
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" />
@@ -147,6 +179,12 @@
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"
android:label="@string/tip_already_accepted"
@@ -218,6 +256,14 @@
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>