taler-android

Android apps for GNU Taler (wallet, PoS, cashier)
Log | Files | Refs | README | LICENSE

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:
Mwallet/src/main/java/net/taler/wallet/MainFragment.kt | 2+-
Mwallet/src/main/java/net/taler/wallet/balances/BalanceManager.kt | 18++++++++++++++----
Mwallet/src/main/java/net/taler/wallet/balances/BalancesComposable.kt | 45++++++++++++++++++++++++++++++++++++---------
Mwallet/src/main/java/net/taler/wallet/donau/DonauStatementComposable.kt | 84++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mwallet/src/main/java/net/taler/wallet/donau/DonauStatementFragment.kt | 51++++++++++++++++++++++++++++++++++-----------------
Mwallet/src/main/java/net/taler/wallet/main/MainComposable.kt | 2+-
Mwallet/src/main/res/navigation/nav_graph.xml | 2+-
Mwallet/src/main/res/values/strings.xml | 2+-
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 -->