commit 71b42e0fbb779853bd7cf68caba6c8fb1c9e36bd
parent c3fd3a371d9cc83ad9a5fc4cf8e87ee8ead42d9e
Author: Iván Ávalos <avalos@disroot.org>
Date: Fri, 4 Apr 2025 16:21:11 +0200
[wallet] add separate view for pending transactions
Diffstat:
8 files changed, 233 insertions(+), 77 deletions(-)
diff --git a/wallet/src/main/java/net/taler/wallet/MainFragment.kt b/wallet/src/main/java/net/taler/wallet/MainFragment.kt
@@ -89,6 +89,7 @@ import net.taler.wallet.compose.GridMenuItem
import net.taler.wallet.compose.TalerSurface
import net.taler.wallet.compose.collectAsStateLifecycleAware
import net.taler.wallet.settings.SettingsFragment
+import net.taler.wallet.transactions.TransactionStateFilter.Nonfinal
import kotlin.math.roundToInt
class MainFragment: Fragment() {
@@ -114,7 +115,8 @@ class MainFragment: Fragment() {
val context = LocalContext.current
val balanceState by model.balanceManager.state.observeAsState(BalanceState.None)
val selectedScope by model.transactionManager.selectedScope.collectAsStateLifecycleAware()
- val txResult by remember(selectedScope) { model.transactionManager.transactionsFlow(selectedScope) }.collectAsStateLifecycleAware()
+ val txStateFilter by model.transactionManager.stateFilter.collectAsStateLifecycleAware()
+ val txResult by remember(selectedScope, txStateFilter) { model.transactionManager.transactionsFlow(selectedScope, stateFilter = txStateFilter) }.collectAsStateLifecycleAware()
val selectedSpec = remember(selectedScope) { selectedScope?.let { model.balanceManager.getSpecForScopeInfo(it) } }
val actionButtonUsed by remember { model.getActionButtonUsed(context) }.collectAsStateLifecycleAware(true)
@@ -173,6 +175,7 @@ class MainFragment: Fragment() {
innerPadding = innerPadding,
state = balanceState,
txResult = txResult,
+ txStateFilter = txStateFilter,
selectedScope = selectedScope,
selectedCurrencySpec = selectedSpec,
onGetDemoMoneyClicked = {
@@ -182,6 +185,9 @@ class MainFragment: Fragment() {
onBalanceClicked = {
model.showTransactions(it.scopeInfo)
},
+ onPendingClicked = {
+ model.showTransactions(it.scopeInfo, Nonfinal)
+ },
onTransactionClicked = { tx ->
if (tx.detailPageNav != 0) {
model.transactionManager.selectTransaction(tx)
diff --git a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
@@ -57,6 +57,7 @@ import net.taler.wallet.refund.RefundManager
import net.taler.wallet.settings.SettingsManager
import net.taler.wallet.settings.userPreferencesDataStore
import net.taler.wallet.transactions.TransactionManager
+import net.taler.wallet.transactions.TransactionStateFilter
import net.taler.wallet.withdraw.WithdrawManager
import org.json.JSONObject
@@ -184,9 +185,9 @@ class MainViewModel(
* Navigates to the given scope info's transaction list, when [MainFragment] is shown.
*/
@UiThread
- fun showTransactions(scopeInfo: ScopeInfo) {
+ fun showTransactions(scopeInfo: ScopeInfo, stateFilter: TransactionStateFilter? = null) {
Log.d(TAG, "selectedScope should change to $scopeInfo")
- transactionManager.selectScope(scopeInfo)
+ transactionManager.selectScope(scopeInfo, stateFilter)
}
@UiThread
diff --git a/wallet/src/main/java/net/taler/wallet/balances/BalancesComposable.kt b/wallet/src/main/java/net/taler/wallet/balances/BalancesComposable.kt
@@ -29,8 +29,13 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material3.Button
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
+import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.ProvideTextStyle
@@ -54,6 +59,7 @@ import net.taler.wallet.cleanExchange
import net.taler.wallet.compose.LoadingScreen
import net.taler.wallet.compose.TalerSurface
import net.taler.wallet.transactions.Transaction
+import net.taler.wallet.transactions.TransactionStateFilter
import net.taler.wallet.transactions.TransactionsComposable
import net.taler.wallet.transactions.TransactionsResult
import net.taler.wallet.withdraw.WithdrawalError
@@ -63,10 +69,12 @@ fun BalancesComposable(
innerPadding: PaddingValues,
state: BalanceState,
txResult: TransactionsResult,
+ txStateFilter: TransactionStateFilter?,
selectedScope: ScopeInfo?,
selectedCurrencySpec: CurrencySpecification?,
onGetDemoMoneyClicked: () -> Unit,
onBalanceClicked: (balance: BalanceItem) -> Unit,
+ onPendingClicked: (balance: BalanceItem) -> Unit,
onTransactionClicked: (tx: Transaction) -> Unit,
onTransactionsDelete: (txIds: List<String>) -> Unit,
onShowBalancesClicked: () -> Unit,
@@ -84,9 +92,10 @@ fun BalancesComposable(
contentPadding = innerPadding,
) {
items(state.balances, key = { it.scopeInfo.hashCode() }) { balance ->
- BalanceRow(balance) {
- onBalanceClicked(balance)
- }
+ BalanceRow(balance,
+ onClick = { onBalanceClicked(balance) },
+ onPendingClick = { onPendingClicked(balance) },
+ )
}
}
} else {
@@ -100,6 +109,7 @@ fun BalancesComposable(
balance = it,
currencySpec = selectedCurrencySpec,
txResult = txResult,
+ txStateFilter = txStateFilter,
onTransactionClick = onTransactionClicked,
onTransactionsDelete = onTransactionsDelete,
onShowBalancesClicked = onShowBalancesClicked,
@@ -121,75 +131,105 @@ fun BalancesComposable(
fun BalanceRow(
balance: BalanceItem,
onClick: () -> Unit,
+ onPendingClick: () -> Unit,
) {
OutlinedCard(
modifier = Modifier
.padding(
horizontal = 9.dp,
vertical = 6.dp,
- ).clickable { onClick() },
+ )
) {
- ListItem(
- modifier = Modifier
- .animateContentSize()
- .padding(6.dp),
- headlineContent = {
- Text(
- balance.available.toString(),
- style = MaterialTheme.typography.displaySmall,
- )
- },
- overlineContent = {
- ProvideTextStyle(MaterialTheme.typography.bodySmall) {
- when (balance.scopeInfo) {
- is Exchange -> Text(
- stringResource(
- R.string.balance_scope_exchange,
- cleanExchange(balance.scopeInfo.url)
- ),
- )
-
- is Auditor -> Text(
- stringResource(
- R.string.balance_scope_auditor,
- cleanExchange(balance.scopeInfo.url)
- ),
- )
-
- else -> {}
- }
- }
- },
- supportingContent = {
- Column {
- ProvideTextStyle(MaterialTheme.typography.bodyLarge) {
- AnimatedVisibility(!balance.pendingIncoming.isZero()) {
- Text(
+ Column {
+ ListItem(
+ modifier = Modifier
+ .animateContentSize()
+ .clickable { onClick() }
+ .padding(vertical = 6.dp),
+ headlineContent = {
+ Text(
+ balance.available.toString(),
+ style = MaterialTheme.typography.displaySmall,
+ )
+ },
+ overlineContent = {
+ ProvideTextStyle(MaterialTheme.typography.bodySmall) {
+ when (balance.scopeInfo) {
+ is Exchange -> Text(
stringResource(
- R.string.balances_inbound_amount,
- balance.pendingIncoming.toString(showSymbol = false),
+ R.string.balance_scope_exchange,
+ cleanExchange(balance.scopeInfo.url)
),
- color = colorResource(R.color.green),
)
- }
- AnimatedVisibility(!balance.pendingOutgoing.isZero()) {
- Text(
+ is Auditor -> Text(
stringResource(
- R.string.balances_outbound_amount,
- balance.pendingOutgoing.toString(showSymbol = false)
+ R.string.balance_scope_auditor,
+ cleanExchange(balance.scopeInfo.url)
),
- color = MaterialTheme.colorScheme.error,
)
+
+ else -> {}
}
}
- }
+ },
+ )
+
+ if (!balance.pendingIncoming.isZero() || !balance.pendingOutgoing.isZero()) {
+ HorizontalDivider()
+ PendingComposable(balance, onPendingClick)
}
- )
+ }
}
}
@Composable
+fun PendingComposable(
+ balance: BalanceItem,
+ onClick: () -> Unit,
+) {
+ ListItem(
+ modifier = Modifier
+ .animateContentSize()
+ .clickable { onClick() },
+ colors = ListItemDefaults.colors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
+ ),
+ headlineContent = {
+ Column(modifier = Modifier.padding(vertical = 5.dp)) {
+ ProvideTextStyle(MaterialTheme.typography.bodyLarge) {
+ AnimatedVisibility(!balance.pendingIncoming.isZero()) {
+ Text(
+ stringResource(
+ R.string.balances_inbound_amount,
+ balance.pendingIncoming.toString(showSymbol = false),
+ ),
+ color = colorResource(R.color.green),
+ )
+ }
+
+ AnimatedVisibility(!balance.pendingOutgoing.isZero()) {
+ Text(
+ stringResource(
+ R.string.balances_outbound_amount,
+ balance.pendingOutgoing.toString(showSymbol = false)
+ ),
+ color = MaterialTheme.colorScheme.error,
+ )
+ }
+ }
+ }
+ },
+ trailingContent = {
+ Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ contentDescription = null,
+ )
+ }
+ )
+}
+
+@Composable
fun EmptyBalancesComposable(
innerPadding: PaddingValues,
onGetDemoMoneyClicked: () -> Unit,
@@ -244,6 +284,7 @@ fun BalancesComposablePreview() {
innerPadding = PaddingValues(0.dp),
state = BalanceState.Success(balances),
txResult = TransactionsResult.Success(listOf()),
+ txStateFilter = null,
selectedScope = null,
selectedCurrencySpec = null,
onGetDemoMoneyClicked = {},
@@ -251,6 +292,7 @@ fun BalancesComposablePreview() {
onTransactionClicked = {},
onTransactionsDelete = {},
onShowBalancesClicked = {},
+ onPendingClicked = {},
)
}
}
@@ -263,6 +305,7 @@ fun BalancesComposableEmptyPreview() {
innerPadding = PaddingValues(0.dp),
state = BalanceState.Success(listOf()),
txResult = TransactionsResult.Success(listOf()),
+ txStateFilter = null,
selectedScope = null,
selectedCurrencySpec = null,
onGetDemoMoneyClicked = {},
@@ -270,6 +313,7 @@ fun BalancesComposableEmptyPreview() {
onTransactionClicked = {},
onTransactionsDelete = {},
onShowBalancesClicked = {},
+ onPendingClicked = {},
)
}
}
\ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/compose/Banner.kt b/wallet/src/main/java/net/taler/wallet/compose/Banner.kt
@@ -0,0 +1,56 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2025 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.compose
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardColors
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ProvideTextStyle
+import androidx.compose.material3.ShapeDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun Banner(
+ modifier: Modifier = Modifier,
+ colors: CardColors = CardDefaults.cardColors(),
+ content: @Composable () -> Unit,
+) {
+ Card(
+ modifier = modifier
+ .padding(horizontal = 9.dp)
+ .fillMaxWidth(),
+ colors = colors,
+ shape = ShapeDefaults.ExtraSmall,
+ ) {
+ Box(Modifier
+ .padding(10.dp)
+ .fillMaxWidth()) {
+ ProvideTextStyle(MaterialTheme.typography.labelLarge.copy(
+ textAlign = TextAlign.Center,
+ )) {
+ content()
+ }
+ }
+ }
+}
+\ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt
@@ -23,7 +23,11 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.encodeToJsonElement
+import kotlinx.serialization.json.jsonPrimitive
import net.taler.wallet.TAG
import net.taler.wallet.backend.BackendManager
import net.taler.wallet.backend.TalerErrorInfo
@@ -39,6 +43,18 @@ sealed class TransactionsResult {
data class Success(val transactions: List<Transaction>) : TransactionsResult()
}
+@Serializable
+enum class TransactionStateFilter {
+ @SerialName("final")
+ Final,
+
+ @SerialName("nonfinal")
+ Nonfinal,
+
+ @SerialName("done")
+ Done,
+}
+
class TransactionManager(
private val api: WalletBackendApi,
private val scope: CoroutineScope,
@@ -48,20 +64,23 @@ class TransactionManager(
private val mSelectedTransaction = MutableStateFlow<Transaction?>(null)
private val mSelectedScope = MutableStateFlow<ScopeInfo?>(null)
private val mSearchQuery = MutableStateFlow<String?>(null)
+ private val mStateFilter = MutableStateFlow<TransactionStateFilter?>(null)
val selectedTransaction = mSelectedTransaction.asStateFlow()
val selectedScope = mSelectedScope.asStateFlow()
val searchQuery = mSearchQuery.asStateFlow()
+ val stateFilter = mStateFilter.asStateFlow()
// This function must be called ONLY when scopeInfo / searchQuery change!
// Use remember() {} in Compose to prevent multiple calls during recomposition
fun transactionsFlow(
scopeInfo: ScopeInfo? = null,
searchQuery: String? = null,
+ stateFilter: TransactionStateFilter? = null,
): StateFlow<TransactionsResult> {
loadTransactions()
return if (scopeInfo != null) {
- loadTransactions(scopeInfo, searchQuery)
+ loadTransactions(scopeInfo, searchQuery, stateFilter)
mTransactions[scopeInfo]?.asStateFlow()
?: MutableStateFlow(TransactionsResult.None)
} else {
@@ -73,8 +92,9 @@ class TransactionManager(
fun loadTransactions(
scopeInfo: ScopeInfo? = null,
searchQuery: String? = null,
+ stateFilter: TransactionStateFilter? = null,
) {
- Log.d(TAG, "loadTransactions($scopeInfo, $searchQuery)")
+ Log.d(TAG, "loadTransactions($scopeInfo, $searchQuery, $stateFilter)")
val s = scopeInfo ?: mSelectedScope.value ?: run {
MutableStateFlow(TransactionsResult.None)
return
@@ -92,7 +112,7 @@ class TransactionManager(
}
// ...then fetch new ones
- val res = getTransactions(s, searchQuery)
+ val res = getTransactions(s, searchQuery, filterByState = stateFilter)
if (res is TransactionsResult.Success) {
allTransactions[s] = res.transactions
}
@@ -102,10 +122,24 @@ class TransactionManager(
}
}
- private suspend fun getTransactions(scope: ScopeInfo, searchQuery: String?): TransactionsResult {
+ private suspend fun getTransactions(
+ scope: ScopeInfo,
+ searchQuery: String?,
+ filterByState: TransactionStateFilter? = null,
+ offsetTransactionId: String? = null,
+ limit: Int? = null,
+ ): TransactionsResult {
var result: TransactionsResult = TransactionsResult.None
- api.request("getTransactions", Transactions.serializer()) {
+ api.request("getTransactionsV2", Transactions.serializer()) {
if (searchQuery != null) put("search", searchQuery)
+ if (filterByState != null) put(
+ "filterByState",
+ BackendManager.json
+ .encodeToJsonElement(filterByState)
+ .jsonPrimitive.content,
+ )
+ if (offsetTransactionId != null) put("offsetTransactionId", offsetTransactionId)
+ if (limit != null) put("limit", limit)
put("scopeInfo", JSONObject(BackendManager.json.encodeToString(scope)))
}.onError { error ->
Log.e(TAG, "Error: getTransactions error result: $error")
@@ -164,8 +198,12 @@ class TransactionManager(
mSelectedTransaction.value = tx
}
- fun selectScope(scopeInfo: ScopeInfo?) = scope.launch {
+ fun selectScope(
+ scopeInfo: ScopeInfo?,
+ stateFilter: TransactionStateFilter? = null,
+ ) = scope.launch {
mSelectedScope.value = scopeInfo
+ mStateFilter.value = stateFilter
}
fun setSearchQuery(searchQuery: String?) = scope.launch {
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionStateComposable.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionStateComposable.kt
@@ -20,10 +20,8 @@ import android.content.res.Configuration
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.ShapeDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -31,7 +29,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
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 net.taler.common.Amount
@@ -40,6 +37,7 @@ import net.taler.common.Timestamp
import net.taler.common.toAbsoluteTime
import net.taler.wallet.R
import net.taler.wallet.balances.ScopeInfo
+import net.taler.wallet.compose.Banner
import net.taler.wallet.compose.TalerSurface
import net.taler.wallet.transactions.TransactionMajorState.Aborted
import net.taler.wallet.transactions.TransactionMajorState.Aborting
@@ -107,23 +105,12 @@ fun TransactionStateComposable(
else -> return
}
- Card(
- modifier = modifier
- .padding(horizontal = 9.dp)
- .fillMaxWidth(),
- colors = CardDefaults.cardColors(
- containerColor = cardColor,
- ),
- shape = ShapeDefaults.ExtraSmall,
- ) {
+ Banner(colors = CardDefaults.cardColors(containerColor = cardColor)) {
Text(
modifier = Modifier
- .padding(10.dp)
.fillMaxWidth(),
text = message,
- style = MaterialTheme.typography.labelLarge,
color = textColor,
- textAlign = TextAlign.Center,
)
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionsComposable.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionsComposable.kt
@@ -78,6 +78,7 @@ import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.balances.BalanceItem
import net.taler.wallet.balances.ScopeInfo.Exchange
import net.taler.wallet.cleanExchange
+import net.taler.wallet.compose.Banner
import net.taler.wallet.compose.LoadingScreen
import net.taler.wallet.compose.SelectionModeTopAppBar
import net.taler.wallet.compose.TalerSurface
@@ -100,6 +101,7 @@ import net.taler.wallet.transactions.TransactionMinorState.Repurchase
import net.taler.wallet.transactions.TransactionsResult.Error
import net.taler.wallet.transactions.TransactionsResult.None
import net.taler.wallet.transactions.TransactionsResult.Success
+import net.taler.wallet.transactions.TransactionStateFilter.*
@Composable
fun TransactionsComposable(
@@ -107,6 +109,7 @@ fun TransactionsComposable(
balance: BalanceItem,
currencySpec: CurrencySpecification?,
txResult: TransactionsResult,
+ txStateFilter: TransactionStateFilter?,
onTransactionClick: (tx: Transaction) -> Unit,
onTransactionsDelete: (txIds: List<String>) -> Unit,
onShowBalancesClicked: () -> Unit,
@@ -179,6 +182,21 @@ fun TransactionsComposable(
)
}
+ when (txStateFilter) {
+ Nonfinal -> item {
+ Banner(Modifier.padding(bottom = 6.dp)) {
+ Text(
+ modifier = Modifier
+ .fillMaxWidth(),
+ text = stringResource(R.string.transactions_filter_nonfinal),
+ style = MaterialTheme.typography.labelLarge,
+ textAlign = TextAlign.Center,
+ )
+ }
+ }
+ else -> {}
+ }
+
val placeholderPadding = PaddingValues(vertical = 50.dp)
when (txResult) {
is Success -> if (txResult.transactions.isEmpty()) item {
@@ -511,6 +529,7 @@ fun TransactionsComposableDonePreview() {
balance = previewBalance,
currencySpec = null,
txResult = Success(transactions),
+ txStateFilter = null,
onTransactionClick = {},
onTransactionsDelete = {},
onShowBalancesClicked = {},
@@ -545,6 +564,7 @@ fun TransactionsComposablePendingPreview() {
balance = previewBalance,
currencySpec = null,
txResult = Success(transactions),
+ txStateFilter = Nonfinal,
onTransactionClick = {},
onTransactionsDelete = {},
onShowBalancesClicked = {},
@@ -561,6 +581,7 @@ fun TransactionsComposableEmptyPreview() {
balance = previewBalance,
currencySpec = null,
txResult = Success(listOf()),
+ txStateFilter = null,
onTransactionClick = {},
onTransactionsDelete = {},
onShowBalancesClicked = {},
@@ -577,6 +598,7 @@ fun TransactionsComposableLoadingPreview() {
balance = previewBalance,
currencySpec = null,
txResult = None,
+ txStateFilter = null,
onTransactionClick = {},
onTransactionsDelete = {},
onShowBalancesClicked = {},
diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml
@@ -115,8 +115,8 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="balances_empty_state">There is no digital cash in your wallet.\n\nYou can get demo money from the demo bank:\n\nhttps://bank.demo.taler.net</string>
<string name="balances_empty_demo_url">https://bank.demo.taler.net</string>
<string name="balances_empty_get_money">Get demo money</string>
- <string name="balances_inbound_amount">+%1$s inbound</string>
- <string name="balances_outbound_amount">-%1$s outbound</string>
+ <string name="balances_inbound_amount">+%1$s incoming</string>
+ <string name="balances_outbound_amount">-%1$s outgoing</string>
<string name="balances_title">Balances</string>
<!-- Transactions -->
@@ -171,6 +171,7 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="transactions_fail">Abandon</string>
<string name="transactions_fail_dialog_message">Are you sure you abandon this transaction? Funds still in transit WILL GET LOST.</string>
<string name="transactions_fail_dialog_title">Abandon transaction</string>
+ <string name="transactions_filter_nonfinal">You are viewing pending transactions</string>
<string name="transactions_receive_funds">Receive</string>
<string name="transactions_resume">Resume</string>
<string name="transactions_retry">Retry</string>