diff options
18 files changed, 343 insertions, 113 deletions
diff --git a/wallet/src/main/java/net/taler/wallet/MainActivity.kt b/wallet/src/main/java/net/taler/wallet/MainActivity.kt index 2797a69..df974ff 100644 --- a/wallet/src/main/java/net/taler/wallet/MainActivity.kt +++ b/wallet/src/main/java/net/taler/wallet/MainActivity.kt @@ -257,6 +257,10 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener, nav.navigate(R.id.action_global_prompt_pull_payment) model.peerManager.checkPeerPullPayment(u) } + action.startsWith("pay-push/") -> { + nav.navigate(R.id.action_global_prompt_push_payment) + model.peerManager.checkPeerPushPayment(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 27f2c96..290c91b 100644 --- a/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt @@ -28,9 +28,9 @@ 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.PeerOutgoingIntro -import net.taler.wallet.peer.PeerPushIntroComposable -import net.taler.wallet.peer.PeerPushResultComposable +import net.taler.wallet.peer.OutgoingIntro +import net.taler.wallet.peer.OutgoingPushIntroComposable +import net.taler.wallet.peer.OutgoingPushResultComposable class SendFundsFragment : Fragment() { private val model: MainViewModel by activityViewModels() @@ -45,12 +45,12 @@ class SendFundsFragment : Fragment() { MdcTheme { Surface { val state = peerManager.pushState.collectAsStateLifecycleAware() - if (state.value is PeerOutgoingIntro) { + if (state.value is OutgoingIntro) { val currency = transactionManager.selectedCurrency ?: error("No currency selected") - PeerPushIntroComposable(currency, this@SendFundsFragment::onSend) + OutgoingPushIntroComposable(currency, this@SendFundsFragment::onSend) } else { - PeerPushResultComposable(state.value) { + OutgoingPushResultComposable(state.value) { findNavController().popBackStack() } } diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerPullPaymentComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/IncomingComposable.kt index fff74ea..0095bc4 100644 --- a/wallet/src/main/java/net/taler/wallet/peer/PeerPullPaymentComposable.kt +++ b/wallet/src/main/java/net/taler/wallet/peer/IncomingComposable.kt @@ -17,6 +17,7 @@ package net.taler.wallet.peer import android.annotation.SuppressLint +import androidx.annotation.StringRes import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row @@ -49,10 +50,26 @@ import net.taler.common.Amount import net.taler.wallet.R import net.taler.wallet.backend.TalerErrorInfo +data class IncomingData( + @StringRes val intro: Int, + @StringRes val button: Int, +) + +val incomingPush = IncomingData( + intro = R.string.receive_peer_payment_intro, + button = R.string.receive_peer_payment_title, +) + +val incomingPull = IncomingData( + intro = R.string.pay_peer_intro, + button = R.string.payment_button_confirm, +) + @Composable -fun PeerPullPaymentComposable( - state: State<PeerIncomingState>, - onAccept: (PeerIncomingTerms) -> Unit, +fun IncomingComposable( + state: State<IncomingState>, + data: IncomingData, + onAccept: (IncomingTerms) -> Unit, ) { val scrollState = rememberScrollState() Column( @@ -64,15 +81,16 @@ fun PeerPullPaymentComposable( modifier = Modifier .padding(16.dp) .align(CenterHorizontally), - text = stringResource(id = R.string.pay_peer_intro)) + text = stringResource(id = data.intro), + ) when (val s = state.value) { - PeerIncomingChecking -> PeerPullCheckingComposable() - is PeerIncomingTerms -> PeerPullTermsComposable(s, onAccept) - is PeerIncomingAccepting -> PeerPullTermsComposable(s, onAccept) - PeerIncomingAccepted -> { + IncomingChecking -> PeerPullCheckingComposable() + is IncomingTerms -> PeerPullTermsComposable(s, onAccept, data) + is IncomingAccepting -> PeerPullTermsComposable(s, onAccept, data) + IncomingAccepted -> { // we navigate away, don't show anything } - is PeerIncomingError -> PeerPullErrorComposable(s) + is IncomingError -> PeerPullErrorComposable(s) } } } @@ -88,8 +106,9 @@ fun ColumnScope.PeerPullCheckingComposable() { @Composable fun ColumnScope.PeerPullTermsComposable( - terms: PeerIncomingTerms, - onAccept: (PeerIncomingTerms) -> Unit, + terms: IncomingTerms, + onAccept: (IncomingTerms) -> Unit, + data: IncomingData, ) { Text( modifier = Modifier @@ -126,7 +145,7 @@ fun ColumnScope.PeerPullTermsComposable( style = MaterialTheme.typography.body1, ) } - if (terms is PeerIncomingAccepting) { + if (terms is IncomingAccepting) { CircularProgressIndicator( modifier = Modifier .padding(end = 64.dp) @@ -144,7 +163,7 @@ fun ColumnScope.PeerPullTermsComposable( onClick = { onAccept(terms) }, ) { Text( - text = stringResource(id = R.string.payment_button_confirm), + text = stringResource(id = data.button), ) } } @@ -153,7 +172,7 @@ fun ColumnScope.PeerPullTermsComposable( } @Composable -fun ColumnScope.PeerPullErrorComposable(s: PeerIncomingError) { +fun ColumnScope.PeerPullErrorComposable(s: IncomingError) { Text( modifier = Modifier .align(CenterHorizontally) @@ -169,8 +188,8 @@ fun ColumnScope.PeerPullErrorComposable(s: PeerIncomingError) { fun PeerPullCheckingPreview() { Surface { @SuppressLint("UnrememberedMutableState") - val s = mutableStateOf(PeerIncomingChecking) - PeerPullPaymentComposable(s) {} + val s = mutableStateOf(IncomingChecking) + IncomingComposable(s, incomingPush) {} } } @@ -178,7 +197,7 @@ fun PeerPullCheckingPreview() { @Composable fun PeerPullTermsPreview() { Surface { - val terms = PeerIncomingTerms( + val terms = IncomingTerms( 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", @@ -189,7 +208,7 @@ fun PeerPullTermsPreview() { @SuppressLint("UnrememberedMutableState") val s = mutableStateOf(terms) - PeerPullPaymentComposable(s) {} + IncomingComposable(s, incomingPush) {} } } @@ -197,7 +216,7 @@ fun PeerPullTermsPreview() { @Composable fun PeerPullAcceptingPreview() { Surface { - val terms = PeerIncomingTerms( + val terms = IncomingTerms( 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", @@ -207,8 +226,8 @@ fun PeerPullAcceptingPreview() { ) @SuppressLint("UnrememberedMutableState") - val s = mutableStateOf(PeerIncomingAccepting(terms)) - PeerPullPaymentComposable(s) {} + val s = mutableStateOf(IncomingAccepting(terms)) + IncomingComposable(s, incomingPush) {} } } @@ -217,7 +236,7 @@ fun PeerPullAcceptingPreview() { fun PeerPullPayErrorPreview() { Surface { @SuppressLint("UnrememberedMutableState") - val s = mutableStateOf(PeerIncomingError(TalerErrorInfo(42, "hint", "msg"))) - PeerPullPaymentComposable(s) {} + val s = mutableStateOf(IncomingError(TalerErrorInfo(42, "hint", "msg"))) + IncomingComposable(s, incomingPush) {} } } diff --git a/wallet/src/main/java/net/taler/wallet/peer/PullPaymentFragment.kt b/wallet/src/main/java/net/taler/wallet/peer/IncomingPullPaymentFragment.kt index 71b1bcc..cd2f39b 100644 --- a/wallet/src/main/java/net/taler/wallet/peer/PullPaymentFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/peer/IncomingPullPaymentFragment.kt @@ -31,7 +31,7 @@ import net.taler.wallet.MainViewModel import net.taler.wallet.R import net.taler.wallet.compose.collectAsStateLifecycleAware -class PullPaymentFragment : Fragment() { +class IncomingPullPaymentFragment : Fragment() { private val model: MainViewModel by activityViewModels() private val peerManager get() = model.peerManager @@ -41,8 +41,8 @@ class PullPaymentFragment : Fragment() { savedInstanceState: Bundle?, ): View { lifecycleScope.launchWhenResumed { - peerManager.paymentState.collect { - if (it is PeerIncomingAccepted) { + peerManager.incomingPullState.collect { + if (it is IncomingAccepted) { findNavController().navigate(R.id.action_promptPullPayment_to_nav_main) } } @@ -51,8 +51,8 @@ class PullPaymentFragment : Fragment() { setContent { MdcTheme { Surface { - val state = peerManager.paymentState.collectAsStateLifecycleAware() - PeerPullPaymentComposable(state) { terms -> + val state = peerManager.incomingPullState.collectAsStateLifecycleAware() + IncomingComposable(state, incomingPull) { terms -> peerManager.acceptPeerPullPayment(terms) } } diff --git a/wallet/src/main/java/net/taler/wallet/peer/IncomingPushPaymentFragment.kt b/wallet/src/main/java/net/taler/wallet/peer/IncomingPushPaymentFragment.kt new file mode 100644 index 0000000..8429ecc --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/peer/IncomingPushPaymentFragment.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 <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.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 IncomingPushPaymentFragment : 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.incomingPushState.collect { + if (it is IncomingAccepted) { + findNavController().navigate(R.id.action_promptPushPayment_to_nav_main) + } + } + } + return ComposeView(requireContext()).apply { + setContent { + MdcTheme { + Surface { + val state = peerManager.incomingPushState.collectAsStateLifecycleAware() + IncomingComposable(state, incomingPush) { terms -> + peerManager.acceptPeerPushPayment(terms) + } + } + } + } + } + } + + override fun onStart() { + super.onStart() + activity?.setTitle(R.string.receive_peer_payment_title) + } +} diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerIncomingState.kt b/wallet/src/main/java/net/taler/wallet/peer/IncomingState.kt index c021c2f..7ca38c4 100644 --- a/wallet/src/main/java/net/taler/wallet/peer/PeerIncomingState.kt +++ b/wallet/src/main/java/net/taler/wallet/peer/IncomingState.kt @@ -20,21 +20,21 @@ import kotlinx.serialization.Serializable import net.taler.common.Amount import net.taler.wallet.backend.TalerErrorInfo -sealed class PeerIncomingState -object PeerIncomingChecking : PeerIncomingState() -open class PeerIncomingTerms( +sealed class IncomingState +object IncomingChecking : IncomingState() +open class IncomingTerms( val amount: Amount, val contractTerms: PeerContractTerms, val id: String, -) : PeerIncomingState() +) : IncomingState() -class PeerIncomingAccepting(s: PeerIncomingTerms) : - PeerIncomingTerms(s.amount, s.contractTerms, s.id) +class IncomingAccepting(s: IncomingTerms) : + IncomingTerms(s.amount, s.contractTerms, s.id) -object PeerIncomingAccepted : PeerIncomingState() -data class PeerIncomingError( +object IncomingAccepted : IncomingState() +data class IncomingError( val info: TalerErrorInfo, -) : PeerIncomingState() +) : IncomingState() @Serializable data class PeerContractTerms( @@ -48,3 +48,10 @@ data class CheckPeerPullPaymentResponse( val contractTerms: PeerContractTerms, val peerPullPaymentIncomingId: String, ) + +@Serializable +data class CheckPeerPushPaymentResponse( + val amount: Amount, + val contractTerms: PeerContractTerms, + val peerPushPaymentIncomingId: String, +) diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerPullFragment.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt index be79e9d..b1593ff 100644 --- a/wallet/src/main/java/net/taler/wallet/peer/PeerPullFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt @@ -32,7 +32,7 @@ import net.taler.wallet.R import net.taler.wallet.compose.collectAsStateLifecycleAware import net.taler.wallet.exchanges.ExchangeItem -class PeerPullFragment : Fragment() { +class OutgoingPullFragment : Fragment() { private val model: MainViewModel by activityViewModels() private val exchangeManager get() = model.exchangeManager private val peerManager get() = model.peerManager @@ -51,16 +51,16 @@ class PeerPullFragment : Fragment() { MdcTheme { Surface { val state = peerManager.pullState.collectAsStateLifecycleAware() - if (state.value is PeerOutgoingIntro) { + if (state.value is OutgoingIntro) { val exchangeState = exchangeFlow.collectAsStateLifecycleAware(initial = null) - PeerPullIntroComposable( + OutgoingPullIntroComposable( amount = amount, exchangeState = exchangeState, - onCreateInvoice = this@PeerPullFragment::onCreateInvoice, + onCreateInvoice = this@OutgoingPullFragment::onCreateInvoice, ) } else { - PeerPullResultComposable(state.value) { + OutgoingPullResultComposable(state.value) { findNavController().popBackStack() } } diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerPullIntroComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullIntroComposable.kt index 02f2c7c..a338836 100644 --- a/wallet/src/main/java/net/taler/wallet/peer/PeerPullIntroComposable.kt +++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullIntroComposable.kt @@ -50,7 +50,7 @@ import net.taler.wallet.cleanExchange import net.taler.wallet.exchanges.ExchangeItem @Composable -fun PeerPullIntroComposable( +fun OutgoingPullIntroComposable( amount: Amount, exchangeState: State<ExchangeItem?>, onCreateInvoice: (amount: Amount, exchange: ExchangeItem) -> Unit, @@ -124,6 +124,6 @@ fun PreviewReceiveFundsIntro() { @SuppressLint("UnrememberedMutableState") val exchangeFlow = mutableStateOf(ExchangeItem("https://example.org", "TESTKUDOS", emptyList())) - PeerPullIntroComposable(Amount.fromDouble("TESTKUDOS", 42.23), exchangeFlow) { _, _ -> } + OutgoingPullIntroComposable(Amount.fromDouble("TESTKUDOS", 42.23), exchangeFlow) { _, _ -> } } } diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerPullResultComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullResultComposable.kt index d37ca4b..2c4001f 100644 --- a/wallet/src/main/java/net/taler/wallet/peer/PeerPullResultComposable.kt +++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullResultComposable.kt @@ -55,7 +55,7 @@ import net.taler.wallet.compose.getQrCodeSize import org.json.JSONObject @Composable -fun PeerPullResultComposable(state: PeerOutgoingState, onClose: () -> Unit) { +fun OutgoingPullResultComposable(state: OutgoingState, onClose: () -> Unit) { val scrollState = rememberScrollState() Column( modifier = Modifier @@ -68,10 +68,10 @@ fun PeerPullResultComposable(state: PeerOutgoingState, onClose: () -> Unit) { text = stringResource(id = R.string.receive_peer_invoice_instruction), ) when (state) { - PeerOutgoingIntro -> error("Result composable with PullPaymentIntro") - is PeerOutgoingCreating -> PeerPullCreatingComposable() - is PeerOutgoingResponse -> PeerPullResponseComposable(state) - is PeerOutgoingError -> PeerPullErrorComposable(state) + OutgoingIntro -> error("Result composable with PullPaymentIntro") + is OutgoingCreating -> PeerPullCreatingComposable() + is OutgoingResponse -> PeerPullResponseComposable(state) + is OutgoingError -> PeerPullErrorComposable(state) } Button(modifier = Modifier .padding(16.dp) @@ -94,7 +94,7 @@ private fun ColumnScope.PeerPullCreatingComposable() { } @Composable -private fun ColumnScope.PeerPullResponseComposable(state: PeerOutgoingResponse) { +private fun ColumnScope.PeerPullResponseComposable(state: OutgoingResponse) { val qrCodeSize = getQrCodeSize() Image( modifier = Modifier @@ -135,7 +135,7 @@ private fun ColumnScope.PeerPullResponseComposable(state: PeerOutgoingResponse) } @Composable -private fun ColumnScope.PeerPullErrorComposable(state: PeerOutgoingError) { +private fun ColumnScope.PeerPullErrorComposable(state: OutgoingError) { Text( modifier = Modifier .align(CenterHorizontally) @@ -150,7 +150,7 @@ private fun ColumnScope.PeerPullErrorComposable(state: PeerOutgoingError) { @Composable fun PeerPullCreatingPreview() { Surface { - PeerPullResultComposable(PeerOutgoingCreating) {} + OutgoingPullResultComposable(OutgoingCreating) {} } } @@ -159,8 +159,8 @@ fun PeerPullCreatingPreview() { fun PeerPullResponsePreview() { Surface { val talerUri = "https://example.org/foo/bar/can/be/very/long/url/so/fit/it/on/screen" - val response = PeerOutgoingResponse(talerUri, QrCodeManager.makeQrCode(talerUri)) - PeerPullResultComposable(response) {} + val response = OutgoingResponse(talerUri, QrCodeManager.makeQrCode(talerUri)) + OutgoingPullResultComposable(response) {} } } @@ -169,8 +169,8 @@ fun PeerPullResponsePreview() { fun PeerPullResponseLandscapePreview() { Surface { val talerUri = "https://example.org/foo/bar/can/be/very/long/url/so/fit/it/on/screen" - val response = PeerOutgoingResponse(talerUri, QrCodeManager.makeQrCode(talerUri)) - PeerPullResultComposable(response) {} + val response = OutgoingResponse(talerUri, QrCodeManager.makeQrCode(talerUri)) + OutgoingPullResultComposable(response) {} } } @@ -179,7 +179,7 @@ fun PeerPullResponseLandscapePreview() { fun PeerPullErrorPreview() { Surface { val json = JSONObject().apply { put("foo", "bar") } - val response = PeerOutgoingError(TalerErrorInfo(42, "hint", "message", json)) - PeerPullResultComposable(response) {} + val response = OutgoingError(TalerErrorInfo(42, "hint", "message", json)) + OutgoingPullResultComposable(response) {} } } diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerPushComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushIntroComposable.kt index 1399fbb..72c8862 100644 --- a/wallet/src/main/java/net/taler/wallet/peer/PeerPushComposable.kt +++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushIntroComposable.kt @@ -47,7 +47,7 @@ import net.taler.wallet.R import net.taler.wallet.getAmount @Composable -fun PeerPushIntroComposable( +fun OutgoingPushIntroComposable( currency: String, onSend: (amount: Amount, summary: String) -> Unit, ) { @@ -134,6 +134,6 @@ fun PeerPushIntroComposable( @Composable fun PeerPushIntroComposablePreview() { Surface { - PeerPushIntroComposable("TESTKUDOS") { _, _ -> } + OutgoingPushIntroComposable("TESTKUDOS") { _, _ -> } } } diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerPushResultComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushResultComposable.kt index b33fc4f..6d8b5dc 100644 --- a/wallet/src/main/java/net/taler/wallet/peer/PeerPushResultComposable.kt +++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushResultComposable.kt @@ -55,7 +55,7 @@ import net.taler.wallet.compose.getQrCodeSize import org.json.JSONObject @Composable -fun PeerPushResultComposable(state: PeerOutgoingState, onClose: () -> Unit) { +fun OutgoingPushResultComposable(state: OutgoingState, onClose: () -> Unit) { val scrollState = rememberScrollState() Column( modifier = Modifier @@ -68,10 +68,10 @@ fun PeerPushResultComposable(state: PeerOutgoingState, onClose: () -> Unit) { text = stringResource(id = R.string.send_peer_payment_instruction), ) when (state) { - PeerOutgoingIntro -> error("Result composable with PullPaymentIntro") - is PeerOutgoingCreating -> PeerPushCreatingComposable() - is PeerOutgoingResponse -> PeerPushResponseComposable(state) - is PeerOutgoingError -> PeerPushErrorComposable(state) + OutgoingIntro -> error("Result composable with PullPaymentIntro") + is OutgoingCreating -> PeerPushCreatingComposable() + is OutgoingResponse -> PeerPushResponseComposable(state) + is OutgoingError -> PeerPushErrorComposable(state) } Button(modifier = Modifier .padding(16.dp) @@ -94,7 +94,7 @@ private fun ColumnScope.PeerPushCreatingComposable() { } @Composable -private fun ColumnScope.PeerPushResponseComposable(state: PeerOutgoingResponse) { +private fun ColumnScope.PeerPushResponseComposable(state: OutgoingResponse) { val qrCodeSize = getQrCodeSize() Image( modifier = Modifier @@ -135,7 +135,7 @@ private fun ColumnScope.PeerPushResponseComposable(state: PeerOutgoingResponse) } @Composable -private fun ColumnScope.PeerPushErrorComposable(state: PeerOutgoingError) { +private fun ColumnScope.PeerPushErrorComposable(state: OutgoingError) { Text( modifier = Modifier .align(CenterHorizontally) @@ -150,7 +150,7 @@ private fun ColumnScope.PeerPushErrorComposable(state: PeerOutgoingError) { @Composable fun PeerPushCreatingPreview() { Surface { - PeerPushResultComposable(PeerOutgoingCreating) {} + OutgoingPushResultComposable(OutgoingCreating) {} } } @@ -159,8 +159,8 @@ fun PeerPushCreatingPreview() { fun PeerPushResponsePreview() { Surface { val talerUri = "https://example.org/foo/bar/can/be/very/long/url/so/fit/it/on/screen" - val response = PeerOutgoingResponse(talerUri, QrCodeManager.makeQrCode(talerUri)) - PeerPushResultComposable(response) {} + val response = OutgoingResponse(talerUri, QrCodeManager.makeQrCode(talerUri)) + OutgoingPushResultComposable(response) {} } } @@ -169,8 +169,8 @@ fun PeerPushResponsePreview() { fun PeerPushResponseLandscapePreview() { Surface { val talerUri = "https://example.org/foo/bar/can/be/very/long/url/so/fit/it/on/screen" - val response = PeerOutgoingResponse(talerUri, QrCodeManager.makeQrCode(talerUri)) - PeerPushResultComposable(response) {} + val response = OutgoingResponse(talerUri, QrCodeManager.makeQrCode(talerUri)) + OutgoingPushResultComposable(response) {} } } @@ -179,7 +179,7 @@ fun PeerPushResponseLandscapePreview() { fun PeerPushErrorPreview() { Surface { val json = JSONObject().apply { put("foo", "bar") } - val response = PeerOutgoingError(TalerErrorInfo(42, "hint", "message", json)) - PeerPushResultComposable(response) {} + val response = OutgoingError(TalerErrorInfo(42, "hint", "message", json)) + OutgoingPushResultComposable(response) {} } } diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerOutgoingState.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingState.kt index 0b6b2a8..0e01056 100644 --- a/wallet/src/main/java/net/taler/wallet/peer/PeerOutgoingState.kt +++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingState.kt @@ -20,17 +20,17 @@ 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( +sealed class OutgoingState +object OutgoingIntro : OutgoingState() +object OutgoingCreating : OutgoingState() +data class OutgoingResponse( val talerUri: String, val qrCode: Bitmap, -) : PeerOutgoingState() +) : OutgoingState() -data class PeerOutgoingError( +data class OutgoingError( val info: TalerErrorInfo, -) : PeerOutgoingState() +) : OutgoingState() @Serializable data class InitiatePeerPullPaymentResponse( 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 5bfd030..b02b2b6 100644 --- a/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt +++ b/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt @@ -34,17 +34,20 @@ class PeerManager( private val scope: CoroutineScope, ) { - private val _pullState = MutableStateFlow<PeerOutgoingState>(PeerOutgoingIntro) - val pullState: StateFlow<PeerOutgoingState> = _pullState + private val _outgoingPullState = MutableStateFlow<OutgoingState>(OutgoingIntro) + val pullState: StateFlow<OutgoingState> = _outgoingPullState - private val _pushState = MutableStateFlow<PeerOutgoingState>(PeerOutgoingIntro) - val pushState: StateFlow<PeerOutgoingState> = _pushState + private val _outgoingPushState = MutableStateFlow<OutgoingState>(OutgoingIntro) + val pushState: StateFlow<OutgoingState> = _outgoingPushState - private val _paymentState = MutableStateFlow<PeerIncomingState>(PeerIncomingChecking) - val paymentState: StateFlow<PeerIncomingState> = _paymentState + private val _incomingPullState = MutableStateFlow<IncomingState>(IncomingChecking) + val incomingPullState: StateFlow<IncomingState> = _incomingPullState + + private val _incomingPushState = MutableStateFlow<IncomingState>(IncomingChecking) + val incomingPushState: StateFlow<IncomingState> = _incomingPushState fun initiatePullPayment(amount: Amount, exchange: ExchangeItem) { - _pullState.value = PeerOutgoingCreating + _outgoingPullState.value = OutgoingCreating scope.launch(Dispatchers.IO) { api.request("initiatePeerPullPayment", InitiatePeerPullPaymentResponse.serializer()) { put("exchangeBaseUrl", exchange.exchangeBaseUrl) @@ -54,20 +57,20 @@ class PeerManager( }) }.onSuccess { val qrCode = QrCodeManager.makeQrCode(it.talerUri) - _pullState.value = PeerOutgoingResponse(it.talerUri, qrCode) + _outgoingPullState.value = OutgoingResponse(it.talerUri, qrCode) }.onError { error -> Log.e(TAG, "got initiatePeerPullPayment error result $error") - _pullState.value = PeerOutgoingError(error) + _outgoingPullState.value = OutgoingError(error) } } } fun resetPullPayment() { - _pullState.value = PeerOutgoingIntro + _outgoingPullState.value = OutgoingIntro } fun initiatePeerPushPayment(amount: Amount, summary: String) { - _pushState.value = PeerOutgoingCreating + _outgoingPushState.value = OutgoingCreating scope.launch(Dispatchers.IO) { api.request("initiatePeerPushPayment", InitiatePeerPushPaymentResponse.serializer()) { put("amount", amount.toJSONString()) @@ -76,46 +79,78 @@ class PeerManager( }) }.onSuccess { response -> val qrCode = QrCodeManager.makeQrCode(response.talerUri) - _pushState.value = PeerOutgoingResponse(response.talerUri, qrCode) + _outgoingPushState.value = OutgoingResponse(response.talerUri, qrCode) }.onError { error -> Log.e(TAG, "got initiatePeerPushPayment error result $error") - _pushState.value = PeerOutgoingError(error) + _outgoingPushState.value = OutgoingError(error) } } } fun resetPushPayment() { - _pushState.value = PeerOutgoingIntro + _outgoingPushState.value = OutgoingIntro } fun checkPeerPullPayment(talerUri: String) { - _paymentState.value = PeerIncomingChecking + _incomingPullState.value = IncomingChecking scope.launch(Dispatchers.IO) { api.request("checkPeerPullPayment", CheckPeerPullPaymentResponse.serializer()) { put("talerUri", talerUri) }.onSuccess { response -> - _paymentState.value = PeerIncomingTerms( + _incomingPullState.value = IncomingTerms( amount = response.amount, contractTerms = response.contractTerms, id = response.peerPullPaymentIncomingId, ) }.onError { error -> Log.e(TAG, "got checkPeerPushPayment error result $error") - _paymentState.value = PeerIncomingError(error) + _incomingPullState.value = IncomingError(error) } } } - fun acceptPeerPullPayment(terms: PeerIncomingTerms) { - _paymentState.value = PeerIncomingAccepting(terms) + fun acceptPeerPullPayment(terms: IncomingTerms) { + _incomingPullState.value = IncomingAccepting(terms) scope.launch(Dispatchers.IO) { api.request<Unit>("acceptPeerPullPayment") { put("peerPullPaymentIncomingId", terms.id) }.onSuccess { - _paymentState.value = PeerIncomingAccepted + _incomingPullState.value = IncomingAccepted + }.onError { error -> + Log.e(TAG, "got checkPeerPushPayment error result $error") + _incomingPullState.value = IncomingError(error) + } + } + } + + fun checkPeerPushPayment(talerUri: String) { + _incomingPushState.value = IncomingChecking + scope.launch(Dispatchers.IO) { + api.request("checkPeerPushPayment", CheckPeerPushPaymentResponse.serializer()) { + put("talerUri", talerUri) + }.onSuccess { response -> + _incomingPushState.value = IncomingTerms( + amount = response.amount, + contractTerms = response.contractTerms, + id = response.peerPushPaymentIncomingId, + ) + }.onError { error -> + Log.e(TAG, "got checkPeerPushPayment error result $error") + _incomingPushState.value = IncomingError(error) + } + } + } + + fun acceptPeerPushPayment(terms: IncomingTerms) { + _incomingPushState.value = IncomingAccepting(terms) + scope.launch(Dispatchers.IO) { + api.request<Unit>("acceptPeerPushPayment") { + put("peerPushPaymentIncomingId", terms.id) + }.onSuccess { + _incomingPushState.value = IncomingAccepted }.onError { error -> Log.e(TAG, "got checkPeerPushPayment error result $error") - _paymentState.value = PeerIncomingError(error) + _incomingPushState.value = IncomingError(error) } } } diff --git a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushCredit.kt b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushCredit.kt new file mode 100644 index 0000000..b986f57 --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushCredit.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 <http://www.gnu.org/licenses/> + */ + +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.TransactionPeerPushCredit + +@Composable +fun TransactionPeerPushCreditComposable(t: TransactionPeerPushCredit) { + TransactionAmountComposable( + label = stringResource(id = R.string.send_peer_payment_amount_received), + amount = t.amountEffective, + amountType = AmountType.Positive, + ) + TransactionAmountComposable( + label = stringResource(id = R.string.send_peer_payment_amount_sent), + 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 ?: "", + ) +} + +@Preview +@Composable +fun TransactionPeerPushCreditPreview() { + val t = TransactionPeerPushCredit( + 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", + ), + ) + 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 9b0c208..749ec30 100644 --- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionPeerFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionPeerFragment.kt @@ -50,6 +50,7 @@ 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.TransactionPeerPushCreditComposable 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 TransactionPeerPushCredit -> TransactionPeerPushCreditComposable(t) 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 6ef6c88..97ac5ea 100644 --- a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt +++ b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt @@ -361,7 +361,7 @@ class TransactionPeerPushCredit( @Transient override val amountType = AmountType.Positive override fun getTitle(context: Context): String { - return context.getString(R.string.transaction_peer_push_debit) + return context.getString(R.string.transaction_peer_push_credit) } - override val generalTitleRes = R.string.withdraw_title + override val generalTitleRes = R.string.transaction_peer_push_credit } diff --git a/wallet/src/main/res/navigation/nav_graph.xml b/wallet/src/main/res/navigation/nav_graph.xml index 3170216..f9060c5 100644 --- a/wallet/src/main/res/navigation/nav_graph.xml +++ b/wallet/src/main/res/navigation/nav_graph.xml @@ -131,7 +131,7 @@ <fragment android:id="@+id/nav_peer_pull" - android:name="net.taler.wallet.peer.PeerPullFragment" + android:name="net.taler.wallet.peer.OutgoingPullFragment" android:label="@string/receive_peer_title"> <argument android:name="amount" @@ -142,7 +142,7 @@ <fragment android:id="@+id/promptPullPayment" - android:name="net.taler.wallet.peer.PullPaymentFragment" + android:name="net.taler.wallet.peer.IncomingPullPaymentFragment" android:label="@string/pay_peer_title"> <action android:id="@+id/action_promptPullPayment_to_nav_main" @@ -151,6 +151,16 @@ </fragment> <fragment + android:id="@+id/promptPushPayment" + android:name="net.taler.wallet.peer.IncomingPushPaymentFragment" + android:label="@string/receive_peer_payment_title"> + <action + android:id="@+id/action_promptPushPayment_to_nav_main" + app:destination="@id/nav_main" + app:popUpTo="@id/nav_main" /> + </fragment> + + <fragment android:id="@+id/nav_transactions" android:name="net.taler.wallet.transactions.TransactionsFragment" android:label="@string/transactions_title" @@ -290,6 +300,10 @@ app:destination="@id/promptPullPayment" /> <action + android:id="@+id/action_global_prompt_push_payment" + app:destination="@id/promptPushPayment" /> + + <action android:id="@+id/action_global_pending_operations" app:destination="@id/nav_pending_operations" /> diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml index 8601b14..ab8984c 100644 --- a/wallet/src/main/res/values/strings.xml +++ b/wallet/src/main/res/values/strings.xml @@ -99,6 +99,7 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card <string name="transaction_peer_push_debit">Push payment</string> <string name="transaction_peer_pull_credit">Invoice</string> <string name="transaction_peer_pull_debit">Invoice paid</string> + <string name="transaction_peer_push_credit">Push payment</string> <string name="payment_title">Payment</string> <string name="payment_fee">+%s payment fee</string> @@ -127,9 +128,13 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card <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="send_peer_payment_amount_received">Amount received</string> + <string name="send_peer_payment_amount_sent">Amount sent</string> <string name="pay_peer_title">Pay invoice</string> <string name="pay_peer_intro">Do you want to pay this invoice?</string> + <string name="receive_peer_payment_title">Receive payment</string> + <string name="receive_peer_payment_intro">Do you want to receive this payment?</string> <string name="withdraw_initiated">Withdrawal initiated</string> <string name="withdraw_title">Withdrawal</string> |