From e350d497abe560aeeef88081ae93d73135ece00f Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 7 Sep 2022 16:28:04 -0300 Subject: [wallet] implement prototype for handling incoming pay-pull URI --- .../src/main/java/net/taler/wallet/MainActivity.kt | 4 + .../java/net/taler/wallet/SendFundsFragment.kt | 12 +- .../net/taler/wallet/peer/IncomingComposable.kt | 242 +++++++++++++++++++++ .../wallet/peer/IncomingPullPaymentFragment.kt | 68 ++++++ .../wallet/peer/IncomingPushPaymentFragment.kt | 68 ++++++ .../java/net/taler/wallet/peer/IncomingState.kt | 57 +++++ .../net/taler/wallet/peer/OutgoingPullFragment.kt | 86 ++++++++ .../wallet/peer/OutgoingPullIntroComposable.kt | 129 +++++++++++ .../wallet/peer/OutgoingPullResultComposable.kt | 185 ++++++++++++++++ .../wallet/peer/OutgoingPushIntroComposable.kt | 139 ++++++++++++ .../wallet/peer/OutgoingPushResultComposable.kt | 185 ++++++++++++++++ .../java/net/taler/wallet/peer/OutgoingState.kt | 47 ++++ .../net/taler/wallet/peer/PeerIncomingState.kt | 50 ----- .../main/java/net/taler/wallet/peer/PeerManager.kt | 77 +++++-- .../net/taler/wallet/peer/PeerOutgoingState.kt | 47 ---- .../java/net/taler/wallet/peer/PeerPullFragment.kt | 86 -------- .../taler/wallet/peer/PeerPullIntroComposable.kt | 129 ----------- .../taler/wallet/peer/PeerPullPaymentComposable.kt | 223 ------------------- .../taler/wallet/peer/PeerPullResultComposable.kt | 185 ---------------- .../net/taler/wallet/peer/PeerPushComposable.kt | 139 ------------ .../taler/wallet/peer/PeerPushResultComposable.kt | 185 ---------------- .../net/taler/wallet/peer/PullPaymentFragment.kt | 68 ------ .../taler/wallet/peer/TransactionPeerPushCredit.kt | 77 +++++++ .../wallet/transactions/TransactionPeerFragment.kt | 3 +- .../net/taler/wallet/transactions/Transactions.kt | 4 +- 25 files changed, 1353 insertions(+), 1142 deletions(-) create mode 100644 wallet/src/main/java/net/taler/wallet/peer/IncomingComposable.kt create mode 100644 wallet/src/main/java/net/taler/wallet/peer/IncomingPullPaymentFragment.kt create mode 100644 wallet/src/main/java/net/taler/wallet/peer/IncomingPushPaymentFragment.kt create mode 100644 wallet/src/main/java/net/taler/wallet/peer/IncomingState.kt create mode 100644 wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt create mode 100644 wallet/src/main/java/net/taler/wallet/peer/OutgoingPullIntroComposable.kt create mode 100644 wallet/src/main/java/net/taler/wallet/peer/OutgoingPullResultComposable.kt create mode 100644 wallet/src/main/java/net/taler/wallet/peer/OutgoingPushIntroComposable.kt create mode 100644 wallet/src/main/java/net/taler/wallet/peer/OutgoingPushResultComposable.kt create mode 100644 wallet/src/main/java/net/taler/wallet/peer/OutgoingState.kt delete mode 100644 wallet/src/main/java/net/taler/wallet/peer/PeerIncomingState.kt delete mode 100644 wallet/src/main/java/net/taler/wallet/peer/PeerOutgoingState.kt delete mode 100644 wallet/src/main/java/net/taler/wallet/peer/PeerPullFragment.kt delete mode 100644 wallet/src/main/java/net/taler/wallet/peer/PeerPullIntroComposable.kt delete mode 100644 wallet/src/main/java/net/taler/wallet/peer/PeerPullPaymentComposable.kt delete mode 100644 wallet/src/main/java/net/taler/wallet/peer/PeerPullResultComposable.kt delete mode 100644 wallet/src/main/java/net/taler/wallet/peer/PeerPushComposable.kt delete mode 100644 wallet/src/main/java/net/taler/wallet/peer/PeerPushResultComposable.kt delete mode 100644 wallet/src/main/java/net/taler/wallet/peer/PullPaymentFragment.kt create mode 100644 wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushCredit.kt (limited to 'wallet/src/main/java/net/taler') 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/IncomingComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/IncomingComposable.kt new file mode 100644 index 0000000..0095bc4 --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/peer/IncomingComposable.kt @@ -0,0 +1,242 @@ +/* + * 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.annotation.StringRes +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 + +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 IncomingComposable( + state: State, + data: IncomingData, + onAccept: (IncomingTerms) -> Unit, +) { + val scrollState = rememberScrollState() + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState), + ) { + Text( + modifier = Modifier + .padding(16.dp) + .align(CenterHorizontally), + text = stringResource(id = data.intro), + ) + when (val s = state.value) { + IncomingChecking -> PeerPullCheckingComposable() + is IncomingTerms -> PeerPullTermsComposable(s, onAccept, data) + is IncomingAccepting -> PeerPullTermsComposable(s, onAccept, data) + IncomingAccepted -> { + // we navigate away, don't show anything + } + is IncomingError -> PeerPullErrorComposable(s) + } + } +} + +@Composable +fun ColumnScope.PeerPullCheckingComposable() { + CircularProgressIndicator( + modifier = Modifier + .align(CenterHorizontally) + .fillMaxSize(0.75f), + ) +} + +@Composable +fun ColumnScope.PeerPullTermsComposable( + terms: IncomingTerms, + onAccept: (IncomingTerms) -> Unit, + data: IncomingData, +) { + 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 IncomingAccepting) { + 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 = data.button), + ) + } + } + } + } +} + +@Composable +fun ColumnScope.PeerPullErrorComposable(s: IncomingError) { + 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(IncomingChecking) + IncomingComposable(s, incomingPush) {} + } +} + +@Preview +@Composable +fun PeerPullTermsPreview() { + Surface { + 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", + amount = Amount.fromDouble("TESTKUDOS", 23.42), + ), + id = "ID123", + ) + + @SuppressLint("UnrememberedMutableState") + val s = mutableStateOf(terms) + IncomingComposable(s, incomingPush) {} + } +} + +@Preview +@Composable +fun PeerPullAcceptingPreview() { + Surface { + 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", + amount = Amount.fromDouble("TESTKUDOS", 23.42), + ), + id = "ID123", + ) + + @SuppressLint("UnrememberedMutableState") + val s = mutableStateOf(IncomingAccepting(terms)) + IncomingComposable(s, incomingPush) {} + } +} + +@Preview +@Composable +fun PeerPullPayErrorPreview() { + Surface { + @SuppressLint("UnrememberedMutableState") + val s = mutableStateOf(IncomingError(TalerErrorInfo(42, "hint", "msg"))) + IncomingComposable(s, incomingPush) {} + } +} diff --git a/wallet/src/main/java/net/taler/wallet/peer/IncomingPullPaymentFragment.kt b/wallet/src/main/java/net/taler/wallet/peer/IncomingPullPaymentFragment.kt new file mode 100644 index 0000000..cd2f39b --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/peer/IncomingPullPaymentFragment.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 IncomingPullPaymentFragment : 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.incomingPullState.collect { + if (it is IncomingAccepted) { + findNavController().navigate(R.id.action_promptPullPayment_to_nav_main) + } + } + } + return ComposeView(requireContext()).apply { + setContent { + MdcTheme { + Surface { + val state = peerManager.incomingPullState.collectAsStateLifecycleAware() + IncomingComposable(state, incomingPull) { 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/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 + */ + +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/IncomingState.kt b/wallet/src/main/java/net/taler/wallet/peer/IncomingState.kt new file mode 100644 index 0000000..7ca38c4 --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/peer/IncomingState.kt @@ -0,0 +1,57 @@ +/* + * 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 IncomingState +object IncomingChecking : IncomingState() +open class IncomingTerms( + val amount: Amount, + val contractTerms: PeerContractTerms, + val id: String, +) : IncomingState() + +class IncomingAccepting(s: IncomingTerms) : + IncomingTerms(s.amount, s.contractTerms, s.id) + +object IncomingAccepted : IncomingState() +data class IncomingError( + val info: TalerErrorInfo, +) : IncomingState() + +@Serializable +data class PeerContractTerms( + val summary: String, + val amount: Amount, +) + +@Serializable +data class CheckPeerPullPaymentResponse( + val amount: Amount, + 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/OutgoingPullFragment.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt new file mode 100644 index 0000000..b1593ff --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.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 + */ + +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 OutgoingPullFragment : 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 OutgoingIntro) { + val exchangeState = + exchangeFlow.collectAsStateLifecycleAware(initial = null) + OutgoingPullIntroComposable( + amount = amount, + exchangeState = exchangeState, + onCreateInvoice = this@OutgoingPullFragment::onCreateInvoice, + ) + } else { + OutgoingPullResultComposable(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/OutgoingPullIntroComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullIntroComposable.kt new file mode 100644 index 0000000..a338836 --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullIntroComposable.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 OutgoingPullIntroComposable( + 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())) + OutgoingPullIntroComposable(Amount.fromDouble("TESTKUDOS", 42.23), exchangeFlow) { _, _ -> } + } +} diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullResultComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullResultComposable.kt new file mode 100644 index 0000000..2c4001f --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullResultComposable.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 + */ + +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 OutgoingPullResultComposable(state: OutgoingState, 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) { + OutgoingIntro -> error("Result composable with PullPaymentIntro") + is OutgoingCreating -> PeerPullCreatingComposable() + is OutgoingResponse -> PeerPullResponseComposable(state) + is OutgoingError -> 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: OutgoingResponse) { + 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: OutgoingError) { + 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 { + OutgoingPullResultComposable(OutgoingCreating) {} + } +} + +@Preview +@Composable +fun PeerPullResponsePreview() { + Surface { + val talerUri = "https://example.org/foo/bar/can/be/very/long/url/so/fit/it/on/screen" + val response = OutgoingResponse(talerUri, QrCodeManager.makeQrCode(talerUri)) + OutgoingPullResultComposable(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 = OutgoingResponse(talerUri, QrCodeManager.makeQrCode(talerUri)) + OutgoingPullResultComposable(response) {} + } +} + +@Preview +@Composable +fun PeerPullErrorPreview() { + Surface { + val json = JSONObject().apply { put("foo", "bar") } + val response = OutgoingError(TalerErrorInfo(42, "hint", "message", json)) + OutgoingPullResultComposable(response) {} + } +} diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushIntroComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushIntroComposable.kt new file mode 100644 index 0000000..72c8862 --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushIntroComposable.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 + */ + +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 OutgoingPushIntroComposable( + 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 { + OutgoingPushIntroComposable("TESTKUDOS") { _, _ -> } + } +} diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushResultComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushResultComposable.kt new file mode 100644 index 0000000..6d8b5dc --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushResultComposable.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 + */ + +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 OutgoingPushResultComposable(state: OutgoingState, 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) { + OutgoingIntro -> error("Result composable with PullPaymentIntro") + is OutgoingCreating -> PeerPushCreatingComposable() + is OutgoingResponse -> PeerPushResponseComposable(state) + is OutgoingError -> 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: OutgoingResponse) { + 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: OutgoingError) { + 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 { + OutgoingPushResultComposable(OutgoingCreating) {} + } +} + +@Preview +@Composable +fun PeerPushResponsePreview() { + Surface { + val talerUri = "https://example.org/foo/bar/can/be/very/long/url/so/fit/it/on/screen" + val response = OutgoingResponse(talerUri, QrCodeManager.makeQrCode(talerUri)) + OutgoingPushResultComposable(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 = OutgoingResponse(talerUri, QrCodeManager.makeQrCode(talerUri)) + OutgoingPushResultComposable(response) {} + } +} + +@Preview +@Composable +fun PeerPushErrorPreview() { + Surface { + val json = JSONObject().apply { put("foo", "bar") } + val response = OutgoingError(TalerErrorInfo(42, "hint", "message", json)) + OutgoingPushResultComposable(response) {} + } +} diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingState.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingState.kt new file mode 100644 index 0000000..0e01056 --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingState.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 OutgoingState +object OutgoingIntro : OutgoingState() +object OutgoingCreating : OutgoingState() +data class OutgoingResponse( + val talerUri: String, + val qrCode: Bitmap, +) : OutgoingState() + +data class OutgoingError( + val info: TalerErrorInfo, +) : OutgoingState() + +@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/PeerIncomingState.kt b/wallet/src/main/java/net/taler/wallet/peer/PeerIncomingState.kt deleted file mode 100644 index c021c2f..0000000 --- a/wallet/src/main/java/net/taler/wallet/peer/PeerIncomingState.kt +++ /dev/null @@ -1,50 +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 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 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(PeerOutgoingIntro) - val pullState: StateFlow = _pullState + private val _outgoingPullState = MutableStateFlow(OutgoingIntro) + val pullState: StateFlow = _outgoingPullState - private val _pushState = MutableStateFlow(PeerOutgoingIntro) - val pushState: StateFlow = _pushState + private val _outgoingPushState = MutableStateFlow(OutgoingIntro) + val pushState: StateFlow = _outgoingPushState - private val _paymentState = MutableStateFlow(PeerIncomingChecking) - val paymentState: StateFlow = _paymentState + private val _incomingPullState = MutableStateFlow(IncomingChecking) + val incomingPullState: StateFlow = _incomingPullState + + private val _incomingPushState = MutableStateFlow(IncomingChecking) + val incomingPushState: StateFlow = _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("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("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/PeerOutgoingState.kt b/wallet/src/main/java/net/taler/wallet/peer/PeerOutgoingState.kt deleted file mode 100644 index 0b6b2a8..0000000 --- a/wallet/src/main/java/net/taler/wallet/peer/PeerOutgoingState.kt +++ /dev/null @@ -1,47 +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.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/PeerPullFragment.kt b/wallet/src/main/java/net/taler/wallet/peer/PeerPullFragment.kt deleted file mode 100644 index be79e9d..0000000 --- a/wallet/src/main/java/net/taler/wallet/peer/PeerPullFragment.kt +++ /dev/null @@ -1,86 +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.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 PeerOutgoingIntro) { - 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/PeerPullIntroComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/PeerPullIntroComposable.kt deleted file mode 100644 index 02f2c7c..0000000 --- a/wallet/src/main/java/net/taler/wallet/peer/PeerPullIntroComposable.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/PeerPullPaymentComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/PeerPullPaymentComposable.kt deleted file mode 100644 index fff74ea..0000000 --- a/wallet/src/main/java/net/taler/wallet/peer/PeerPullPaymentComposable.kt +++ /dev/null @@ -1,223 +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.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 deleted file mode 100644 index d37ca4b..0000000 --- a/wallet/src/main/java/net/taler/wallet/peer/PeerPullResultComposable.kt +++ /dev/null @@ -1,185 +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.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: PeerOutgoingState, 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) { - PeerOutgoingIntro -> error("Result composable with PullPaymentIntro") - is PeerOutgoingCreating -> PeerPullCreatingComposable() - is PeerOutgoingResponse -> PeerPullResponseComposable(state) - is PeerOutgoingError -> 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: PeerOutgoingResponse) { - 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: PeerOutgoingError) { - 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(PeerOutgoingCreating) {} - } -} - -@Preview -@Composable -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) {} - } -} - -@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 = PeerOutgoingResponse(talerUri, QrCodeManager.makeQrCode(talerUri)) - PeerPullResultComposable(response) {} - } -} - -@Preview -@Composable -fun PeerPullErrorPreview() { - Surface { - val json = JSONObject().apply { put("foo", "bar") } - val response = PeerOutgoingError(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 deleted file mode 100644 index 1399fbb..0000000 --- a/wallet/src/main/java/net/taler/wallet/peer/PeerPushComposable.kt +++ /dev/null @@ -1,139 +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 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 deleted file mode 100644 index b33fc4f..0000000 --- a/wallet/src/main/java/net/taler/wallet/peer/PeerPushResultComposable.kt +++ /dev/null @@ -1,185 +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.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: PeerOutgoingState, 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) { - PeerOutgoingIntro -> error("Result composable with PullPaymentIntro") - is PeerOutgoingCreating -> PeerPushCreatingComposable() - is PeerOutgoingResponse -> PeerPushResponseComposable(state) - is PeerOutgoingError -> 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: PeerOutgoingResponse) { - 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: PeerOutgoingError) { - 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(PeerOutgoingCreating) {} - } -} - -@Preview -@Composable -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) {} - } -} - -@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 = PeerOutgoingResponse(talerUri, QrCodeManager.makeQrCode(talerUri)) - PeerPushResultComposable(response) {} - } -} - -@Preview -@Composable -fun PeerPushErrorPreview() { - Surface { - val json = JSONObject().apply { put("foo", "bar") } - 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 deleted file mode 100644 index 71b1bcc..0000000 --- a/wallet/src/main/java/net/taler/wallet/peer/PullPaymentFragment.kt +++ /dev/null @@ -1,68 +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.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/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 + */ + +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 } -- cgit v1.2.3