diff options
16 files changed, 562 insertions, 68 deletions
diff --git a/wallet/src/main/java/net/taler/wallet/MainActivity.kt b/wallet/src/main/java/net/taler/wallet/MainActivity.kt index ea604c4..2797a69 100644 --- a/wallet/src/main/java/net/taler/wallet/MainActivity.kt +++ b/wallet/src/main/java/net/taler/wallet/MainActivity.kt @@ -253,6 +253,10 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener, model.showProgressBar.value = true model.refundManager.refund(u).observe(this, Observer(::onRefundResponse)) } + action.startsWith("pay-pull/") -> { + nav.navigate(R.id.action_global_prompt_pull_payment) + model.peerManager.checkPeerPullPayment(u) + } else -> { showError(R.string.error_unsupported_uri, "From: $from\nURI: $u") } diff --git a/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt b/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt index c67b345..27f2c96 100644 --- a/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt @@ -28,7 +28,7 @@ import androidx.navigation.findNavController import com.google.android.material.composethemeadapter.MdcTheme import net.taler.common.Amount import net.taler.wallet.compose.collectAsStateLifecycleAware -import net.taler.wallet.peer.PeerPaymentIntro +import net.taler.wallet.peer.PeerOutgoingIntro import net.taler.wallet.peer.PeerPushIntroComposable import net.taler.wallet.peer.PeerPushResultComposable @@ -45,7 +45,7 @@ class SendFundsFragment : Fragment() { MdcTheme { Surface { val state = peerManager.pushState.collectAsStateLifecycleAware() - if (state.value is PeerPaymentIntro) { + if (state.value is PeerOutgoingIntro) { val currency = transactionManager.selectedCurrency ?: error("No currency selected") PeerPushIntroComposable(currency, this@SendFundsFragment::onSend) diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerIncomingState.kt b/wallet/src/main/java/net/taler/wallet/peer/PeerIncomingState.kt new file mode 100644 index 0000000..c021c2f --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/peer/PeerIncomingState.kt @@ -0,0 +1,50 @@ +/* + * This file is part of GNU Taler + * (C) 2022 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +package net.taler.wallet.peer + +import kotlinx.serialization.Serializable +import net.taler.common.Amount +import net.taler.wallet.backend.TalerErrorInfo + +sealed class PeerIncomingState +object PeerIncomingChecking : PeerIncomingState() +open class PeerIncomingTerms( + val amount: Amount, + val contractTerms: PeerContractTerms, + val id: String, +) : PeerIncomingState() + +class PeerIncomingAccepting(s: PeerIncomingTerms) : + PeerIncomingTerms(s.amount, s.contractTerms, s.id) + +object PeerIncomingAccepted : PeerIncomingState() +data class PeerIncomingError( + val info: TalerErrorInfo, +) : PeerIncomingState() + +@Serializable +data class PeerContractTerms( + val summary: String, + val amount: Amount, +) + +@Serializable +data class CheckPeerPullPaymentResponse( + val amount: Amount, + val contractTerms: PeerContractTerms, + val peerPullPaymentIncomingId: String, +) diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt b/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt index 898dcfd..5bfd030 100644 --- a/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt +++ b/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt @@ -16,18 +16,15 @@ package net.taler.wallet.peer -import android.graphics.Bitmap import android.util.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -import kotlinx.serialization.Serializable import net.taler.common.Amount import net.taler.common.QrCodeManager import net.taler.wallet.TAG -import net.taler.wallet.backend.TalerErrorInfo import net.taler.wallet.backend.WalletBackendApi import net.taler.wallet.exchanges.ExchangeItem import org.json.JSONObject @@ -37,14 +34,17 @@ class PeerManager( private val scope: CoroutineScope, ) { - private val _pullState = MutableStateFlow<PeerPaymentState>(PeerPaymentIntro) - val pullState: StateFlow<PeerPaymentState> = _pullState + private val _pullState = MutableStateFlow<PeerOutgoingState>(PeerOutgoingIntro) + val pullState: StateFlow<PeerOutgoingState> = _pullState - private val _pushState = MutableStateFlow<PeerPaymentState>(PeerPaymentIntro) - val pushState: StateFlow<PeerPaymentState> = _pushState + private val _pushState = MutableStateFlow<PeerOutgoingState>(PeerOutgoingIntro) + val pushState: StateFlow<PeerOutgoingState> = _pushState + + private val _paymentState = MutableStateFlow<PeerIncomingState>(PeerIncomingChecking) + val paymentState: StateFlow<PeerIncomingState> = _paymentState fun initiatePullPayment(amount: Amount, exchange: ExchangeItem) { - _pullState.value = PeerPaymentCreating + _pullState.value = PeerOutgoingCreating scope.launch(Dispatchers.IO) { api.request("initiatePeerPullPayment", InitiatePeerPullPaymentResponse.serializer()) { put("exchangeBaseUrl", exchange.exchangeBaseUrl) @@ -54,20 +54,20 @@ class PeerManager( }) }.onSuccess { val qrCode = QrCodeManager.makeQrCode(it.talerUri) - _pullState.value = PeerPaymentResponse(it.talerUri, qrCode) + _pullState.value = PeerOutgoingResponse(it.talerUri, qrCode) }.onError { error -> Log.e(TAG, "got initiatePeerPullPayment error result $error") - _pullState.value = PeerPaymentError(error) + _pullState.value = PeerOutgoingError(error) } } } fun resetPullPayment() { - _pullState.value = PeerPaymentIntro + _pullState.value = PeerOutgoingIntro } fun initiatePeerPushPayment(amount: Amount, summary: String) { - _pushState.value = PeerPaymentCreating + _pushState.value = PeerOutgoingCreating scope.launch(Dispatchers.IO) { api.request("initiatePeerPushPayment", InitiatePeerPushPaymentResponse.serializer()) { put("amount", amount.toJSONString()) @@ -76,42 +76,48 @@ class PeerManager( }) }.onSuccess { response -> val qrCode = QrCodeManager.makeQrCode(response.talerUri) - _pushState.value = PeerPaymentResponse(response.talerUri, qrCode) + _pushState.value = PeerOutgoingResponse(response.talerUri, qrCode) }.onError { error -> Log.e(TAG, "got initiatePeerPushPayment error result $error") - _pushState.value = PeerPaymentError(error) + _pushState.value = PeerOutgoingError(error) } } } fun resetPushPayment() { - _pushState.value = PeerPaymentIntro + _pushState.value = PeerOutgoingIntro } -} - -sealed class PeerPaymentState -object PeerPaymentIntro : PeerPaymentState() -object PeerPaymentCreating : PeerPaymentState() -data class PeerPaymentResponse( - val talerUri: String, - val qrCode: Bitmap, -) : PeerPaymentState() - -data class PeerPaymentError( - val info: TalerErrorInfo, -) : PeerPaymentState() + fun checkPeerPullPayment(talerUri: String) { + _paymentState.value = PeerIncomingChecking + scope.launch(Dispatchers.IO) { + api.request("checkPeerPullPayment", CheckPeerPullPaymentResponse.serializer()) { + put("talerUri", talerUri) + }.onSuccess { response -> + _paymentState.value = PeerIncomingTerms( + amount = response.amount, + contractTerms = response.contractTerms, + id = response.peerPullPaymentIncomingId, + ) + }.onError { error -> + Log.e(TAG, "got checkPeerPushPayment error result $error") + _paymentState.value = PeerIncomingError(error) + } + } + } -@Serializable -data class InitiatePeerPullPaymentResponse( - /** - * Taler URI for the other party to make the payment that was requested. - */ - val talerUri: String, -) + fun acceptPeerPullPayment(terms: PeerIncomingTerms) { + _paymentState.value = PeerIncomingAccepting(terms) + scope.launch(Dispatchers.IO) { + api.request<Unit>("acceptPeerPullPayment") { + put("peerPullPaymentIncomingId", terms.id) + }.onSuccess { + _paymentState.value = PeerIncomingAccepted + }.onError { error -> + Log.e(TAG, "got checkPeerPushPayment error result $error") + _paymentState.value = PeerIncomingError(error) + } + } + } -@Serializable -data class InitiatePeerPushPaymentResponse( - val exchangeBaseUrl: String, - val talerUri: String, -) +} diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerOutgoingState.kt b/wallet/src/main/java/net/taler/wallet/peer/PeerOutgoingState.kt new file mode 100644 index 0000000..0b6b2a8 --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/peer/PeerOutgoingState.kt @@ -0,0 +1,47 @@ +/* + * This file is part of GNU Taler + * (C) 2022 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +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 index d38ae34..be79e9d 100644 --- a/wallet/src/main/java/net/taler/wallet/peer/PeerPullFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/peer/PeerPullFragment.kt @@ -51,7 +51,7 @@ class PeerPullFragment : Fragment() { MdcTheme { Surface { val state = peerManager.pullState.collectAsStateLifecycleAware() - if (state.value is PeerPaymentIntro) { + if (state.value is PeerOutgoingIntro) { val exchangeState = exchangeFlow.collectAsStateLifecycleAware(initial = null) PeerPullIntroComposable( diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerPullComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/PeerPullIntroComposable.kt index 02f2c7c..02f2c7c 100644 --- a/wallet/src/main/java/net/taler/wallet/peer/PeerPullComposable.kt +++ b/wallet/src/main/java/net/taler/wallet/peer/PeerPullIntroComposable.kt diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerPullPaymentComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/PeerPullPaymentComposable.kt new file mode 100644 index 0000000..fff74ea --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/peer/PeerPullPaymentComposable.kt @@ -0,0 +1,223 @@ +/* + * This file is part of GNU Taler + * (C) 2022 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +package net.taler.wallet.peer + +import android.annotation.SuppressLint +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.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<PeerIncomingState>, + onAccept: (PeerIncomingTerms) -> Unit, +) { + val scrollState = rememberScrollState() + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState), + ) { + Text( + modifier = Modifier + .padding(16.dp) + .align(CenterHorizontally), + text = stringResource(id = R.string.pay_peer_intro)) + when (val s = state.value) { + PeerIncomingChecking -> PeerPullCheckingComposable() + is PeerIncomingTerms -> PeerPullTermsComposable(s, onAccept) + is PeerIncomingAccepting -> PeerPullTermsComposable(s, onAccept) + PeerIncomingAccepted -> { + // we navigate away, don't show anything + } + is PeerIncomingError -> PeerPullErrorComposable(s) + } + } +} + +@Composable +fun ColumnScope.PeerPullCheckingComposable() { + CircularProgressIndicator( + modifier = Modifier + .align(CenterHorizontally) + .fillMaxSize(0.75f), + ) +} + +@Composable +fun ColumnScope.PeerPullTermsComposable( + terms: PeerIncomingTerms, + onAccept: (PeerIncomingTerms) -> Unit, +) { + Text( + modifier = Modifier + .padding(16.dp) + .align(CenterHorizontally), + text = terms.contractTerms.summary, + style = MaterialTheme.typography.h5, + ) + Spacer(modifier = Modifier.weight(1f)) + Card(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier.padding(8.dp) + ) { + Row( + modifier = Modifier.align(End), + ) { + Text( + text = stringResource(id = R.string.payment_label_amount_total), + style = MaterialTheme.typography.body1, + ) + Text( + modifier = Modifier.padding(start = 8.dp), + text = terms.contractTerms.amount.toString(), + style = MaterialTheme.typography.body1, + fontWeight = FontWeight.Bold, + ) + } + val fee = + Amount.zero(terms.amount.currency) // terms.amount - terms.contractTerms.amount + if (!fee.isZero()) { + Text( + modifier = Modifier.align(End), + text = stringResource(id = R.string.payment_fee, fee), + style = MaterialTheme.typography.body1, + ) + } + if (terms is PeerIncomingAccepting) { + CircularProgressIndicator( + modifier = Modifier + .padding(end = 64.dp) + .align(End), + ) + } else { + Button( + modifier = Modifier + .align(End) + .padding(top = 8.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = colorResource(R.color.green), + contentColor = Color.White, + ), + onClick = { onAccept(terms) }, + ) { + Text( + text = stringResource(id = R.string.payment_button_confirm), + ) + } + } + } + } +} + +@Composable +fun ColumnScope.PeerPullErrorComposable(s: PeerIncomingError) { + Text( + modifier = Modifier + .align(CenterHorizontally) + .padding(horizontal = 32.dp), + text = s.info.userFacingMsg, + style = MaterialTheme.typography.h5, + color = colorResource(id = R.color.red), + ) +} + +@Preview +@Composable +fun PeerPullCheckingPreview() { + Surface { + @SuppressLint("UnrememberedMutableState") + val s = mutableStateOf(PeerIncomingChecking) + PeerPullPaymentComposable(s) {} + } +} + +@Preview +@Composable +fun PeerPullTermsPreview() { + Surface { + val terms = PeerIncomingTerms( + amount = Amount.fromDouble("TESTKUDOS", 42.23), + contractTerms = PeerContractTerms( + summary = "This is a long test summary that can be more than one line long for sure", + amount = Amount.fromDouble("TESTKUDOS", 23.42), + ), + id = "ID123", + ) + + @SuppressLint("UnrememberedMutableState") + val s = mutableStateOf(terms) + PeerPullPaymentComposable(s) {} + } +} + +@Preview +@Composable +fun PeerPullAcceptingPreview() { + Surface { + val terms = PeerIncomingTerms( + amount = Amount.fromDouble("TESTKUDOS", 42.23), + contractTerms = PeerContractTerms( + summary = "This is a long test summary that can be more than one line long for sure", + amount = Amount.fromDouble("TESTKUDOS", 23.42), + ), + id = "ID123", + ) + + @SuppressLint("UnrememberedMutableState") + val s = mutableStateOf(PeerIncomingAccepting(terms)) + PeerPullPaymentComposable(s) {} + } +} + +@Preview +@Composable +fun PeerPullPayErrorPreview() { + Surface { + @SuppressLint("UnrememberedMutableState") + val s = mutableStateOf(PeerIncomingError(TalerErrorInfo(42, "hint", "msg"))) + PeerPullPaymentComposable(s) {} + } +} diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerPullResultComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/PeerPullResultComposable.kt index 0b9b546..d37ca4b 100644 --- a/wallet/src/main/java/net/taler/wallet/peer/PeerPullResultComposable.kt +++ b/wallet/src/main/java/net/taler/wallet/peer/PeerPullResultComposable.kt @@ -55,7 +55,7 @@ import net.taler.wallet.compose.getQrCodeSize import org.json.JSONObject @Composable -fun PeerPullResultComposable(state: PeerPaymentState, onClose: () -> Unit) { +fun PeerPullResultComposable(state: PeerOutgoingState, onClose: () -> Unit) { val scrollState = rememberScrollState() Column( modifier = Modifier @@ -68,10 +68,10 @@ fun PeerPullResultComposable(state: PeerPaymentState, onClose: () -> Unit) { text = stringResource(id = R.string.receive_peer_invoice_instruction), ) when (state) { - PeerPaymentIntro -> error("Result composable with PullPaymentIntro") - is PeerPaymentCreating -> PeerPullCreatingComposable() - is PeerPaymentResponse -> PeerPullResponseComposable(state) - is PeerPaymentError -> PeerPullErrorComposable(state) + PeerOutgoingIntro -> error("Result composable with PullPaymentIntro") + is PeerOutgoingCreating -> PeerPullCreatingComposable() + is PeerOutgoingResponse -> PeerPullResponseComposable(state) + is PeerOutgoingError -> PeerPullErrorComposable(state) } Button(modifier = Modifier .padding(16.dp) @@ -94,7 +94,7 @@ private fun ColumnScope.PeerPullCreatingComposable() { } @Composable -private fun ColumnScope.PeerPullResponseComposable(state: PeerPaymentResponse) { +private fun ColumnScope.PeerPullResponseComposable(state: PeerOutgoingResponse) { val qrCodeSize = getQrCodeSize() Image( modifier = Modifier @@ -135,7 +135,7 @@ private fun ColumnScope.PeerPullResponseComposable(state: PeerPaymentResponse) { } @Composable -private fun ColumnScope.PeerPullErrorComposable(state: PeerPaymentError) { +private fun ColumnScope.PeerPullErrorComposable(state: PeerOutgoingError) { Text( modifier = Modifier .align(CenterHorizontally) @@ -150,7 +150,7 @@ private fun ColumnScope.PeerPullErrorComposable(state: PeerPaymentError) { @Composable fun PeerPullCreatingPreview() { Surface { - PeerPullResultComposable(PeerPaymentCreating) {} + PeerPullResultComposable(PeerOutgoingCreating) {} } } @@ -159,7 +159,7 @@ fun PeerPullCreatingPreview() { fun PeerPullResponsePreview() { Surface { val talerUri = "https://example.org/foo/bar/can/be/very/long/url/so/fit/it/on/screen" - val response = PeerPaymentResponse(talerUri, QrCodeManager.makeQrCode(talerUri)) + val response = PeerOutgoingResponse(talerUri, QrCodeManager.makeQrCode(talerUri)) PeerPullResultComposable(response) {} } } @@ -169,7 +169,7 @@ fun PeerPullResponsePreview() { fun PeerPullResponseLandscapePreview() { Surface { val talerUri = "https://example.org/foo/bar/can/be/very/long/url/so/fit/it/on/screen" - val response = PeerPaymentResponse(talerUri, QrCodeManager.makeQrCode(talerUri)) + val response = PeerOutgoingResponse(talerUri, QrCodeManager.makeQrCode(talerUri)) PeerPullResultComposable(response) {} } } @@ -179,7 +179,7 @@ fun PeerPullResponseLandscapePreview() { fun PeerPullErrorPreview() { Surface { val json = JSONObject().apply { put("foo", "bar") } - val response = PeerPaymentError(TalerErrorInfo(42, "hint", "message", json)) + val response = PeerOutgoingError(TalerErrorInfo(42, "hint", "message", json)) PeerPullResultComposable(response) {} } } diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerPushResultComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/PeerPushResultComposable.kt index f3d1a79..b33fc4f 100644 --- a/wallet/src/main/java/net/taler/wallet/peer/PeerPushResultComposable.kt +++ b/wallet/src/main/java/net/taler/wallet/peer/PeerPushResultComposable.kt @@ -55,7 +55,7 @@ import net.taler.wallet.compose.getQrCodeSize import org.json.JSONObject @Composable -fun PeerPushResultComposable(state: PeerPaymentState, onClose: () -> Unit) { +fun PeerPushResultComposable(state: PeerOutgoingState, onClose: () -> Unit) { val scrollState = rememberScrollState() Column( modifier = Modifier @@ -68,10 +68,10 @@ fun PeerPushResultComposable(state: PeerPaymentState, onClose: () -> Unit) { text = stringResource(id = R.string.send_peer_payment_instruction), ) when (state) { - PeerPaymentIntro -> error("Result composable with PullPaymentIntro") - is PeerPaymentCreating -> PeerPushCreatingComposable() - is PeerPaymentResponse -> PeerPushResponseComposable(state) - is PeerPaymentError -> PeerPushErrorComposable(state) + PeerOutgoingIntro -> error("Result composable with PullPaymentIntro") + is PeerOutgoingCreating -> PeerPushCreatingComposable() + is PeerOutgoingResponse -> PeerPushResponseComposable(state) + is PeerOutgoingError -> PeerPushErrorComposable(state) } Button(modifier = Modifier .padding(16.dp) @@ -94,7 +94,7 @@ private fun ColumnScope.PeerPushCreatingComposable() { } @Composable -private fun ColumnScope.PeerPushResponseComposable(state: PeerPaymentResponse) { +private fun ColumnScope.PeerPushResponseComposable(state: PeerOutgoingResponse) { val qrCodeSize = getQrCodeSize() Image( modifier = Modifier @@ -135,7 +135,7 @@ private fun ColumnScope.PeerPushResponseComposable(state: PeerPaymentResponse) { } @Composable -private fun ColumnScope.PeerPushErrorComposable(state: PeerPaymentError) { +private fun ColumnScope.PeerPushErrorComposable(state: PeerOutgoingError) { Text( modifier = Modifier .align(CenterHorizontally) @@ -150,7 +150,7 @@ private fun ColumnScope.PeerPushErrorComposable(state: PeerPaymentError) { @Composable fun PeerPushCreatingPreview() { Surface { - PeerPushResultComposable(PeerPaymentCreating) {} + PeerPushResultComposable(PeerOutgoingCreating) {} } } @@ -159,7 +159,7 @@ fun PeerPushCreatingPreview() { fun PeerPushResponsePreview() { Surface { val talerUri = "https://example.org/foo/bar/can/be/very/long/url/so/fit/it/on/screen" - val response = PeerPaymentResponse(talerUri, QrCodeManager.makeQrCode(talerUri)) + val response = PeerOutgoingResponse(talerUri, QrCodeManager.makeQrCode(talerUri)) PeerPushResultComposable(response) {} } } @@ -169,7 +169,7 @@ fun PeerPushResponsePreview() { fun PeerPushResponseLandscapePreview() { Surface { val talerUri = "https://example.org/foo/bar/can/be/very/long/url/so/fit/it/on/screen" - val response = PeerPaymentResponse(talerUri, QrCodeManager.makeQrCode(talerUri)) + val response = PeerOutgoingResponse(talerUri, QrCodeManager.makeQrCode(talerUri)) PeerPushResultComposable(response) {} } } @@ -179,7 +179,7 @@ fun PeerPushResponseLandscapePreview() { fun PeerPushErrorPreview() { Surface { val json = JSONObject().apply { put("foo", "bar") } - val response = PeerPaymentError(TalerErrorInfo(42, "hint", "message", json)) + val response = PeerOutgoingError(TalerErrorInfo(42, "hint", "message", json)) PeerPushResultComposable(response) {} } } diff --git a/wallet/src/main/java/net/taler/wallet/peer/PullPaymentFragment.kt b/wallet/src/main/java/net/taler/wallet/peer/PullPaymentFragment.kt new file mode 100644 index 0000000..71b1bcc --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/peer/PullPaymentFragment.kt @@ -0,0 +1,68 @@ +/* + * This file is part of GNU Taler + * (C) 2022 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +package net.taler.wallet.peer + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.material.Surface +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import com.google.android.material.composethemeadapter.MdcTheme +import net.taler.wallet.MainViewModel +import net.taler.wallet.R +import net.taler.wallet.compose.collectAsStateLifecycleAware + +class PullPaymentFragment : Fragment() { + private val model: MainViewModel by activityViewModels() + private val peerManager get() = model.peerManager + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + lifecycleScope.launchWhenResumed { + peerManager.paymentState.collect { + if (it is PeerIncomingAccepted) { + findNavController().navigate(R.id.action_promptPullPayment_to_nav_main) + } + } + } + return ComposeView(requireContext()).apply { + setContent { + MdcTheme { + Surface { + val state = peerManager.paymentState.collectAsStateLifecycleAware() + PeerPullPaymentComposable(state) { terms -> + peerManager.acceptPeerPullPayment(terms) + } + } + } + } + } + } + + override fun onStart() { + super.onStart() + activity?.setTitle(R.string.pay_peer_title) + } +} diff --git a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullDebit.kt b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullDebit.kt new file mode 100644 index 0000000..823126b --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullDebit.kt @@ -0,0 +1,77 @@ +/* + * This file is part of GNU Taler + * (C) 2022 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +package net.taler.wallet.peer + +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import net.taler.common.Amount +import net.taler.common.Timestamp +import net.taler.wallet.R +import net.taler.wallet.transactions.AmountType +import net.taler.wallet.transactions.PeerInfoShort +import net.taler.wallet.transactions.TransactionAmountComposable +import net.taler.wallet.transactions.TransactionInfoComposable +import net.taler.wallet.transactions.TransactionPeerComposable +import net.taler.wallet.transactions.TransactionPeerPullDebit + +@Composable +fun TransactionPeerPullDebitComposable(t: TransactionPeerPullDebit) { + TransactionAmountComposable( + label = stringResource(id = R.string.transaction_paid), + amount = t.amountEffective, + amountType = AmountType.Negative, + ) + TransactionAmountComposable( + label = stringResource(id = R.string.transaction_order_total), + amount = t.amountRaw, + amountType = AmountType.Neutral, + ) + val fee = t.amountEffective - t.amountRaw + if (!fee.isZero()) { + TransactionAmountComposable( + label = stringResource(id = R.string.withdraw_fees), + amount = fee, + amountType = AmountType.Negative, + ) + } + TransactionInfoComposable( + label = stringResource(id = R.string.withdraw_manual_ready_subject), + info = t.info.summary ?: "", + ) +} + +@Preview +@Composable +fun TransactionPeerPullDebitPreview() { + val t = TransactionPeerPullDebit( + transactionId = "transactionId", + timestamp = Timestamp(System.currentTimeMillis() - 360 * 60 * 1000), + pending = true, + exchangeBaseUrl = "https://exchange.example.org/", + amountRaw = Amount.fromDouble("TESTKUDOS", 42.1337), + amountEffective = Amount.fromDouble("TESTKUDOS", 42.23), + info = PeerInfoShort( + expiration = Timestamp(System.currentTimeMillis() + 60 * 60 * 1000), + summary = "test invoice", + ), + ) + Surface { + TransactionPeerComposable(t) {} + } +} diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionPeerFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionPeerFragment.kt index f1afb41..9b0c208 100644 --- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionPeerFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionPeerFragment.kt @@ -49,6 +49,7 @@ import net.taler.common.Amount import net.taler.common.toAbsoluteTime import net.taler.wallet.R import net.taler.wallet.peer.TransactionPeerPullCreditComposable +import net.taler.wallet.peer.TransactionPeerPullDebitComposable import net.taler.wallet.peer.TransactionPeerPushDebitComposable class TransactionPeerFragment : TransactionDetailFragment() { @@ -89,7 +90,7 @@ fun TransactionPeerComposable(t: Transaction, onDelete: () -> Unit) { when (t) { is TransactionPeerPullCredit -> TransactionPeerPullCreditComposable(t) is TransactionPeerPushCredit -> TODO() - is TransactionPeerPullDebit -> TODO() + is TransactionPeerPullDebit -> TransactionPeerPullDebitComposable(t) is TransactionPeerPushDebit -> TransactionPeerPushDebitComposable(t) else -> error("unexpected transaction: ${t::class.simpleName}") } diff --git a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt index 6f72567..6ef6c88 100644 --- a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt +++ b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt @@ -280,9 +280,9 @@ class TransactionPeerPullDebit( @Transient override val amountType = AmountType.Negative override fun getTitle(context: Context): String { - return context.getString(R.string.transaction_peer_push_debit) + return context.getString(R.string.transaction_peer_pull_debit) } - override val generalTitleRes = R.string.payment_title + override val generalTitleRes = R.string.transaction_peer_pull_debit } /** diff --git a/wallet/src/main/res/navigation/nav_graph.xml b/wallet/src/main/res/navigation/nav_graph.xml index e3d526e..3170216 100644 --- a/wallet/src/main/res/navigation/nav_graph.xml +++ b/wallet/src/main/res/navigation/nav_graph.xml @@ -141,6 +141,16 @@ </fragment> <fragment + android:id="@+id/promptPullPayment" + android:name="net.taler.wallet.peer.PullPaymentFragment" + android:label="@string/pay_peer_title"> + <action + android:id="@+id/action_promptPullPayment_to_nav_main" + app:destination="@id/nav_main" + app:popUpTo="@id/nav_main" /> + </fragment> + + <fragment android:id="@+id/nav_transactions" android:name="net.taler.wallet.transactions.TransactionsFragment" android:label="@string/transactions_title" @@ -276,6 +286,10 @@ app:destination="@id/promptTip" /> <action + android:id="@+id/action_global_prompt_pull_payment" + app:destination="@id/promptPullPayment" /> + + <action android:id="@+id/action_global_pending_operations" app:destination="@id/nav_pending_operations" /> diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml index 96a3453..8601b14 100644 --- a/wallet/src/main/res/values/strings.xml +++ b/wallet/src/main/res/values/strings.xml @@ -98,6 +98,7 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card <string name="transaction_refresh">Coin expiry change fee</string> <string name="transaction_peer_push_debit">Push payment</string> <string name="transaction_peer_pull_credit">Invoice</string> + <string name="transaction_peer_pull_debit">Invoice paid</string> <string name="payment_title">Payment</string> <string name="payment_fee">+%s payment fee</string> @@ -127,6 +128,9 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card <string name="send_peer_warning">Warning: Funds will leave the wallet immediately.</string> <string name="send_peer_payment_instruction">Let the payee scan this QR code to receive:</string> + <string name="pay_peer_title">Pay invoice</string> + <string name="pay_peer_intro">Do you want to pay this invoice?</string> + <string name="withdraw_initiated">Withdrawal initiated</string> <string name="withdraw_title">Withdrawal</string> <string name="withdraw_total">Withdraw</string> |