commit 23ee29e540635311797f9e7fc836ea060d826227
parent aa45abb2425dea36e7662a48fab87bf999b0117d
Author: Iván Ávalos <avalos@disroot.org>
Date: Fri, 17 Oct 2025 16:17:04 +0200
[wallet] DONAU UI refinements
Diffstat:
8 files changed, 158 insertions(+), 48 deletions(-)
diff --git a/wallet/src/main/java/net/taler/wallet/MainFragment.kt b/wallet/src/main/java/net/taler/wallet/MainFragment.kt
@@ -213,7 +213,7 @@ class MainFragment: Fragment() {
onStatementClicked = {
findNavController().navigate(
R.id.nav_donau_statement,
- bundleOf("donationStatementSig" to it),
+ bundleOf("host" to it),
)
}
)
diff --git a/wallet/src/main/java/net/taler/wallet/balances/BalanceManager.kt b/wallet/src/main/java/net/taler/wallet/balances/BalanceManager.kt
@@ -18,6 +18,7 @@ package net.taler.wallet.balances
import android.util.Log
import androidx.annotation.UiThread
+import androidx.compose.ui.util.fastDistinctBy
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.distinctUntilChanged
@@ -114,16 +115,25 @@ class BalanceManager(
}
private suspend fun loadDonauStatements(): List<DonauStatement>? {
- var res: List<DonauStatement>? = null
+ var list: List<DonauStatement>? = null
api.request("getDonauStatements", GetDonauStatementsResponse.serializer())
.onError {
Log.e(TAG, "Error retrieving donau statements: $it")
// TODO: throw error when getDonationStatements stop relying on network
// mState.postValue(BalanceState.Error(it))
- }.onSuccess {
- res = it.statements
+ }.onSuccess { res ->
+ // only return last year for each authority
+ list = res.statements.map { statement ->
+ val spec = runBlocking { exchangeManager
+ .getSpecForCurrency(statement.total.currency) }
+ statement.copy(total = statement.total.withSpec(spec))
+ }.sortedByDescending {
+ it.year
+ }.fastDistinctBy {
+ it.host
+ }
}
- return res
+ return list
}
@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
@@ -34,6 +34,7 @@ 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.Badge
import androidx.compose.material3.Button
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
@@ -71,13 +72,15 @@ fun BalancesComposable(
onGetDemoMoneyClicked: () -> Unit,
onBalanceClicked: (balance: BalanceItem) -> Unit,
onPendingClicked: (balance: BalanceItem) -> Unit,
- onStatementClicked: (sig: String) -> Unit,
+ onStatementClicked: (host: String) -> Unit,
) {
when (state) {
is BalanceState.None -> {}
is BalanceState.Loading -> LoadingScreen()
is BalanceState.Error -> WithdrawalError(state.error)
- is BalanceState.Success -> if (state.balances.isNotEmpty()) {
+ is BalanceState.Success -> if (
+ state.balances.isNotEmpty()
+ || state.statements.isNotEmpty()) {
LazyColumn(
Modifier
.consumeWindowInsets(innerPadding)
@@ -103,7 +106,9 @@ fun BalancesComposable(
items(state.statements, key = { it.year }) { statement ->
StatementRow(
statement,
- onClick = { onStatementClicked(statement.donationStatementSig) },
+ onClick = { statement.host?.let {
+ onStatementClicked(it)
+ } },
)
}
}
@@ -196,16 +201,27 @@ fun StatementRow(
.padding(vertical = 6.dp),
headlineContent = {
Text(
- "${statement.year}",
+ statement.total.toString(),
style = MaterialTheme.typography.displaySmall,
)
},
overlineContent = {
- val host = statement.host
- if (host != null) ProvideTextStyle(MaterialTheme.typography.bodySmall) {
- Text(stringResource(R.string.balance_scope_exchange, host))
+ ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
+ Text(stringResource(R.string.balance_scope_exchange, statement.legalDomain))
}
- }
+ },
+ trailingContent = {
+ Badge(
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
+ ) {
+ Text(
+ "${statement.year}",
+ modifier = Modifier.padding(6.dp),
+ style = MaterialTheme.typography.titleMedium,
+ )
+ }
+ },
)
}
}
@@ -307,10 +323,21 @@ fun BalancesComposablePreview() {
),
)
+ val statements = listOf(
+ DonauStatement(
+ total = Amount.fromJSONString("KUDOS:0.1"),
+ year = 2025,
+ legalDomain = "Gnuland",
+ uri = "donau://donau.test.taler.net/...",
+ donationStatementSig = "1234567890",
+ donauPub = "1234567890"
+ )
+ )
+
TalerSurface {
BalancesComposable(
innerPadding = PaddingValues(0.dp),
- state = BalanceState.Success(balances, listOf()),
+ state = BalanceState.Success(balances, statements),
onGetDemoMoneyClicked = {},
onBalanceClicked = {},
onPendingClicked = {},
diff --git a/wallet/src/main/java/net/taler/wallet/donau/DonauStatementComposable.kt b/wallet/src/main/java/net/taler/wallet/donau/DonauStatementComposable.kt
@@ -22,24 +22,39 @@ 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.ScrollableTabRow
+import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
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
import net.taler.wallet.BottomInsetsSpacer
import net.taler.wallet.R
import net.taler.wallet.compose.QrCodeUriComposable
+import net.taler.wallet.compose.TalerSurface
import net.taler.wallet.transactions.AmountType
import net.taler.wallet.transactions.TransactionAmountComposable
import net.taler.wallet.transactions.TransactionInfoComposable
@Composable
fun DonauStatementComposable(
- statement: DonauStatement,
+ statements: List<DonauStatement>,
+ selectedIndex: Int,
+ onSelectIndex: (index: Int) -> Unit,
) {
+ val statement = remember(selectedIndex) {
+ statements[selectedIndex]
+ }
+
// TODO: create common instruction + QR composable
Column(
modifier = Modifier
@@ -47,8 +62,23 @@ fun DonauStatementComposable(
.verticalScroll(rememberScrollState()),
horizontalAlignment = CenterHorizontally,
) {
+ ScrollableTabRow(
+ selectedTabIndex = selectedIndex,
+ edgePadding = 8.dp,
+ ) {
+ statements.forEachIndexed { i, statement ->
+ Tab(
+ selected = selectedIndex == i,
+ onClick = { onSelectIndex(i) },
+ text = {
+ Text(statement.year.toString())
+ }
+ )
+ }
+ }
+
Text(
- modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp),
+ modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.titleLarge,
text = stringResource(R.string.donau_statement_instruction),
textAlign = TextAlign.Center,
@@ -61,16 +91,6 @@ fun DonauStatementComposable(
shareAsQrCode = true,
)
- val host = statement.host
- if (host != null) TransactionInfoComposable(
- label = stringResource(R.string.donau_statement_tax_authority),
- info = host,
- )
-
- TransactionInfoComposable(
- label = stringResource(R.string.donau_statement_legal_domain),
- info = statement.legalDomain,
- )
TransactionAmountComposable(
label = stringResource(id = R.string.amount_total),
@@ -79,10 +99,46 @@ fun DonauStatementComposable(
)
TransactionInfoComposable(
- label = stringResource(R.string.donau_statement_year),
- info = statement.year.toString(),
+ label = stringResource(R.string.donau_statement_legal_domain),
+ info = statement.legalDomain,
+ )
+
+ val host = statement.host
+ if (host != null) TransactionInfoComposable(
+ label = stringResource(R.string.donau_statement_tax_authority),
+ info = host,
)
BottomInsetsSpacer()
}
+}
+
+@Preview
+@Composable
+fun DonauStatementComposablePreview() {
+ TalerSurface {
+ var selectedIndex by remember { mutableIntStateOf(0) }
+ DonauStatementComposable(
+ statements = listOf(
+ DonauStatement(
+ total = Amount.fromJSONString("KUDOS:0.1"),
+ year = 2025,
+ legalDomain = "Gnuland",
+ uri = "donau://donau.test.taler.net/",
+ donationStatementSig = "123456",
+ donauPub = "123456",
+ ),
+ DonauStatement(
+ total = Amount.fromJSONString("KUDOS:100.0"),
+ year = 2024,
+ legalDomain = "Gnuland",
+ uri = "donau://donau.test.taler.net/",
+ donationStatementSig = "123456",
+ donauPub = "123456",
+ ),
+ ),
+ selectedIndex = selectedIndex,
+ onSelectIndex = { selectedIndex = it },
+ )
+ }
}
\ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/donau/DonauStatementFragment.kt b/wallet/src/main/java/net/taler/wallet/donau/DonauStatementFragment.kt
@@ -21,7 +21,11 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
@@ -31,37 +35,52 @@ import net.taler.wallet.MainViewModel
import net.taler.wallet.R
import net.taler.wallet.balances.BalanceState
import net.taler.wallet.compose.LoadingScreen
+import net.taler.wallet.compose.TalerSurface
import net.taler.wallet.compose.collectAsStateLifecycleAware
import net.taler.wallet.showError
class DonauStatementFragment: Fragment() {
private val model: MainViewModel by activityViewModels()
- private lateinit var donationStatementSig: String
- private val mStatement = MutableStateFlow<DonauStatement?>(null)
+ private lateinit var host: String
+ private val mStatements = MutableStateFlow<List<DonauStatement>>(emptyList())
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = ComposeView(requireContext()).apply {
- donationStatementSig = arguments?.getString("donationStatementSig")
- ?: error("no donationStatementSig provided")
+ host = arguments?.getString("host")
+ ?: error("no host provided")
+
+ val supportActionBar = (requireActivity() as? AppCompatActivity)
+ ?.supportActionBar
setContent {
- val statement by mStatement.collectAsStateLifecycleAware()
- statement?.let {
- DonauStatementComposable(it)
- } ?: run {
- LoadingScreen()
+ TalerSurface {
+ val statements by mStatements.collectAsStateLifecycleAware()
+ if (statements.isEmpty()) {
+ LoadingScreen()
+ } else {
+ var selectedIndex by rememberSaveable { mutableIntStateOf(0) }
+ DonauStatementComposable(statements, selectedIndex) { index ->
+ selectedIndex = index
+ }
+
+ LaunchedEffect(selectedIndex) {
+ supportActionBar?.title =
+ getString(
+ R.string.donau_statement_title_year,
+ statements[selectedIndex].year,
+ )
+ }
+ }
}
}
}
override fun onStart() {
super.onStart()
- model.balanceManager.loadAssets(true)
- val supportActionBar = (requireActivity() as? AppCompatActivity)?.supportActionBar
model.balanceManager.state.observe(viewLifecycleOwner) { state ->
when (state) {
is BalanceState.Error -> {
@@ -73,12 +92,10 @@ class DonauStatementFragment: Fragment() {
}
is BalanceState.Success -> {
- state.statements.find {
- it.donationStatementSig == donationStatementSig
- }?.let {
- mStatement.value = it
- supportActionBar?.title =
- getString(R.string.donau_statement_title_year, it.year)
+ state.statements.filter {
+ it.host == host
+ }.let {
+ mStatements.value = it
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/main/MainComposable.kt b/wallet/src/main/java/net/taler/wallet/main/MainComposable.kt
@@ -35,7 +35,7 @@ fun MainComposable(
onGetDemoMoneyClicked: () -> Unit,
onBalanceClicked: (balance: BalanceItem) -> Unit,
onPendingClicked: (balance: BalanceItem) -> Unit,
- onStatementClicked: (sig: String) -> Unit,
+ onStatementClicked: (host: String) -> Unit,
onTransactionClicked: (tx: Transaction) -> Unit,
onTransactionsDelete: (txIds: List<String>) -> Unit,
onShowBalancesClicked: () -> Unit,
diff --git a/wallet/src/main/res/navigation/nav_graph.xml b/wallet/src/main/res/navigation/nav_graph.xml
@@ -110,7 +110,7 @@
android:name="net.taler.wallet.donau.DonauStatementFragment"
android:label="@string/donau_statement_title">
<argument
- android:name="donationStatementSig"
+ android:name="host"
app:argType="string"
app:nullable="false" />
</fragment>
diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml
@@ -126,7 +126,7 @@ GNU Taler is immune to many types of fraud such as credit card data theft, phish
<!-- Assets -->
<string name="assets_section_balances">Balances</string>
- <string name="assets_section_statements">Donations</string>
+ <string name="assets_section_statements">Donation statements</string>
<string name="assets_title">Assets</string>
<!-- Balances -->