From 77cd01bf1a23fc218d265a97925624f066c3236b Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 22 Feb 2023 14:29:26 -0300 Subject: [wallet] show fees for peer pull credit --- .../main/java/net/taler/wallet/MainViewModel.kt | 2 +- .../java/net/taler/wallet/ReceiveFundsFragment.kt | 2 + .../net/taler/wallet/exchanges/ExchangeManager.kt | 14 +++- .../net/taler/wallet/peer/OutgoingPullFragment.kt | 25 ++++--- .../wallet/peer/OutgoingPullIntroComposable.kt | 79 +++++++++++++--------- .../wallet/peer/OutgoingPushIntroComposable.kt | 6 +- .../java/net/taler/wallet/peer/OutgoingState.kt | 9 +++ .../main/java/net/taler/wallet/peer/PeerManager.kt | 31 +++++++++ 8 files changed, 117 insertions(+), 51 deletions(-) (limited to 'wallet/src') diff --git a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt index 255c28b..ed12533 100644 --- a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt +++ b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt @@ -99,7 +99,7 @@ class MainViewModel(val app: Application) : AndroidViewModel(app) { val transactionManager: TransactionManager = TransactionManager(api, viewModelScope) val refundManager = RefundManager(api, viewModelScope) val exchangeManager: ExchangeManager = ExchangeManager(api, viewModelScope) - val peerManager: PeerManager = PeerManager(api, viewModelScope) + val peerManager: PeerManager = PeerManager(api, exchangeManager, viewModelScope) val settingsManager: SettingsManager = SettingsManager(app.applicationContext, viewModelScope) val accountManager: AccountManager = AccountManager(api, viewModelScope) val depositManager: DepositManager = DepositManager(api, viewModelScope) diff --git a/wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt b/wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt index 4fbb09b..0e362ac 100644 --- a/wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt @@ -62,6 +62,7 @@ import net.taler.wallet.exchanges.ExchangeItem class ReceiveFundsFragment : 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?, @@ -107,6 +108,7 @@ class ReceiveFundsFragment : Fragment() { private fun onPeerPull(amount: Amount) { val bundle = bundleOf("amount" to amount.toJSONString()) + peerManager.checkPeerPullCredit(amount) findNavController().navigate(R.id.action_receiveFunds_to_nav_peer_pull, bundle) } } diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt index 5a4c6c2..4a57068 100644 --- a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt +++ b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt @@ -17,6 +17,7 @@ package net.taler.wallet.exchanges import android.util.Log +import androidx.annotation.WorkerThread import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.CoroutineScope @@ -81,13 +82,20 @@ class ExchangeManager( } fun findExchangeForCurrency(currency: String): Flow = flow { - val response = api.request("listExchanges", ExchangeListResponse.serializer()) + emit(findExchange(currency)) + } + + @WorkerThread + suspend fun findExchange(currency: String): ExchangeItem? { var exchange: ExchangeItem? = null - response.onSuccess { exchangeListResponse -> + api.request( + operation = "listExchanges", + serializer = ExchangeListResponse.serializer() + ).onSuccess { exchangeListResponse -> // just pick the first for now exchange = exchangeListResponse.exchanges.find { it.currency == currency } } - emit(exchange) + return exchange } } diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt index 5dc1af7..cccae0f 100644 --- a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt @@ -44,22 +44,21 @@ class OutgoingPullFragment : Fragment() { val amount = arguments?.getString("amount")?.let { Amount.fromJSONString(it) } ?: error("no amount passed") - val exchangeFlow = exchangeManager.findExchangeForCurrency(amount.currency) return ComposeView(requireContext()).apply { setContent { TalerSurface { - 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() + when (val state = peerManager.pullState.collectAsStateLifecycleAware().value) { + is OutgoingIntro, OutgoingChecking, is OutgoingChecked -> { + OutgoingPullIntroComposable( + amount = amount, + state = state, + onCreateInvoice = this@OutgoingPullFragment::onCreateInvoice, + ) + } + OutgoingCreating, is OutgoingResponse, is OutgoingError -> { + OutgoingPullResultComposable(state) { + findNavController().popBackStack() + } } } } diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullIntroComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullIntroComposable.kt index 6d74ba6..a7cd2a8 100644 --- a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullIntroComposable.kt +++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullIntroComposable.kt @@ -16,21 +16,19 @@ 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.material3.MaterialTheme import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Surface import androidx.compose.material3.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 @@ -41,34 +39,36 @@ 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.text.style.TextAlign 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 +import net.taler.wallet.transactions.AmountType +import net.taler.wallet.transactions.TransactionAmountComposable +import net.taler.wallet.transactions.TransactionInfoComposable +import kotlin.random.Random @OptIn(ExperimentalMaterial3Api::class) @Composable fun OutgoingPullIntroComposable( amount: Amount, - exchangeState: State, + state: OutgoingState, onCreateInvoice: (amount: Amount, subject: String, exchange: ExchangeItem) -> Unit, ) { val scrollState = rememberScrollState() Column( modifier = Modifier .fillMaxWidth() + .padding(16.dp) .verticalScroll(scrollState), horizontalAlignment = CenterHorizontally, ) { var subject by rememberSaveable { mutableStateOf("") } val focusRequester = remember { FocusRequester() } - val exchangeItem = exchangeState.value OutlinedTextField( modifier = Modifier .fillMaxWidth() @@ -101,30 +101,33 @@ fun OutgoingPullIntroComposable( text = stringResource(R.string.char_count, subject.length, MAX_LENGTH_SUBJECT), textAlign = TextAlign.End, ) - Text( - modifier = Modifier.padding(horizontal = 16.dp), - text = stringResource(id = R.string.amount_chosen), + TransactionAmountComposable( + label = stringResource(id = R.string.amount_chosen), + amount = amount, + amountType = AmountType.Positive, ) - 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), + if (state is OutgoingChecked) { + val fee = state.amountRaw - state.amountEffective + if (!fee.isZero()) TransactionAmountComposable( + label = stringResource(id = R.string.withdraw_fees), + amount = fee, + amountType = AmountType.Negative, + ) + } + val exchangeItem = (state as? OutgoingChecked)?.exchangeItem + TransactionInfoComposable( + label = stringResource(id = R.string.withdraw_exchange), + info = if (exchangeItem == null) "" else cleanExchange(exchangeItem.exchangeBaseUrl), ) Button( modifier = Modifier.padding(16.dp), - enabled = subject.isNotBlank() && exchangeItem != null, + enabled = subject.isNotBlank() && state is OutgoingChecked, onClick = { - onCreateInvoice(amount, subject, exchangeItem ?: error("clickable without exchange")) + onCreateInvoice( + amount, + subject, + exchangeItem ?: error("clickable without exchange") + ) }, ) { Text(text = stringResource(R.string.receive_peer_create_button)) @@ -134,11 +137,25 @@ fun OutgoingPullIntroComposable( @Preview @Composable -fun PreviewReceiveFundsIntro() { +fun PreviewReceiveFundsCheckingIntro() { + Surface { + OutgoingPullIntroComposable( + Amount.fromDouble("TESTKUDOS", 42.23), + if (Random.nextBoolean()) OutgoingIntro else OutgoingChecking, + ) { _, _, _ -> } + } +} + +@Preview +@Composable +fun PreviewReceiveFundsCheckedIntro() { Surface { - @SuppressLint("UnrememberedMutableState") - val exchangeFlow = - mutableStateOf(ExchangeItem("https://example.org", "TESTKUDOS", emptyList())) - OutgoingPullIntroComposable(Amount.fromDouble("TESTKUDOS", 42.23), exchangeFlow) { _, _, _ -> } + val amountRaw = Amount.fromDouble("TESTKUDOS", 42.42) + val amountEffective = Amount.fromDouble("TESTKUDOS", 42.23) + val exchangeItem = ExchangeItem("https://example.org", "TESTKUDOS", emptyList()) + OutgoingPullIntroComposable( + Amount.fromDouble("TESTKUDOS", 42.23), + OutgoingChecked(amountRaw, amountEffective, exchangeItem) + ) { _, _, _ -> } } } diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushIntroComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushIntroComposable.kt index 7d109c7..33e8390 100644 --- a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushIntroComposable.kt +++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushIntroComposable.kt @@ -127,9 +127,9 @@ fun PeerPushIntroComposableCheckingPreview() { @Composable fun PeerPushIntroComposableCheckedPreview() { Surface { - val amountEffective = Amount.fromDouble("TESTKUDOS", 42.23) - val amountRaw = Amount.fromDouble("TESTKUDOS", 42.42) - val state = OutgoingChecked(amountEffective, amountRaw) + val amountEffective = Amount.fromDouble("TESTKUDOS", 42.42) + val amountRaw = Amount.fromDouble("TESTKUDOS", 42.23) + val state = OutgoingChecked(amountRaw, amountEffective) OutgoingPushIntroComposable(state, amountEffective) { _, _ -> } } } diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingState.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingState.kt index b0a31d2..5673417 100644 --- a/wallet/src/main/java/net/taler/wallet/peer/OutgoingState.kt +++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingState.kt @@ -20,6 +20,7 @@ import android.graphics.Bitmap import kotlinx.serialization.Serializable import net.taler.common.Amount import net.taler.wallet.backend.TalerErrorInfo +import net.taler.wallet.exchanges.ExchangeItem sealed class OutgoingState object OutgoingIntro : OutgoingState() @@ -27,6 +28,7 @@ object OutgoingChecking : OutgoingState() data class OutgoingChecked( val amountRaw: Amount, val amountEffective: Amount, + val exchangeItem: ExchangeItem? = null, ) : OutgoingState() object OutgoingCreating : OutgoingState() data class OutgoingResponse( @@ -38,6 +40,13 @@ data class OutgoingError( val info: TalerErrorInfo, ) : OutgoingState() +@Serializable +data class CheckPeerPullCreditResponse( + val exchangeBaseUrl: String, + val amountRaw: Amount, + val amountEffective: Amount, +) + @Serializable data class InitiatePeerPullPaymentResponse( /** diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt b/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt index 7875c6f..f031d44 100644 --- a/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt +++ b/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt @@ -28,8 +28,11 @@ import net.taler.common.Amount import net.taler.common.QrCodeManager import net.taler.common.Timestamp import net.taler.wallet.TAG +import net.taler.wallet.backend.TalerErrorCode.UNKNOWN +import net.taler.wallet.backend.TalerErrorInfo import net.taler.wallet.backend.WalletBackendApi import net.taler.wallet.exchanges.ExchangeItem +import net.taler.wallet.exchanges.ExchangeManager import org.json.JSONObject import java.util.concurrent.TimeUnit.DAYS @@ -37,6 +40,7 @@ const val MAX_LENGTH_SUBJECT = 100 class PeerManager( private val api: WalletBackendApi, + private val exchangeManager: ExchangeManager, private val scope: CoroutineScope, ) { @@ -52,6 +56,32 @@ class PeerManager( private val _incomingPushState = MutableStateFlow(IncomingChecking) val incomingPushState: StateFlow = _incomingPushState + fun checkPeerPullCredit(amount: Amount) { + _outgoingPullState.value = OutgoingChecking + scope.launch(Dispatchers.IO) { + val exchangeItem = exchangeManager.findExchange(amount.currency) + if (exchangeItem == null) { + _outgoingPullState.value = OutgoingError( + TalerErrorInfo(UNKNOWN, "No exchange found for ${amount.currency}") + ) + return@launch + } + api.request("checkPeerPullCredit", CheckPeerPullCreditResponse.serializer()) { + put("exchangeBaseUrl", exchangeItem.exchangeBaseUrl) + put("amount", amount.toJSONString()) + }.onSuccess { + _outgoingPullState.value = OutgoingChecked( + amountRaw = it.amountRaw, + amountEffective = it.amountEffective, + exchangeItem = exchangeItem, + ) + }.onError { error -> + Log.e(TAG, "got checkPeerPullCredit error result $error") + _outgoingPullState.value = OutgoingError(error) + } + } + } + fun initiatePeerPullCredit(amount: Amount, summary: String, exchange: ExchangeItem) { _outgoingPullState.value = OutgoingCreating scope.launch(Dispatchers.IO) { @@ -86,6 +116,7 @@ class PeerManager( _outgoingPushState.value = OutgoingChecked( amountRaw = response.amountRaw, amountEffective = response.amountEffective, + // FIXME add exchangeItem once available in API ) }.onError { error -> Log.e(TAG, "got checkPeerPushDebit error result $error") -- cgit v1.2.3