From 6876951cb30f057a852937a533d5d51d26645680 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 7 Sep 2022 15:48:00 -0300 Subject: [wallet] implement prototype for handling incoming pay-push URI --- .../src/main/java/net/taler/wallet/MainActivity.kt | 4 + .../java/net/taler/wallet/SendFundsFragment.kt | 4 +- .../net/taler/wallet/peer/PeerIncomingState.kt | 50 +++++ .../main/java/net/taler/wallet/peer/PeerManager.kt | 86 ++++---- .../net/taler/wallet/peer/PeerOutgoingState.kt | 47 +++++ .../net/taler/wallet/peer/PeerPullComposable.kt | 129 ------------ .../java/net/taler/wallet/peer/PeerPullFragment.kt | 2 +- .../taler/wallet/peer/PeerPullIntroComposable.kt | 129 ++++++++++++ .../taler/wallet/peer/PeerPullPaymentComposable.kt | 223 +++++++++++++++++++++ .../taler/wallet/peer/PeerPullResultComposable.kt | 22 +- .../taler/wallet/peer/PeerPushResultComposable.kt | 22 +- .../net/taler/wallet/peer/PullPaymentFragment.kt | 68 +++++++ .../taler/wallet/peer/TransactionPeerPullDebit.kt | 77 +++++++ .../wallet/transactions/TransactionPeerFragment.kt | 3 +- .../net/taler/wallet/transactions/Transactions.kt | 4 +- wallet/src/main/res/navigation/nav_graph.xml | 14 ++ wallet/src/main/res/values/strings.xml | 4 + 17 files changed, 691 insertions(+), 197 deletions(-) create mode 100644 wallet/src/main/java/net/taler/wallet/peer/PeerIncomingState.kt create mode 100644 wallet/src/main/java/net/taler/wallet/peer/PeerOutgoingState.kt delete mode 100644 wallet/src/main/java/net/taler/wallet/peer/PeerPullComposable.kt create mode 100644 wallet/src/main/java/net/taler/wallet/peer/PeerPullIntroComposable.kt create mode 100644 wallet/src/main/java/net/taler/wallet/peer/PeerPullPaymentComposable.kt create mode 100644 wallet/src/main/java/net/taler/wallet/peer/PullPaymentFragment.kt create mode 100644 wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullDebit.kt (limited to 'wallet/src/main') diff --git a/wallet/src/main/java/net/taler/wallet/MainActivity.kt b/wallet/src/main/java/net/taler/wallet/MainActivity.kt index ea604c4..2797a69 100644 --- a/wallet/src/main/java/net/taler/wallet/MainActivity.kt +++ b/wallet/src/main/java/net/taler/wallet/MainActivity.kt @@ -253,6 +253,10 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener, model.showProgressBar.value = true model.refundManager.refund(u).observe(this, Observer(::onRefundResponse)) } + action.startsWith("pay-pull/") -> { + nav.navigate(R.id.action_global_prompt_pull_payment) + model.peerManager.checkPeerPullPayment(u) + } else -> { showError(R.string.error_unsupported_uri, "From: $from\nURI: $u") } diff --git a/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt b/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt index c67b345..27f2c96 100644 --- a/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt @@ -28,7 +28,7 @@ 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.PeerOutgoingIntro import net.taler.wallet.peer.PeerPushIntroComposable import net.taler.wallet.peer.PeerPushResultComposable @@ -45,7 +45,7 @@ class SendFundsFragment : Fragment() { MdcTheme { Surface { val state = peerManager.pushState.collectAsStateLifecycleAware() - if (state.value is PeerPaymentIntro) { + if (state.value is PeerOutgoingIntro) { val currency = transactionManager.selectedCurrency ?: error("No currency selected") PeerPushIntroComposable(currency, this@SendFundsFragment::onSend) diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerIncomingState.kt b/wallet/src/main/java/net/taler/wallet/peer/PeerIncomingState.kt new file mode 100644 index 0000000..c021c2f --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/peer/PeerIncomingState.kt @@ -0,0 +1,50 @@ +/* + * 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 + */ + +package net.taler.wallet.peer + +import kotlinx.serialization.Serializable +import net.taler.common.Amount +import net.taler.wallet.backend.TalerErrorInfo + +sealed class PeerIncomingState +object PeerIncomingChecking : PeerIncomingState() +open class PeerIncomingTerms( + val amount: Amount, + val contractTerms: PeerContractTerms, + val id: String, +) : PeerIncomingState() + +class PeerIncomingAccepting(s: PeerIncomingTerms) : + PeerIncomingTerms(s.amount, s.contractTerms, s.id) + +object PeerIncomingAccepted : PeerIncomingState() +data class PeerIncomingError( + val info: TalerErrorInfo, +) : PeerIncomingState() + +@Serializable +data class PeerContractTerms( + val summary: String, + val amount: Amount, +) + +@Serializable +data class CheckPeerPullPaymentResponse( + val amount: Amount, + val contractTerms: PeerContractTerms, + val peerPullPaymentIncomingId: String, +) diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt b/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt index 898dcfd..5bfd030 100644 --- a/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt +++ b/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt @@ -16,18 +16,15 @@ 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 @@ -37,14 +34,17 @@ class PeerManager( private val scope: CoroutineScope, ) { - private val _pullState = MutableStateFlow(PeerPaymentIntro) - val pullState: StateFlow = _pullState + private val _pullState = MutableStateFlow(PeerOutgoingIntro) + val pullState: StateFlow = _pullState - private val _pushState = MutableStateFlow(PeerPaymentIntro) - val pushState: StateFlow = _pushState + private val _pushState = MutableStateFlow(PeerOutgoingIntro) + val pushState: StateFlow = _pushState + + private val _paymentState = MutableStateFlow(PeerIncomingChecking) + val paymentState: StateFlow = _paymentState fun initiatePullPayment(amount: Amount, exchange: ExchangeItem) { - _pullState.value = PeerPaymentCreating + _pullState.value = PeerOutgoingCreating scope.launch(Dispatchers.IO) { api.request("initiatePeerPullPayment", InitiatePeerPullPaymentResponse.serializer()) { put("exchangeBaseUrl", exchange.exchangeBaseUrl) @@ -54,20 +54,20 @@ class PeerManager( }) }.onSuccess { val qrCode = QrCodeManager.makeQrCode(it.talerUri) - _pullState.value = PeerPaymentResponse(it.talerUri, qrCode) + _pullState.value = PeerOutgoingResponse(it.talerUri, qrCode) }.onError { error -> Log.e(TAG, "got initiatePeerPullPayment error result $error") - _pullState.value = PeerPaymentError(error) + _pullState.value = PeerOutgoingError(error) } } } fun resetPullPayment() { - _pullState.value = PeerPaymentIntro + _pullState.value = PeerOutgoingIntro } fun initiatePeerPushPayment(amount: Amount, summary: String) { - _pushState.value = PeerPaymentCreating + _pushState.value = PeerOutgoingCreating scope.launch(Dispatchers.IO) { api.request("initiatePeerPushPayment", InitiatePeerPushPaymentResponse.serializer()) { put("amount", amount.toJSONString()) @@ -76,42 +76,48 @@ class PeerManager( }) }.onSuccess { response -> val qrCode = QrCodeManager.makeQrCode(response.talerUri) - _pushState.value = PeerPaymentResponse(response.talerUri, qrCode) + _pushState.value = PeerOutgoingResponse(response.talerUri, qrCode) }.onError { error -> Log.e(TAG, "got initiatePeerPushPayment error result $error") - _pushState.value = PeerPaymentError(error) + _pushState.value = PeerOutgoingError(error) } } } fun resetPushPayment() { - _pushState.value = PeerPaymentIntro + _pushState.value = PeerOutgoingIntro } -} - -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() + fun checkPeerPullPayment(talerUri: String) { + _paymentState.value = PeerIncomingChecking + scope.launch(Dispatchers.IO) { + api.request("checkPeerPullPayment", CheckPeerPullPaymentResponse.serializer()) { + put("talerUri", talerUri) + }.onSuccess { response -> + _paymentState.value = PeerIncomingTerms( + amount = response.amount, + contractTerms = response.contractTerms, + id = response.peerPullPaymentIncomingId, + ) + }.onError { error -> + Log.e(TAG, "got checkPeerPushPayment error result $error") + _paymentState.value = PeerIncomingError(error) + } + } + } -@Serializable -data class InitiatePeerPullPaymentResponse( - /** - * Taler URI for the other party to make the payment that was requested. - */ - val talerUri: String, -) + fun acceptPeerPullPayment(terms: PeerIncomingTerms) { + _paymentState.value = PeerIncomingAccepting(terms) + scope.launch(Dispatchers.IO) { + api.request("acceptPeerPullPayment") { + put("peerPullPaymentIncomingId", terms.id) + }.onSuccess { + _paymentState.value = PeerIncomingAccepted + }.onError { error -> + Log.e(TAG, "got checkPeerPushPayment error result $error") + _paymentState.value = PeerIncomingError(error) + } + } + } -@Serializable -data class InitiatePeerPushPaymentResponse( - val exchangeBaseUrl: String, - val talerUri: String, -) +} diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerOutgoingState.kt b/wallet/src/main/java/net/taler/wallet/peer/PeerOutgoingState.kt new file mode 100644 index 0000000..0b6b2a8 --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/peer/PeerOutgoingState.kt @@ -0,0 +1,47 @@ +/* + * 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 + */ + +package net.taler.wallet.peer + +import android.graphics.Bitmap +import kotlinx.serialization.Serializable +import net.taler.wallet.backend.TalerErrorInfo + +sealed class PeerOutgoingState +object PeerOutgoingIntro : PeerOutgoingState() +object PeerOutgoingCreating : PeerOutgoingState() +data class PeerOutgoingResponse( + val talerUri: String, + val qrCode: Bitmap, +) : PeerOutgoingState() + +data class PeerOutgoingError( + val info: TalerErrorInfo, +) : PeerOutgoingState() + +@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 deleted file mode 100644 index 02f2c7c..0000000 --- a/wallet/src/main/java/net/taler/wallet/peer/PeerPullComposable.kt +++ /dev/null @@ -1,129 +0,0 @@ -/* - * 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 - */ - -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, - 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 index d38ae34..be79e9d 100644 --- a/wallet/src/main/java/net/taler/wallet/peer/PeerPullFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/peer/PeerPullFragment.kt @@ -51,7 +51,7 @@ class PeerPullFragment : Fragment() { MdcTheme { Surface { val state = peerManager.pullState.collectAsStateLifecycleAware() - if (state.value is PeerPaymentIntro) { + if (state.value is PeerOutgoingIntro) { val exchangeState = exchangeFlow.collectAsStateLifecycleAware(initial = null) PeerPullIntroComposable( diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerPullIntroComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/PeerPullIntroComposable.kt new file mode 100644 index 0000000..02f2c7c --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/peer/PeerPullIntroComposable.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 + */ + +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, + 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/PeerPullPaymentComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/PeerPullPaymentComposable.kt new file mode 100644 index 0000000..fff74ea --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/peer/PeerPullPaymentComposable.kt @@ -0,0 +1,223 @@ +/* + * 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 + */ + +package net.taler.wallet.peer + +import android.annotation.SuppressLint +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +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.Card +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Alignment.Companion.End +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.font.FontWeight +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.backend.TalerErrorInfo + +@Composable +fun PeerPullPaymentComposable( + state: State, + onAccept: (PeerIncomingTerms) -> Unit, +) { + val scrollState = rememberScrollState() + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState), + ) { + Text( + modifier = Modifier + .padding(16.dp) + .align(CenterHorizontally), + text = stringResource(id = R.string.pay_peer_intro)) + when (val s = state.value) { + PeerIncomingChecking -> PeerPullCheckingComposable() + is PeerIncomingTerms -> PeerPullTermsComposable(s, onAccept) + is PeerIncomingAccepting -> PeerPullTermsComposable(s, onAccept) + PeerIncomingAccepted -> { + // we navigate away, don't show anything + } + is PeerIncomingError -> PeerPullErrorComposable(s) + } + } +} + +@Composable +fun ColumnScope.PeerPullCheckingComposable() { + CircularProgressIndicator( + modifier = Modifier + .align(CenterHorizontally) + .fillMaxSize(0.75f), + ) +} + +@Composable +fun ColumnScope.PeerPullTermsComposable( + terms: PeerIncomingTerms, + onAccept: (PeerIncomingTerms) -> Unit, +) { + Text( + modifier = Modifier + .padding(16.dp) + .align(CenterHorizontally), + text = terms.contractTerms.summary, + style = MaterialTheme.typography.h5, + ) + Spacer(modifier = Modifier.weight(1f)) + Card(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier.padding(8.dp) + ) { + Row( + modifier = Modifier.align(End), + ) { + Text( + text = stringResource(id = R.string.payment_label_amount_total), + style = MaterialTheme.typography.body1, + ) + Text( + modifier = Modifier.padding(start = 8.dp), + text = terms.contractTerms.amount.toString(), + style = MaterialTheme.typography.body1, + fontWeight = FontWeight.Bold, + ) + } + val fee = + Amount.zero(terms.amount.currency) // terms.amount - terms.contractTerms.amount + if (!fee.isZero()) { + Text( + modifier = Modifier.align(End), + text = stringResource(id = R.string.payment_fee, fee), + style = MaterialTheme.typography.body1, + ) + } + if (terms is PeerIncomingAccepting) { + CircularProgressIndicator( + modifier = Modifier + .padding(end = 64.dp) + .align(End), + ) + } else { + Button( + modifier = Modifier + .align(End) + .padding(top = 8.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = colorResource(R.color.green), + contentColor = Color.White, + ), + onClick = { onAccept(terms) }, + ) { + Text( + text = stringResource(id = R.string.payment_button_confirm), + ) + } + } + } + } +} + +@Composable +fun ColumnScope.PeerPullErrorComposable(s: PeerIncomingError) { + Text( + modifier = Modifier + .align(CenterHorizontally) + .padding(horizontal = 32.dp), + text = s.info.userFacingMsg, + style = MaterialTheme.typography.h5, + color = colorResource(id = R.color.red), + ) +} + +@Preview +@Composable +fun PeerPullCheckingPreview() { + Surface { + @SuppressLint("UnrememberedMutableState") + val s = mutableStateOf(PeerIncomingChecking) + PeerPullPaymentComposable(s) {} + } +} + +@Preview +@Composable +fun PeerPullTermsPreview() { + Surface { + val terms = PeerIncomingTerms( + amount = Amount.fromDouble("TESTKUDOS", 42.23), + contractTerms = PeerContractTerms( + summary = "This is a long test summary that can be more than one line long for sure", + amount = Amount.fromDouble("TESTKUDOS", 23.42), + ), + id = "ID123", + ) + + @SuppressLint("UnrememberedMutableState") + val s = mutableStateOf(terms) + PeerPullPaymentComposable(s) {} + } +} + +@Preview +@Composable +fun PeerPullAcceptingPreview() { + Surface { + val terms = PeerIncomingTerms( + amount = Amount.fromDouble("TESTKUDOS", 42.23), + contractTerms = PeerContractTerms( + summary = "This is a long test summary that can be more than one line long for sure", + amount = Amount.fromDouble("TESTKUDOS", 23.42), + ), + id = "ID123", + ) + + @SuppressLint("UnrememberedMutableState") + val s = mutableStateOf(PeerIncomingAccepting(terms)) + PeerPullPaymentComposable(s) {} + } +} + +@Preview +@Composable +fun PeerPullPayErrorPreview() { + Surface { + @SuppressLint("UnrememberedMutableState") + val s = mutableStateOf(PeerIncomingError(TalerErrorInfo(42, "hint", "msg"))) + PeerPullPaymentComposable(s) {} + } +} diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerPullResultComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/PeerPullResultComposable.kt index 0b9b546..d37ca4b 100644 --- a/wallet/src/main/java/net/taler/wallet/peer/PeerPullResultComposable.kt +++ b/wallet/src/main/java/net/taler/wallet/peer/PeerPullResultComposable.kt @@ -55,7 +55,7 @@ import net.taler.wallet.compose.getQrCodeSize import org.json.JSONObject @Composable -fun PeerPullResultComposable(state: PeerPaymentState, onClose: () -> Unit) { +fun PeerPullResultComposable(state: PeerOutgoingState, onClose: () -> Unit) { val scrollState = rememberScrollState() Column( modifier = Modifier @@ -68,10 +68,10 @@ fun PeerPullResultComposable(state: PeerPaymentState, onClose: () -> Unit) { 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) + PeerOutgoingIntro -> error("Result composable with PullPaymentIntro") + is PeerOutgoingCreating -> PeerPullCreatingComposable() + is PeerOutgoingResponse -> PeerPullResponseComposable(state) + is PeerOutgoingError -> PeerPullErrorComposable(state) } Button(modifier = Modifier .padding(16.dp) @@ -94,7 +94,7 @@ private fun ColumnScope.PeerPullCreatingComposable() { } @Composable -private fun ColumnScope.PeerPullResponseComposable(state: PeerPaymentResponse) { +private fun ColumnScope.PeerPullResponseComposable(state: PeerOutgoingResponse) { val qrCodeSize = getQrCodeSize() Image( modifier = Modifier @@ -135,7 +135,7 @@ private fun ColumnScope.PeerPullResponseComposable(state: PeerPaymentResponse) { } @Composable -private fun ColumnScope.PeerPullErrorComposable(state: PeerPaymentError) { +private fun ColumnScope.PeerPullErrorComposable(state: PeerOutgoingError) { Text( modifier = Modifier .align(CenterHorizontally) @@ -150,7 +150,7 @@ private fun ColumnScope.PeerPullErrorComposable(state: PeerPaymentError) { @Composable fun PeerPullCreatingPreview() { Surface { - PeerPullResultComposable(PeerPaymentCreating) {} + PeerPullResultComposable(PeerOutgoingCreating) {} } } @@ -159,7 +159,7 @@ fun PeerPullCreatingPreview() { 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)) + val response = PeerOutgoingResponse(talerUri, QrCodeManager.makeQrCode(talerUri)) PeerPullResultComposable(response) {} } } @@ -169,7 +169,7 @@ fun PeerPullResponsePreview() { 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)) + val response = PeerOutgoingResponse(talerUri, QrCodeManager.makeQrCode(talerUri)) PeerPullResultComposable(response) {} } } @@ -179,7 +179,7 @@ fun PeerPullResponseLandscapePreview() { fun PeerPullErrorPreview() { Surface { val json = JSONObject().apply { put("foo", "bar") } - val response = PeerPaymentError(TalerErrorInfo(42, "hint", "message", json)) + val response = PeerOutgoingError(TalerErrorInfo(42, "hint", "message", json)) PeerPullResultComposable(response) {} } } diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerPushResultComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/PeerPushResultComposable.kt index f3d1a79..b33fc4f 100644 --- a/wallet/src/main/java/net/taler/wallet/peer/PeerPushResultComposable.kt +++ b/wallet/src/main/java/net/taler/wallet/peer/PeerPushResultComposable.kt @@ -55,7 +55,7 @@ import net.taler.wallet.compose.getQrCodeSize import org.json.JSONObject @Composable -fun PeerPushResultComposable(state: PeerPaymentState, onClose: () -> Unit) { +fun PeerPushResultComposable(state: PeerOutgoingState, onClose: () -> Unit) { val scrollState = rememberScrollState() Column( modifier = Modifier @@ -68,10 +68,10 @@ fun PeerPushResultComposable(state: PeerPaymentState, onClose: () -> Unit) { 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) + PeerOutgoingIntro -> error("Result composable with PullPaymentIntro") + is PeerOutgoingCreating -> PeerPushCreatingComposable() + is PeerOutgoingResponse -> PeerPushResponseComposable(state) + is PeerOutgoingError -> PeerPushErrorComposable(state) } Button(modifier = Modifier .padding(16.dp) @@ -94,7 +94,7 @@ private fun ColumnScope.PeerPushCreatingComposable() { } @Composable -private fun ColumnScope.PeerPushResponseComposable(state: PeerPaymentResponse) { +private fun ColumnScope.PeerPushResponseComposable(state: PeerOutgoingResponse) { val qrCodeSize = getQrCodeSize() Image( modifier = Modifier @@ -135,7 +135,7 @@ private fun ColumnScope.PeerPushResponseComposable(state: PeerPaymentResponse) { } @Composable -private fun ColumnScope.PeerPushErrorComposable(state: PeerPaymentError) { +private fun ColumnScope.PeerPushErrorComposable(state: PeerOutgoingError) { Text( modifier = Modifier .align(CenterHorizontally) @@ -150,7 +150,7 @@ private fun ColumnScope.PeerPushErrorComposable(state: PeerPaymentError) { @Composable fun PeerPushCreatingPreview() { Surface { - PeerPushResultComposable(PeerPaymentCreating) {} + PeerPushResultComposable(PeerOutgoingCreating) {} } } @@ -159,7 +159,7 @@ fun PeerPushCreatingPreview() { 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)) + val response = PeerOutgoingResponse(talerUri, QrCodeManager.makeQrCode(talerUri)) PeerPushResultComposable(response) {} } } @@ -169,7 +169,7 @@ fun PeerPushResponsePreview() { 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)) + val response = PeerOutgoingResponse(talerUri, QrCodeManager.makeQrCode(talerUri)) PeerPushResultComposable(response) {} } } @@ -179,7 +179,7 @@ fun PeerPushResponseLandscapePreview() { fun PeerPushErrorPreview() { Surface { val json = JSONObject().apply { put("foo", "bar") } - val response = PeerPaymentError(TalerErrorInfo(42, "hint", "message", json)) + val response = PeerOutgoingError(TalerErrorInfo(42, "hint", "message", json)) PeerPushResultComposable(response) {} } } diff --git a/wallet/src/main/java/net/taler/wallet/peer/PullPaymentFragment.kt b/wallet/src/main/java/net/taler/wallet/peer/PullPaymentFragment.kt new file mode 100644 index 0000000..71b1bcc --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/peer/PullPaymentFragment.kt @@ -0,0 +1,68 @@ +/* + * 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 + */ + +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.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import com.google.android.material.composethemeadapter.MdcTheme +import net.taler.wallet.MainViewModel +import net.taler.wallet.R +import net.taler.wallet.compose.collectAsStateLifecycleAware + +class PullPaymentFragment : Fragment() { + private val model: MainViewModel by activityViewModels() + private val peerManager get() = model.peerManager + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + lifecycleScope.launchWhenResumed { + peerManager.paymentState.collect { + if (it is PeerIncomingAccepted) { + findNavController().navigate(R.id.action_promptPullPayment_to_nav_main) + } + } + } + return ComposeView(requireContext()).apply { + setContent { + MdcTheme { + Surface { + val state = peerManager.paymentState.collectAsStateLifecycleAware() + PeerPullPaymentComposable(state) { terms -> + peerManager.acceptPeerPullPayment(terms) + } + } + } + } + } + } + + override fun onStart() { + super.onStart() + activity?.setTitle(R.string.pay_peer_title) + } +} diff --git a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullDebit.kt b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullDebit.kt new file mode 100644 index 0000000..823126b --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullDebit.kt @@ -0,0 +1,77 @@ +/* + * 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 + */ + +package net.taler.wallet.peer + +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import net.taler.common.Amount +import net.taler.common.Timestamp +import net.taler.wallet.R +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.TransactionPeerPullDebit + +@Composable +fun TransactionPeerPullDebitComposable(t: TransactionPeerPullDebit) { + 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 ?: "", + ) +} + +@Preview +@Composable +fun TransactionPeerPullDebitPreview() { + val t = TransactionPeerPullDebit( + 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", + ), + ) + 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 index f1afb41..9b0c208 100644 --- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionPeerFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionPeerFragment.kt @@ -49,6 +49,7 @@ 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.TransactionPeerPullDebitComposable import net.taler.wallet.peer.TransactionPeerPushDebitComposable class TransactionPeerFragment : TransactionDetailFragment() { @@ -89,7 +90,7 @@ fun TransactionPeerComposable(t: Transaction, onDelete: () -> Unit) { when (t) { is TransactionPeerPullCredit -> TransactionPeerPullCreditComposable(t) is TransactionPeerPushCredit -> TODO() - is TransactionPeerPullDebit -> TODO() + is TransactionPeerPullDebit -> TransactionPeerPullDebitComposable(t) is TransactionPeerPushDebit -> TransactionPeerPushDebitComposable(t) else -> error("unexpected transaction: ${t::class.simpleName}") } 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 6f72567..6ef6c88 100644 --- a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt +++ b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt @@ -280,9 +280,9 @@ class TransactionPeerPullDebit( @Transient override val amountType = AmountType.Negative override fun getTitle(context: Context): String { - return context.getString(R.string.transaction_peer_push_debit) + return context.getString(R.string.transaction_peer_pull_debit) } - override val generalTitleRes = R.string.payment_title + override val generalTitleRes = R.string.transaction_peer_pull_debit } /** diff --git a/wallet/src/main/res/navigation/nav_graph.xml b/wallet/src/main/res/navigation/nav_graph.xml index e3d526e..3170216 100644 --- a/wallet/src/main/res/navigation/nav_graph.xml +++ b/wallet/src/main/res/navigation/nav_graph.xml @@ -140,6 +140,16 @@ app:nullable="true" /> + + + + + + diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml index 96a3453..8601b14 100644 --- a/wallet/src/main/res/values/strings.xml +++ b/wallet/src/main/res/values/strings.xml @@ -98,6 +98,7 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card Coin expiry change fee Push payment Invoice + Invoice paid Payment +%s payment fee @@ -127,6 +128,9 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card Warning: Funds will leave the wallet immediately. Let the payee scan this QR code to receive: + Pay invoice + Do you want to pay this invoice? + Withdrawal initiated Withdrawal Withdraw -- cgit v1.2.3