commit f458809ebcbf614f6bca4c41240604633d0f111d
parent fafac2b94058cf65a59ca55fa8d105c01d1282c0
Author: Iván Ávalos <avalos@disroot.org>
Date: Thu, 29 Oct 2026 14:05:22 +0100
[wallet] hide new balances screen behind dev mode toggle
Diffstat:
11 files changed, 2232 insertions(+), 400 deletions(-)
diff --git a/wallet/src/main/java/net/taler/wallet/WalletNavHost.kt b/wallet/src/main/java/net/taler/wallet/WalletNavHost.kt
@@ -130,6 +130,7 @@ fun WalletNavHost(
model = model,
onNavigate = onNavigate,
onFulfillPayment = onFulfillPayment,
+ onShowError = onShowError,
)
}
composable<WalletDestination.Transactions> { backStackEntry ->
diff --git a/wallet/src/main/java/net/taler/wallet/balances/LegacyBalancesComposable.kt b/wallet/src/main/java/net/taler/wallet/balances/LegacyBalancesComposable.kt
@@ -0,0 +1,471 @@
+/*
+ * 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.balances
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.consumeWindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+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.material.icons.filled.EventRepeat
+import androidx.compose.material.icons.filled.Warning
+import androidx.compose.material3.Badge
+import androidx.compose.material3.ExperimentalMaterial3Api
+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.PlainTooltip
+import androidx.compose.material3.ProvideTextStyle
+import androidx.compose.material3.Text
+import androidx.compose.material3.TooltipAnchorPosition
+import androidx.compose.material3.TooltipBox
+import androidx.compose.material3.TooltipDefaults
+import androidx.compose.material3.rememberTooltipState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.launch
+import net.taler.common.Amount
+import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.balances.ScopeInfo.Auditor
+import net.taler.wallet.balances.ScopeInfo.Exchange
+import net.taler.wallet.balances.ScopeInfo.ExchangeLegacyKeys
+import net.taler.wallet.balances.ScopeInfo.Global
+import net.taler.wallet.cleanExchange
+import net.taler.wallet.compose.ErrorComposable
+import net.taler.wallet.compose.LoadingScreen
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.compose.cardPaddings
+import net.taler.wallet.donau.DonauSummaryItem
+import net.taler.wallet.ui.theme.TalerTheme
+
+/**
+ * Classic balances overview. Pending amounts are only shown when
+ * [onPendingClicked] is provided; the experimental pager's list page omits them
+ * because each balance has its own card with pending chips.
+ */
+@Composable
+fun LegacyBalancesComposable(
+ innerPadding: PaddingValues,
+ state: BalanceState,
+ devMode: Boolean,
+ networkStatus: Boolean,
+ onWithdrawMoneyClicked: () -> Unit,
+ onGetDemoMoneyClicked: () -> Unit,
+ onBalanceClicked: (balance: BalanceItem) -> Unit,
+ onStatementClicked: (host: String) -> Unit,
+ onPendingClicked: ((balance: BalanceItem) -> Unit)? = null,
+ onShowDiscounts: (() -> Unit)? = null,
+ onShowPasses: (() -> Unit)? = null,
+) {
+ if (state.showWelcome()) {
+ EmptyBalancesComposable(
+ innerPadding = innerPadding,
+ networkStatus = networkStatus,
+ onWithdrawMoneyClicked,
+ onGetDemoMoneyClicked,
+ )
+
+ return
+ }
+
+ when (state) {
+ is BalanceState.Loading -> LoadingScreen()
+ is BalanceState.Error -> ErrorComposable(state.error,
+ devMode = devMode,
+ modifier = Modifier.fillMaxSize())
+ is BalanceState.Success -> LazyColumn(
+ Modifier
+ .consumeWindowInsets(innerPadding)
+ .fillMaxSize(),
+ contentPadding = innerPadding,
+ ) {
+ items(state.balances, key = { it.scopeInfo.hashCode() }) { balance ->
+ LegacyBalanceRow(
+ balance,
+ onClick = { onBalanceClicked(balance) },
+ onPendingClick = onPendingClicked?.let { { it(balance) } },
+ )
+ }
+
+ if (state.donauSummary.isNotEmpty()) stickyHeader {
+ LegacySectionHeader { Text(stringResource(R.string.balances_section_statements)) }
+ }
+
+ items(state.donauSummary) { statement ->
+ LegacyStatementRow(
+ statement,
+ onClick = { onStatementClicked(statement.donauBaseUrl) },
+ )
+ }
+
+ if (onShowDiscounts != null || onShowPasses != null) {
+ stickyHeader {
+ LegacySectionHeader { Text(stringResource(R.string.balances_section_tokens)) }
+ }
+
+ if (onShowDiscounts != null) item {
+ OutlinedCard(Modifier.cardPaddings()) {
+ ListItem(
+ modifier = Modifier.clickable { onShowDiscounts() },
+ leadingContent = { Icon(
+ painterResource(R.drawable.ic_discounts),
+ contentDescription = null,
+ ) },
+ headlineContent = {
+ Text(
+ stringResource(R.string.discounts_title),
+ style = MaterialTheme.typography.titleMedium,
+ )
+ },
+ )
+ }
+ }
+
+ if (onShowPasses != null) item {
+ OutlinedCard(Modifier.cardPaddings()) {
+ ListItem(
+ modifier = Modifier.clickable { onShowPasses() },
+ leadingContent = { Icon(Icons.Default.EventRepeat, contentDescription = null) },
+ headlineContent = {
+ Text(stringResource(R.string.passes_title),
+ style = MaterialTheme.typography.titleMedium)
+ }
+ )
+ }
+ }
+ }
+ }
+
+ else -> {}
+ }
+}
+
+@Composable
+fun LegacySectionHeader(
+ label: @Composable () -> Unit,
+) {
+ Box(Modifier
+ .fillMaxWidth()
+ .background(MaterialTheme.colorScheme.background)) {
+ Box(Modifier.cardPaddings()) {
+ ProvideTextStyle(MaterialTheme.typography.titleMedium
+ .copy(color = MaterialTheme.colorScheme.onBackground)) {
+ label()
+ }
+ }
+ }
+}
+
+@Composable
+fun LegacyBalanceRow(
+ balance: BalanceItem,
+ onClick: () -> Unit,
+ onPendingClick: (() -> Unit)? = null,
+) {
+ OutlinedCard(Modifier.cardPaddings()) {
+ Column {
+ ListItem(
+ modifier = Modifier
+ .animateContentSize()
+ .clickable { onClick() }
+ .padding(vertical = 6.dp),
+ headlineContent = {
+ Text(
+ balance.available.toString(),
+ style = MaterialTheme.typography.displaySmall,
+ textDecoration = if (balance.scopeInfo is ExchangeLegacyKeys) {
+ TextDecoration.LineThrough
+ } else {
+ TextDecoration.None
+ },
+ )
+ },
+ 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)
+ ),
+ )
+
+ is ExchangeLegacyKeys -> Text(
+ stringResource(
+ R.string.balance_scope_exchange,
+ cleanExchange(balance.scopeInfo.url)
+ ),
+ )
+
+ else -> {}
+ }
+ }
+ },
+ trailingContent = {
+ if (balance.scopeInfo is ExchangeLegacyKeys) {
+ LegacyWarningTooltip()
+ }
+ }
+ )
+
+ if (onPendingClick != null &&
+ (!balance.pendingIncoming.isZero() || !balance.pendingOutgoing.isZero())
+ ) {
+ HorizontalDivider()
+ LegacyPendingComposable(balance, onPendingClick)
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun LegacyWarningTooltip() {
+ val tooltipState = rememberTooltipState()
+ val coroutineScope = rememberCoroutineScope()
+
+ TooltipBox(
+ positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above, 4.dp),
+ tooltip = { PlainTooltip { Text(stringResource(R.string.balances_legacy_unavailable)) } },
+ state = tooltipState,
+ ) {
+ Box(
+ modifier = Modifier
+ .clickable { coroutineScope.launch { tooltipState.show() } },
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ Icons.Default.Warning,
+ tint = MaterialTheme.colorScheme.error,
+ contentDescription = stringResource(R.string.balances_legacy_unavailable),
+ )
+ }
+ }
+}
+
+@Composable
+fun LegacyStatementRow(
+ summaryItem: DonauSummaryItem,
+ onClick: () -> Unit,
+) {
+ OutlinedCard(Modifier.cardPaddings()) {
+ Column {
+ ListItem(
+ modifier = Modifier
+ .animateContentSize()
+ .clickable { onClick() }
+ .padding(vertical = 6.dp),
+ headlineContent = {
+ Text(
+ summaryItem.amountReceiptsAvailable.toString(),
+ style = MaterialTheme.typography.displaySmall,
+ )
+ },
+ overlineContent = {
+ ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
+ // FIXME: security risk, faking donauBaseUrl!
+ Text(stringResource(R.string.balance_scope_exchange,
+ summaryItem.legalDomain
+ ?: cleanExchange(summaryItem.donauBaseUrl)))
+ }
+ },
+ trailingContent = {
+ Badge(
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
+ ) {
+ Text(
+ "${summaryItem.year}",
+ modifier = Modifier.padding(6.dp),
+ style = MaterialTheme.typography.titleMedium,
+ )
+ }
+ },
+ )
+ }
+ }
+}
+
+@Composable
+fun LegacyPendingComposable(
+ 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 = TalerTheme.extraColors.success,
+ )
+ }
+
+ 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,
+ )
+ }
+ )
+}
+
+@Preview
+@Composable
+fun LegacyBalancesComposablePreview() {
+ val balances = listOf(
+ BalanceItem(
+ scopeInfo = Global("CHF"),
+ available = Amount.fromJSONString("CHF:10.20"),
+ pendingIncoming = Amount.fromJSONString("CHF:1.20"),
+ pendingOutgoing = Amount.fromJSONString("CHF:0.40"),
+ disablePeerPayments = false,
+ ),
+ BalanceItem(
+ scopeInfo = Exchange("KUDOS", "https://exchange.demo.taler.net"),
+ available = Amount.fromJSONString("KUDOS:1407.37"),
+ pendingIncoming = Amount.fromJSONString("KUDOS:0"),
+ pendingOutgoing = Amount.fromJSONString("KUDOS:2.15"),
+ disablePeerPayments = false,
+ ),
+ BalanceItem(
+ scopeInfo = Auditor("MXN", "https://auditor.taler.banxico.org.mx"),
+ available = Amount.fromJSONString("MXN:5.50"),
+ pendingIncoming = Amount.fromJSONString("MXN:1.40"),
+ pendingOutgoing = Amount.fromJSONString("MXN:0"),
+ disablePeerPayments = false,
+ ),
+ BalanceItem(
+ scopeInfo = ExchangeLegacyKeys("KUDOS", "https://legacy.taler.net", masterPub = "123"),
+ available = Amount.fromJSONString("KUDOS:5.50"),
+ pendingIncoming = Amount.fromJSONString("KUDOS:1.40"),
+ pendingOutgoing = Amount.fromJSONString("KUDOS:0"),
+ disablePeerPayments = false,
+ ),
+ )
+
+ val donauSummary = listOf(
+ DonauSummaryItem(
+ donauBaseUrl = "https://donau.test.taler.net/",
+ legalDomain = "Gnuland",
+ year = 2025,
+ amountReceiptsSubmitted = Amount.fromJSONString("KUDOS:10"),
+ amountReceiptsAvailable = Amount.fromJSONString("KUDOS:10"),
+ )
+ )
+
+ TalerSurface {
+ LegacyBalancesComposable(
+ innerPadding = PaddingValues(0.dp),
+ state = BalanceState.Success(balances, donauSummary),
+ devMode = false,
+ networkStatus = true,
+ onWithdrawMoneyClicked = {},
+ onGetDemoMoneyClicked = {},
+ onBalanceClicked = {},
+ onStatementClicked = {},
+ onPendingClicked = {},
+ onShowDiscounts = {},
+ onShowPasses = {},
+ )
+ }
+}
+
+@Preview
+@Composable
+fun LegacyBalancesComposableErrorPreview() {
+ TalerSurface {
+ LegacyBalancesComposable (
+ innerPadding = PaddingValues(0.dp),
+ state = BalanceState.Error(TalerErrorInfo
+ .makeCustomError("Balances could not be loaded")),
+ devMode = false,
+ networkStatus = false,
+ onWithdrawMoneyClicked = {},
+ onGetDemoMoneyClicked = {},
+ onBalanceClicked = {},
+ onStatementClicked = {},
+ )
+ }
+}
+
+@Preview
+@Composable
+fun LegacyBalancesComposableEmptyPreview() {
+ TalerSurface {
+ LegacyBalancesComposable (
+ innerPadding = PaddingValues(0.dp),
+ state = BalanceState.Success(listOf(), listOf()),
+ devMode = false,
+ networkStatus = false,
+ onWithdrawMoneyClicked = {},
+ onGetDemoMoneyClicked = {},
+ onBalanceClicked = {},
+ onStatementClicked = {},
+ )
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/main/LegacyMainScreen.kt b/wallet/src/main/java/net/taler/wallet/main/LegacyMainScreen.kt
@@ -0,0 +1,396 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2026 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.main
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.BarChart
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material.icons.filled.SelectAll
+import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material.icons.rounded.Close
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.SnackbarResult
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.launch
+import net.taler.wallet.NavigateCallback
+import net.taler.wallet.R
+import net.taler.wallet.WalletDestination
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.balances.BalanceState
+import net.taler.wallet.balances.LegacyBalancesComposable
+import net.taler.wallet.balances.ScopeInfo
+import net.taler.wallet.compose.GlobalScaffold
+import net.taler.wallet.compose.collectAsStateLifecycleAware
+import net.taler.wallet.scan.ScanTab
+import net.taler.wallet.settings.SettingsContent
+import net.taler.wallet.transactions.LegacyTransactionsComposable
+import net.taler.wallet.transactions.TransactionStateFilter
+import net.taler.wallet.transactions.TransactionsResult
+import net.taler.wallet.transactions.onTransactionClicked
+
+private enum class LegacyMainTab { ASSETS, SETTINGS }
+
+/**
+ * Main screen as it looked before the paginated balances redesign.
+ *
+ * This is intentionally self-contained so it can be deleted together with the
+ * developer switch once [NewMainScreen] is ready to become the only main screen.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun LegacyMainScreen(
+ model: MainViewModel,
+ onNavigate: NavigateCallback,
+ onFulfillPayment: (url: String) -> Unit,
+ onShowError: (TalerErrorInfo) -> Unit,
+) {
+ val scope = rememberCoroutineScope()
+ val snackbarHostState = remember { SnackbarHostState() }
+ var tab by rememberSaveable { mutableStateOf(LegacyMainTab.ASSETS) }
+ var showTransactions by rememberSaveable { mutableStateOf(false) }
+ var showSheet by rememberSaveable { mutableStateOf(false) }
+ val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
+
+ var stateFilter by remember { mutableStateOf<TransactionStateFilter?>(null) }
+ var selectionMode by remember { mutableStateOf(false) }
+ val selectedItems = remember { mutableStateListOf<String>() }
+ var showDeleteDialog by remember { mutableStateOf(false) }
+
+ val context = LocalContext.current
+ val testWithdrawalMessage = stringResource(R.string.settings_test_withdrawal)
+ val networkStatus by model.networkManager.networkStatus.observeAsState(false)
+ val balanceState by model.balanceManager.state.observeAsState(BalanceState.None)
+ val viewMode by model.viewMode.collectAsStateLifecycleAware()
+ val devMode by model.devMode.observeAsState(false)
+ val actionButtonUsed by remember {
+ model.settingsManager.getActionButtonUsed(context)
+ }.collectAsStateLifecycleAware(true)
+ val txResult by remember(viewMode.selectedScope, stateFilter) {
+ model.transactionManager.transactionsFlow(viewMode.selectedScope, stateFilter = stateFilter)
+ }.collectAsStateLifecycleAware()
+ val selectedBalance = remember(balanceState, viewMode.selectedScope) {
+ (balanceState as? BalanceState.Success)?.balances?.find {
+ it.scopeInfo == viewMode.selectedScope
+ }
+ }
+ val disableActions = remember(balanceState, networkStatus) {
+ val hasSpendableBalance = (balanceState as? BalanceState.Success)?.balances
+ ?.any { it.scopeInfo !is ScopeInfo.ExchangeLegacyKeys } ?: false
+ !networkStatus || !hasSpendableBalance
+ }
+
+ val showMessageEvent by model.showMessageEvent.observeAsState()
+ LaunchedEffect(showMessageEvent) {
+ showMessageEvent?.getIfNotConsumed()?.let { notify ->
+ val result = snackbarHostState.showSnackbar(
+ message = notify.content,
+ actionLabel = notify.actionLabel,
+ duration = SnackbarDuration.Short
+ )
+ if (result == SnackbarResult.ActionPerformed) {
+ if (notify.actionLabel == "View") {
+ onNavigate(WalletDestination.Mailbox, false)
+ }
+ notify.onAction?.invoke()
+ }
+ }
+ }
+
+ val title = when (tab) {
+ LegacyMainTab.ASSETS -> when {
+ showTransactions -> if (selectionMode) {
+ stringResource(R.string.selection_count, selectedItems.size)
+ } else stringResource(R.string.transactions_title)
+
+ !balanceState.showWelcome() -> stringResource(R.string.balances_title)
+ else -> null
+ }
+
+ LegacyMainTab.SETTINGS -> stringResource(R.string.menu_settings)
+ }
+
+ if (showDeleteDialog) AlertDialog(
+ title = { Text(stringResource(R.string.transactions_delete_selected_dialog_title)) },
+ text = { Text(stringResource(R.string.transactions_delete_selected_dialog_message)) },
+ onDismissRequest = { showDeleteDialog = false },
+ confirmButton = {
+ TextButton(onClick = {
+ model.transactionManager.deleteTransactions(selectedItems) { error ->
+ onShowError(error)
+ }
+ showDeleteDialog = false
+ selectionMode = false
+ selectedItems.clear()
+ }) {
+ Text(stringResource(R.string.transactions_delete))
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = {
+ showDeleteDialog = false
+ }) {
+ Text(stringResource(R.string.cancel))
+ }
+ },
+ )
+
+ BackHandler(selectionMode || (tab == LegacyMainTab.ASSETS && showTransactions)) {
+ if (selectionMode) {
+ selectionMode = false
+ selectedItems.clear()
+ } else {
+ showTransactions = false
+ model.showAssets()
+ }
+ }
+
+ GlobalScaffold(
+ model = model,
+ modifier = Modifier.fillMaxSize(),
+ title = title?.let { { Text(it) } },
+ navigationIcon = if (selectionMode) {
+ {
+ IconButton(onClick = {
+ selectionMode = false
+ selectedItems.clear()
+ }) {
+ Icon(
+ imageVector = Icons.Rounded.Close,
+ contentDescription = null,
+ )
+ }
+ }
+ } else null,
+ actions = {
+ if (selectionMode && txResult is TransactionsResult.Success) {
+ IconButton(onClick = {
+ selectedItems.clear()
+ val successResult = txResult as TransactionsResult.Success
+ selectedItems.addAll(successResult.transactions.map { it.transactionId })
+ }) {
+ Icon(
+ imageVector = Icons.Default.SelectAll,
+ contentDescription = stringResource(R.string.transactions_select_all),
+ )
+ }
+ IconButton(onClick = {
+ showDeleteDialog = true
+ }) {
+ Icon(
+ imageVector = Icons.Default.Delete,
+ contentDescription = stringResource(R.string.transactions_delete),
+ )
+ }
+ }
+ },
+ snackbarHost = { SnackbarHost(snackbarHostState) },
+ bottomBar = {
+ NavigationBar {
+ NavigationBarItem(
+ icon = { Icon(Icons.Default.BarChart, contentDescription = null) },
+ label = { Text(stringResource(R.string.balances_title)) },
+ selected = tab == LegacyMainTab.ASSETS,
+ onClick = {
+ tab = LegacyMainTab.ASSETS
+ showTransactions = false
+ selectionMode = false
+ selectedItems.clear()
+ model.showAssets()
+ },
+ )
+
+ TalerActionButton(
+ demandAttention = !actionButtonUsed,
+ onShowSheet = { showSheet = true },
+ onScanQr = {
+ model.settingsManager.saveActionButtonUsed(context)
+ onNavigate(WalletDestination.ScanQr(ScanTab.SCAN_QR), true)
+ },
+ )
+
+ NavigationBarItem(
+ icon = { Icon(Icons.Default.Settings, contentDescription = null) },
+ label = { Text(stringResource(R.string.menu_settings)) },
+ selected = tab == LegacyMainTab.SETTINGS,
+ onClick = {
+ tab = LegacyMainTab.SETTINGS
+ showTransactions = false
+ selectionMode = false
+ selectedItems.clear()
+ },
+ )
+ }
+ },
+ ) { innerPadding ->
+ LaunchedEffect(Unit) {
+ val initialLoad = model.balanceManager.state.value is BalanceState.None
+ model.balanceManager.loadAssets(initialLoad)
+ }
+
+ when (tab) {
+ LegacyMainTab.ASSETS -> if (showTransactions && selectedBalance != null) {
+ LegacyTransactionsComposable(
+ innerPadding = innerPadding,
+ balance = selectedBalance,
+ txResult = txResult,
+ stateFilter = stateFilter,
+ onTransactionClick = { tx ->
+ onTransactionClicked(tx, model, onNavigate)
+ },
+ onShowBalancesClicked = {
+ showTransactions = false
+ model.showAssets()
+ },
+ selectionMode = selectionMode,
+ selectedItems = selectedItems,
+ onToggleSelection = { txId ->
+ if (selectedItems.contains(txId)) {
+ selectedItems.remove(txId)
+ if (selectedItems.isEmpty()) selectionMode = false
+ } else {
+ selectionMode = true
+ selectedItems.add(txId)
+ }
+ },
+ )
+ } else {
+ LegacyBalancesComposable(
+ innerPadding = innerPadding,
+ state = balanceState,
+ devMode = devMode,
+ networkStatus = networkStatus,
+ onWithdrawMoneyClicked = {
+ onNavigate(
+ WalletDestination.HandleUri("taler://withdraw-exchange/exchange.taler-ops.ch/"),
+ true,
+ )
+ },
+ onGetDemoMoneyClicked = {
+ model.withdrawManager.withdrawTestBalance()
+ scope.launch {
+ snackbarHostState.showSnackbar(testWithdrawalMessage)
+ }
+ },
+ onBalanceClicked = { balance ->
+ model.displayScope(balance.scopeInfo)
+ stateFilter = null
+ showTransactions = true
+ },
+ onPendingClicked = { balance ->
+ model.displayScope(balance.scopeInfo)
+ stateFilter = TransactionStateFilter.Nonfinal
+ showTransactions = true
+ },
+ onStatementClicked = { host ->
+ onNavigate(WalletDestination.DonauStatement(host), false)
+ },
+ onShowDiscounts = {
+ onNavigate(WalletDestination.DiscountList, false)
+ },
+ onShowPasses = {
+ onNavigate(WalletDestination.PassList, false)
+ },
+ )
+ }
+
+ LegacyMainTab.SETTINGS -> SettingsContent(
+ model = model,
+ innerPadding = innerPadding,
+ snackbarHostState = snackbarHostState,
+ onNavigate = onNavigate,
+ onShowError = onShowError,
+ )
+ }
+
+ TalerActionsModal(
+ showSheet = showSheet,
+ sheetState = sheetState,
+ selectedCurrency = selectedBalance?.currency,
+ showShopping = selectedBalance?.shoppingUrls?.isNotEmpty() == true,
+ onDismiss = {
+ showSheet = false
+ },
+ disableActions = disableActions,
+ disablePeer = selectedBalance?.disablePeerPayments == true,
+ onSend = {
+ showSheet = false
+ model.settingsManager.saveActionButtonUsed(context)
+ onNavigate(WalletDestination.OutgoingPush, true)
+ },
+ onReceive = {
+ showSheet = false
+ model.settingsManager.saveActionButtonUsed(context)
+ onNavigate(WalletDestination.OutgoingPull, true)
+ },
+ onScanQr = {
+ showSheet = false
+ model.settingsManager.saveActionButtonUsed(context)
+ onNavigate(WalletDestination.ScanQr(ScanTab.SCAN_QR), true)
+ },
+ onDeposit = {
+ showSheet = false
+ model.settingsManager.saveActionButtonUsed(context)
+ onNavigate(WalletDestination.Deposit(), true)
+ },
+ onWithdraw = {
+ showSheet = false
+ model.settingsManager.saveActionButtonUsed(context)
+ model.withdrawManager.resetWithdrawal()
+ onNavigate(WalletDestination.PromptWithdraw(), true)
+ },
+ onEnterUri = {
+ showSheet = false
+ model.settingsManager.saveActionButtonUsed(context)
+ onNavigate(WalletDestination.ScanQr(ScanTab.ENTER_LINK), true)
+ },
+ onShoppingDiscovery = {
+ showSheet = false
+ model.settingsManager.saveActionButtonUsed(context)
+ val shoppingUrls = selectedBalance?.shoppingUrls ?: emptyList()
+ if (shoppingUrls.size == 1) {
+ onFulfillPayment(shoppingUrls[0])
+ } else onNavigate(WalletDestination.ExchangeShopping, false)
+ }
+ )
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/main/MainScreen.kt b/wallet/src/main/java/net/taler/wallet/main/MainScreen.kt
@@ -11,209 +11,50 @@
* 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/>
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package net.taler.wallet.main
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.navigationBarsPadding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.QrCodeScanner
-import androidx.compose.material.icons.filled.Settings
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MediumFloatingActionButton
-import androidx.compose.material3.PlainTooltip
-import androidx.compose.material3.SnackbarDuration
-import androidx.compose.material3.SnackbarHost
-import androidx.compose.material3.SnackbarHostState
-import androidx.compose.material3.SnackbarResult
-import androidx.compose.material3.Text
-import androidx.compose.material3.TooltipAnchorPosition
-import androidx.compose.material3.TooltipBox
-import androidx.compose.material3.TooltipDefaults
-import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.livedata.observeAsState
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import kotlinx.coroutines.launch
import net.taler.wallet.NavigateCallback
-import net.taler.wallet.R
-import net.taler.wallet.WalletDestination
-import net.taler.wallet.balances.BalanceState
-import net.taler.wallet.balances.BalancesPagerComposable
-import net.taler.wallet.compose.GlobalScaffold
-import net.taler.wallet.compose.collectAsStateLifecycleAware
-import net.taler.wallet.scan.ScanTab
-import net.taler.wallet.transactions.TransactionStateFilter
-import net.taler.wallet.transactions.onTransactionClicked
-import net.taler.wallet.ui.theme.onPrimaryLight
-import net.taler.wallet.ui.theme.primaryLight
+import net.taler.wallet.backend.TalerErrorInfo
-@OptIn(ExperimentalMaterial3Api::class)
+/**
+ * Temporary developer switch between the old and the new main screen.
+ *
+ * The old implementation is fully contained in [LegacyMainScreen] and its
+ * Legacy* composables. Once the new main screen is finished, delete
+ * [LegacyMainScreen], the legacy composables, the developer preference and this
+ * switch.
+ */
@Composable
fun MainScreen(
model: MainViewModel,
onNavigate: NavigateCallback,
onFulfillPayment: (url: String) -> Unit,
+ onShowError: (TalerErrorInfo) -> Unit,
) {
- val scope = rememberCoroutineScope()
- val snackbarHostState = remember { SnackbarHostState() }
-
val context = LocalContext.current
- val testWithdrawalMessage = stringResource(R.string.settings_test_withdrawal)
- val networkStatus by model.networkManager.networkStatus.observeAsState(false)
- val balanceState by model.balanceManager.state.observeAsState(BalanceState.None)
- val viewMode by model.viewMode.collectAsStateLifecycleAware()
- val devMode by model.devMode.observeAsState(false)
- val txResults = remember(balanceState) {
- (balanceState as? BalanceState.Success)?.let { state ->
- state.balances.associate { balance ->
- balance.scopeInfo to model.transactionManager.recentTransactionsFlow(balance.scopeInfo)
- }
- } ?: emptyMap()
- }
-
- val showMessageEvent by model.showMessageEvent.observeAsState()
- LaunchedEffect(showMessageEvent) {
- showMessageEvent?.getIfNotConsumed()?.let { notify ->
- val result = snackbarHostState.showSnackbar(
- message = notify.content,
- actionLabel = notify.actionLabel,
- duration = SnackbarDuration.Short
- )
- if (result == SnackbarResult.ActionPerformed) {
- if (notify.actionLabel == "View") {
- onNavigate(WalletDestination.Mailbox, false)
- }
- notify.onAction?.invoke()
- }
- }
- }
-
- val tooltipState = rememberTooltipState()
- var fabHeight by remember { mutableStateOf(0.dp) }
- val density = LocalDensity.current
-
- GlobalScaffold(
- model = model,
- modifier = Modifier.fillMaxSize(),
- title = {},
- snackbarHost = { SnackbarHost(snackbarHostState) },
- actions = {
- IconButton(onClick = {
- onNavigate(WalletDestination.Settings, false)
- }) {
- Icon(
- Icons.Default.Settings,
- contentDescription = stringResource(R.string.menu_settings))
- }
- },
- floatingActionButton = {
- TooltipBox(
- positionProvider = TooltipDefaults
- .rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
- tooltip = {
- PlainTooltip { Text(stringResource(R.string.button_scan_qr_code_label)) }
- },
- state = tooltipState,
- ) {
- MediumFloatingActionButton(
- modifier = Modifier
- .navigationBarsPadding()
- .onGloballyPositioned { coordinates ->
- fabHeight = with( density) { coordinates.size.height.toDp() }
- },
- shape = CircleShape,
- containerColor = primaryLight,
- contentColor = onPrimaryLight,
- onClick = {
- onNavigate(WalletDestination.ScanQr(ScanTab.SCAN_QR), true)
- },
- ) {
- Icon(
- Icons.Default.QrCodeScanner,
- modifier = Modifier.size(34.dp),
- contentDescription = stringResource(R.string.button_scan_qr_code_label),
- )
- }
- }
- },
- ) { innerPadding ->
- LaunchedEffect(Unit) {
- val initialLoad = model.balanceManager.state.value is BalanceState.None
- model.balanceManager.loadAssets(initialLoad)
- }
-
- BalancesPagerComposable(
- innerPadding = innerPadding,
- bottomFabSkip = fabHeight + 24.dp,
- state = balanceState,
- selectedBalance = viewMode.selectedScope,
- recentTxs = txResults,
- devMode = devMode,
- networkStatus = networkStatus,
- onBalanceSelected = {
- model.displayScope(it?.scopeInfo)
- },
- onWithdrawMoney = {
- onNavigate(
- WalletDestination.HandleUri("taler://withdraw-exchange/exchange.taler-ops.ch/"),
- true,
- )
- },
- onGetDemoMoney = {
- model.withdrawManager.withdrawTestBalance()
- scope.launch {
- snackbarHostState.showSnackbar(testWithdrawalMessage)
- }
- },
- onShopping = { balance ->
- model.settingsManager.saveActionButtonUsed(context)
- if (balance.shoppingUrls.size == 1) {
- onFulfillPayment(balance.shoppingUrls[0])
- } else onNavigate(WalletDestination.ExchangeShopping, false)
- },
- onSend = { onNavigate(WalletDestination.OutgoingPush, true) },
- onRequest = { onNavigate(WalletDestination.OutgoingPull, true) },
- onWithdraw = {
- model.withdrawManager.resetWithdrawal()
- onNavigate(WalletDestination.PromptWithdraw(), true)
- },
- onDeposit = { onNavigate(WalletDestination.Deposit(), true) },
- onTransactionClicked = { tx ->
- onTransactionClicked(tx, model, onNavigate)
- },
- onShowAllTransactions = { scope ->
- onNavigate(WalletDestination.Transactions(), false)
- },
- onShowPendingIncoming = {
- onNavigate(WalletDestination.Transactions(TransactionStateFilter.Nonfinal), false)
- },
- onShowPendingOutgoing = {
- onNavigate(WalletDestination.Transactions(TransactionStateFilter.Nonfinal), false)
- },
- onShowDiscounts = {
- onNavigate(WalletDestination.DiscountList, false)
- },
- onShowPasses = {
- onNavigate(WalletDestination.PassList, false)
- },
+ val newMainScreenEnabled by model.settingsManager
+ .getNewMainScreenEnabled(context)
+ .collectAsState(false)
+
+ if (newMainScreenEnabled) {
+ NewMainScreen(
+ model = model,
+ onNavigate = onNavigate,
+ onFulfillPayment = onFulfillPayment,
+ )
+ } else {
+ LegacyMainScreen(
+ model = model,
+ onNavigate = onNavigate,
+ onFulfillPayment = onFulfillPayment,
+ onShowError = onShowError,
)
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/main/NewMainScreen.kt b/wallet/src/main/java/net/taler/wallet/main/NewMainScreen.kt
@@ -0,0 +1,219 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2026 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.main
+
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.QrCodeScanner
+import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MediumFloatingActionButton
+import androidx.compose.material3.PlainTooltip
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.SnackbarResult
+import androidx.compose.material3.Text
+import androidx.compose.material3.TooltipAnchorPosition
+import androidx.compose.material3.TooltipBox
+import androidx.compose.material3.TooltipDefaults
+import androidx.compose.material3.rememberTooltipState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.launch
+import net.taler.wallet.NavigateCallback
+import net.taler.wallet.R
+import net.taler.wallet.WalletDestination
+import net.taler.wallet.balances.BalanceState
+import net.taler.wallet.balances.BalancesPagerComposable
+import net.taler.wallet.compose.GlobalScaffold
+import net.taler.wallet.compose.collectAsStateLifecycleAware
+import net.taler.wallet.scan.ScanTab
+import net.taler.wallet.transactions.TransactionStateFilter
+import net.taler.wallet.transactions.onTransactionClicked
+import net.taler.wallet.ui.theme.onPrimaryLight
+import net.taler.wallet.ui.theme.primaryLight
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun NewMainScreen(
+ model: MainViewModel,
+ onNavigate: NavigateCallback,
+ onFulfillPayment: (url: String) -> Unit,
+) {
+ val scope = rememberCoroutineScope()
+ val snackbarHostState = remember { SnackbarHostState() }
+
+ val context = LocalContext.current
+ val testWithdrawalMessage = stringResource(R.string.settings_test_withdrawal)
+ val networkStatus by model.networkManager.networkStatus.observeAsState(false)
+ val balanceState by model.balanceManager.state.observeAsState(BalanceState.None)
+ val viewMode by model.viewMode.collectAsStateLifecycleAware()
+ val devMode by model.devMode.observeAsState(false)
+ val txResults = remember(balanceState) {
+ (balanceState as? BalanceState.Success)?.let { state ->
+ state.balances.associate { balance ->
+ balance.scopeInfo to model.transactionManager.recentTransactionsFlow(balance.scopeInfo)
+ }
+ } ?: emptyMap()
+ }
+
+ val showMessageEvent by model.showMessageEvent.observeAsState()
+ LaunchedEffect(showMessageEvent) {
+ showMessageEvent?.getIfNotConsumed()?.let { notify ->
+ val result = snackbarHostState.showSnackbar(
+ message = notify.content,
+ actionLabel = notify.actionLabel,
+ duration = SnackbarDuration.Short
+ )
+ if (result == SnackbarResult.ActionPerformed) {
+ if (notify.actionLabel == "View") {
+ onNavigate(WalletDestination.Mailbox, false)
+ }
+ notify.onAction?.invoke()
+ }
+ }
+ }
+
+ val tooltipState = rememberTooltipState()
+ var fabHeight by remember { mutableStateOf(0.dp) }
+ val density = LocalDensity.current
+
+ GlobalScaffold(
+ model = model,
+ modifier = Modifier.fillMaxSize(),
+ title = {},
+ snackbarHost = { SnackbarHost(snackbarHostState) },
+ actions = {
+ IconButton(onClick = {
+ onNavigate(WalletDestination.Settings, false)
+ }) {
+ Icon(
+ Icons.Default.Settings,
+ contentDescription = stringResource(R.string.menu_settings))
+ }
+ },
+ floatingActionButton = {
+ TooltipBox(
+ positionProvider = TooltipDefaults
+ .rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
+ tooltip = {
+ PlainTooltip { Text(stringResource(R.string.button_scan_qr_code_label)) }
+ },
+ state = tooltipState,
+ ) {
+ MediumFloatingActionButton(
+ modifier = Modifier
+ .navigationBarsPadding()
+ .onGloballyPositioned { coordinates ->
+ fabHeight = with( density) { coordinates.size.height.toDp() }
+ },
+ shape = CircleShape,
+ containerColor = primaryLight,
+ contentColor = onPrimaryLight,
+ onClick = {
+ onNavigate(WalletDestination.ScanQr(ScanTab.SCAN_QR), true)
+ },
+ ) {
+ Icon(
+ Icons.Default.QrCodeScanner,
+ modifier = Modifier.size(34.dp),
+ contentDescription = stringResource(R.string.button_scan_qr_code_label),
+ )
+ }
+ }
+ },
+ ) { innerPadding ->
+ LaunchedEffect(Unit) {
+ val initialLoad = model.balanceManager.state.value is BalanceState.None
+ model.balanceManager.loadAssets(initialLoad)
+ }
+
+ BalancesPagerComposable(
+ innerPadding = innerPadding,
+ bottomFabSkip = fabHeight + 24.dp,
+ state = balanceState,
+ selectedBalance = viewMode.selectedScope,
+ recentTxs = txResults,
+ devMode = devMode,
+ networkStatus = networkStatus,
+ onBalanceSelected = {
+ model.displayScope(it?.scopeInfo)
+ },
+ onWithdrawMoney = {
+ onNavigate(
+ WalletDestination.HandleUri("taler://withdraw-exchange/exchange.taler-ops.ch/"),
+ true,
+ )
+ },
+ onGetDemoMoney = {
+ model.withdrawManager.withdrawTestBalance()
+ scope.launch {
+ snackbarHostState.showSnackbar(testWithdrawalMessage)
+ }
+ },
+ onShopping = { balance ->
+ model.settingsManager.saveActionButtonUsed(context)
+ if (balance.shoppingUrls.size == 1) {
+ onFulfillPayment(balance.shoppingUrls[0])
+ } else onNavigate(WalletDestination.ExchangeShopping, false)
+ },
+ onSend = { onNavigate(WalletDestination.OutgoingPush, true) },
+ onRequest = { onNavigate(WalletDestination.OutgoingPull, true) },
+ onWithdraw = {
+ model.withdrawManager.resetWithdrawal()
+ onNavigate(WalletDestination.PromptWithdraw(), true)
+ },
+ onDeposit = { onNavigate(WalletDestination.Deposit(), true) },
+ onTransactionClicked = { tx ->
+ onTransactionClicked(tx, model, onNavigate)
+ },
+ onShowAllTransactions = { scope ->
+ onNavigate(WalletDestination.Transactions(), false)
+ },
+ onShowPendingIncoming = {
+ onNavigate(WalletDestination.Transactions(TransactionStateFilter.Nonfinal), false)
+ },
+ onShowPendingOutgoing = {
+ onNavigate(WalletDestination.Transactions(TransactionStateFilter.Nonfinal), false)
+ },
+ onShowDiscounts = {
+ onNavigate(WalletDestination.DiscountList, false)
+ },
+ onShowPasses = {
+ onNavigate(WalletDestination.PassList, false)
+ },
+ )
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/main/TalerActionButton.kt b/wallet/src/main/java/net/taler/wallet/main/TalerActionButton.kt
@@ -0,0 +1,252 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2026 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.main
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.gestures.rememberDraggableState
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.LocationOn
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LargeFloatingActionButton
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.PlainTooltip
+import androidx.compose.material3.SheetState
+import androidx.compose.material3.Text
+import androidx.compose.material3.TooltipBox
+import androidx.compose.material3.TooltipDefaults
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.material3.rememberTooltipState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.runBlocking
+import net.taler.wallet.R
+import net.taler.wallet.compose.DemandAttention
+import net.taler.wallet.compose.GridMenu
+import net.taler.wallet.compose.GridMenuItem
+import net.taler.wallet.compose.Material3MenuGroup
+import net.taler.wallet.compose.Material3MenuItemData
+import net.taler.wallet.compose.TalerSurface
+import kotlin.math.roundToInt
+
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+fun TalerActionButton(
+ demandAttention: Boolean,
+ onShowSheet: () -> Unit,
+ onScanQr: () -> Unit,
+) {
+ val tooltipState = rememberTooltipState()
+ TooltipBox(
+ positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
+ tooltip = { PlainTooltip { Text(stringResource(R.string.actions)) } },
+ state = tooltipState,
+ ) {
+ val offsetY = remember { Animatable(0f) }
+ var cancelled by remember { mutableStateOf(false) }
+
+ DemandAttention(demandAttention = demandAttention) {
+ LargeFloatingActionButton(
+ modifier = Modifier
+ .requiredSize(86.dp)
+ .padding(8.dp)
+ .offset { IntOffset(0, offsetY.value.roundToInt() / 6) }
+ .draggable(
+ orientation = Orientation.Vertical,
+ state = rememberDraggableState { delta ->
+ runBlocking { offsetY.snapTo(offsetY.value + delta) }
+ if (delta > 0) {
+ cancelled = true
+ }
+ },
+ onDragStopped = {
+ offsetY.animateTo(0.0f)
+ if (!cancelled) {
+ onScanQr()
+ }
+ cancelled = false
+ },
+ ),
+ shape = CircleShape,
+ onClick = { onShowSheet() },
+ ) {
+ if (offsetY.value == 0.0f) {
+ Icon(
+ painterResource(R.drawable.ic_actions),
+ modifier = Modifier.size(38.dp),
+ contentDescription = stringResource(R.string.actions),
+ )
+ } else {
+ Icon(
+ painterResource(R.drawable.ic_scan_qr),
+ contentDescription = stringResource(R.string.actions),
+ )
+ }
+ }
+ }
+ }
+
+ LaunchedEffect(demandAttention) {
+ if (demandAttention) {
+ tooltipState.show()
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun TalerActionsModal(
+ showSheet: Boolean,
+ sheetState: SheetState,
+ selectedCurrency: String? = null,
+ showShopping: Boolean,
+ disableActions: Boolean,
+ disablePeer: Boolean,
+ onDismiss: () -> Unit,
+ onSend: () -> Unit,
+ onReceive: () -> Unit,
+ onScanQr: () -> Unit,
+ onDeposit: () -> Unit,
+ onWithdraw: () -> Unit,
+ onEnterUri: () -> Unit,
+ onShoppingDiscovery: () -> Unit,
+) {
+ if (showSheet) ModalBottomSheet(
+ onDismissRequest = onDismiss,
+ sheetState = sheetState,
+ ) {
+ Column {
+ if (showShopping && selectedCurrency != null) {
+ Box(Modifier
+ .padding(horizontal = 12.dp)
+ .padding(bottom = 9.dp)
+ ) {
+ Material3MenuGroup(items = buildList {
+ add(
+ Material3MenuItemData(
+ title = { Text(stringResource(R.string.exchange_shopping_label, selectedCurrency)) },
+ icon = { Icon(
+ Icons.Default.LocationOn,
+ contentDescription = null
+ ) },
+ onClick = onShoppingDiscovery,
+ )
+ )
+ })
+ }
+ }
+
+ GridMenu(
+ contentPadding = PaddingValues(
+ start = 8.dp,
+ end = 8.dp,
+ bottom = 8.dp + WindowInsets
+ .systemBars
+ .asPaddingValues()
+ .calculateBottomPadding(),
+ ),
+ ) {
+ GridMenuItem(
+ icon = R.drawable.ic_link,
+ title = R.string.enter_uri,
+ onClick = { onEnterUri(); onDismiss() },
+ )
+
+ GridMenuItem(
+ icon = R.drawable.transaction_deposit,
+ title = R.string.send_deposit_button_label,
+ onClick = { onDeposit(); onDismiss() },
+ enabled = !disableActions
+ )
+
+ GridMenuItem(
+ icon = R.drawable.ic_scan_qr,
+ title = R.string.button_scan_qr_code_label,
+ onClick = { onScanQr(); onDismiss() },
+ )
+
+ GridMenuItem(
+ icon = R.drawable.transaction_p2p_incoming,
+ title = R.string.transactions_receive_funds,
+ onClick = { onReceive(); onDismiss() },
+ enabled = !disableActions && !disablePeer,
+ )
+
+ GridMenuItem(
+ icon = R.drawable.transaction_withdrawal,
+ title = R.string.withdraw_button_label,
+ onClick = { onWithdraw(); onDismiss() },
+ enabled = !disableActions,
+ )
+
+ GridMenuItem(
+ icon = R.drawable.transaction_p2p_outgoing,
+ title = R.string.transactions_send_funds,
+ onClick = { onSend(); onDismiss() },
+ enabled = !disableActions && !disablePeer,
+ )
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Preview
+@Composable
+fun TalerActionsModalPreview() {
+ TalerSurface {
+ TalerActionsModal(
+ showSheet = true,
+ sheetState = rememberModalBottomSheetState(),
+ selectedCurrency = "CHF",
+ showShopping = true,
+ disableActions = false,
+ disablePeer = false,
+ onDismiss = {},
+ onSend = {},
+ onReceive = {},
+ onScanQr = {},
+ onDeposit = {},
+ onWithdraw = {},
+ onEnterUri = {},
+ onShoppingDiscovery = {},
+ )
+ }
+}
+\ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/settings/SettingsManager.kt b/wallet/src/main/java/net/taler/wallet/settings/SettingsManager.kt
@@ -75,6 +75,14 @@ class SettingsManager(
}
}
+ fun getActionButtonUsed(c: Context) = c.userPreferencesDataStore.data.map { prefs ->
+ if (prefs.hasActionButtonUsed()) {
+ prefs.actionButtonUsed
+ } else {
+ false
+ }
+ }
+
fun saveActionButtonUsed(c: Context) = scope.launch {
c.userPreferencesDataStore.updateData { current ->
current.toBuilder()
@@ -99,6 +107,22 @@ class SettingsManager(
}
}
+ fun getNewMainScreenEnabled(c: Context) = c.userPreferencesDataStore.data.map { prefs ->
+ if (prefs.hasNewMainScreenEnabled()) {
+ prefs.newMainScreenEnabled
+ } else {
+ false
+ }
+ }
+
+ fun setNewMainScreenEnabled(c: Context, enabled: Boolean) = scope.launch {
+ c.userPreferencesDataStore.updateData { current ->
+ current.toBuilder()
+ .setNewMainScreenEnabled(enabled)
+ .build()
+ }
+ }
+
fun getBiometricLockEnabled(c: Context) = c.userPreferencesDataStore.data.map { prefs ->
if (prefs.hasBiometricLockEnabled()) {
prefs.biometricLockEnabled
diff --git a/wallet/src/main/java/net/taler/wallet/settings/SettingsScreen.kt b/wallet/src/main/java/net/taler/wallet/settings/SettingsScreen.kt
@@ -38,6 +38,7 @@ import androidx.biometric.BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -53,6 +54,7 @@ import androidx.compose.material.icons.filled.LocalAtm
import androidx.compose.material.icons.filled.Memory
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.VideoLibrary
+import androidx.compose.material.icons.filled.ViewCarousel
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -99,8 +101,34 @@ fun SettingsScreen(
onNavigateBack: () -> Unit,
onShowError: (TalerErrorInfo) -> Unit,
) {
- val context = LocalContext.current
val snackbarHostState = remember { SnackbarHostState() }
+
+ GlobalScaffold(
+ model = model,
+ modifier = Modifier.fillMaxSize(),
+ title = { Text(stringResource(R.string.menu_settings)) },
+ snackbarHost = { SnackbarHost(snackbarHostState) },
+ onNavigateBack = onNavigateBack,
+ ) { innerPadding ->
+ SettingsContent(
+ model = model,
+ innerPadding = innerPadding,
+ snackbarHostState = snackbarHostState,
+ onNavigate = onNavigate,
+ onShowError = onShowError,
+ )
+ }
+}
+
+@Composable
+fun SettingsContent(
+ model: MainViewModel,
+ innerPadding: PaddingValues,
+ snackbarHostState: SnackbarHostState,
+ onNavigate: NavigateCallback,
+ onShowError: (TalerErrorInfo) -> Unit,
+) {
+ val context = LocalContext.current
val dbExportMessage = stringResource(R.string.settings_db_export_message)
val dbImportMessage = stringResource(R.string.settings_db_import_message)
val testWithdrawalMessage = stringResource(R.string.settings_test_withdrawal)
@@ -115,6 +143,7 @@ fun SettingsScreen(
val biometricLockEnabled by settingsManager.getBiometricLockEnabled(context).collectAsState(false)
val devModeEnabled by settingsManager.getDevModeEnabled(context).collectAsState(false)
+ val newMainScreenEnabled by settingsManager.getNewMainScreenEnabled(context).collectAsState(false)
val withdrawTestStatus by withdrawManager.withdrawTestStatus.collectAsState()
val databaseBackend by model.databaseBackend.collectAsState()
@@ -150,256 +179,260 @@ fun SettingsScreen(
}
}
- GlobalScaffold(
- model = model,
- modifier = Modifier.fillMaxSize(),
- title = { Text(stringResource(R.string.menu_settings)) },
- snackbarHost = { SnackbarHost(snackbarHostState) },
- onNavigateBack = onNavigateBack,
- ) { innerPadding ->
- Column(
- modifier = Modifier
- .fillMaxSize()
- .padding(innerPadding)
- .verticalScroll(rememberScrollState())
- ) {
- SettingsItem(
- title = stringResource(R.string.exchange_settings_title),
- summary = stringResource(R.string.exchange_settings_summary),
- icon = Icons.Default.Dns,
- onClick = { onNavigate(WalletDestination.ExchangeList, false) }
- )
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ .verticalScroll(rememberScrollState())
+ ) {
+ SettingsItem(
+ title = stringResource(R.string.exchange_settings_title),
+ summary = stringResource(R.string.exchange_settings_summary),
+ icon = Icons.Default.Dns,
+ onClick = { onNavigate(WalletDestination.ExchangeList, false) }
+ )
- SettingsItem(
- title = stringResource(R.string.settings_bank_accounts),
- summary = stringResource(R.string.settings_bank_accounts_summary),
- icon = Icons.Default.AccountBalance,
- onClick = { onNavigate(WalletDestination.BankAccounts(), false) }
- )
+ SettingsItem(
+ title = stringResource(R.string.settings_bank_accounts),
+ summary = stringResource(R.string.settings_bank_accounts_summary),
+ icon = Icons.Default.AccountBalance,
+ onClick = { onNavigate(WalletDestination.BankAccounts(), false) }
+ )
- SettingsItem(
- title = stringResource(R.string.settings_donau),
- summary = stringResource(R.string.settings_donau_summary),
- icon = ImageVector.vectorResource(R.drawable.ic_donau),
- onClick = { onNavigate(WalletDestination.SetDonau(), false) }
- )
+ SettingsItem(
+ title = stringResource(R.string.settings_donau),
+ summary = stringResource(R.string.settings_donau_summary),
+ icon = ImageVector.vectorResource(R.drawable.ic_donau),
+ onClick = { onNavigate(WalletDestination.SetDonau(), false) }
+ )
- SettingsSwitchItem(
- title = stringResource(R.string.settings_lock_auth),
- summary = stringResource(R.string.settings_lock_auth_summary),
- icon = ImageVector.vectorResource(R.drawable.ic_shield),
- checked = biometricLockEnabled,
- onCheckedChange = { enabled ->
- if (enabled) {
- enableBiometrics(
- context,
- settingsManager,
- true,
- biometricAuthUnavailableMessage
- ) {
- biometricEnrollLauncher.launch(it)
- }
- } else {
- settingsManager.setBiometricLockEnabled(context, false)
+ SettingsSwitchItem(
+ title = stringResource(R.string.settings_lock_auth),
+ summary = stringResource(R.string.settings_lock_auth_summary),
+ icon = ImageVector.vectorResource(R.drawable.ic_shield),
+ checked = biometricLockEnabled,
+ onCheckedChange = { enabled ->
+ if (enabled) {
+ enableBiometrics(
+ context,
+ settingsManager,
+ true,
+ biometricAuthUnavailableMessage
+ ) {
+ biometricEnrollLauncher.launch(it)
}
+ } else {
+ settingsManager.setBiometricLockEnabled(context, false)
}
- )
+ }
+ )
+
+ SettingsSwitchItem(
+ title = stringResource(R.string.settings_dev_mode),
+ summary = stringResource(R.string.settings_dev_mode_summary),
+ icon = ImageVector.vectorResource(R.drawable.ic_developer_mode),
+ checked = devModeEnabled,
+ onCheckedChange = { enabled ->
+ settingsManager.setDevModeEnabled(context, enabled)
+ }
+ )
+
+ if (devModeEnabled) {
+ HorizontalDivider()
SettingsSwitchItem(
- title = stringResource(R.string.settings_dev_mode),
- summary = stringResource(R.string.settings_dev_mode_summary),
- icon = ImageVector.vectorResource(R.drawable.ic_developer_mode),
- checked = devModeEnabled,
+ title = stringResource(R.string.settings_new_main_screen),
+ summary = stringResource(R.string.settings_new_main_screen_summary),
+ icon = Icons.Default.ViewCarousel,
+ checked = newMainScreenEnabled,
onCheckedChange = { enabled ->
- settingsManager.setDevModeEnabled(context, enabled)
+ settingsManager.setNewMainScreenEnabled(context, enabled)
}
)
- if (devModeEnabled) {
- HorizontalDivider()
+ HorizontalDivider()
- SettingsItem(
- title = stringResource(R.string.exchange_list_add_dev),
- icon = Icons.Default.DomainAdd,
- onClick = {
- model.exchangeManager.addDevExchanges()
- onNavigate(WalletDestination.ExchangeList, false)
- }
- )
+ SettingsItem(
+ title = stringResource(R.string.exchange_list_add_dev),
+ icon = Icons.Default.DomainAdd,
+ onClick = {
+ model.exchangeManager.addDevExchanges()
+ onNavigate(WalletDestination.ExchangeList, false)
+ }
+ )
- SettingsItem(
- title = stringResource(R.string.settings_withdraw_testkudos),
- summary = stringResource(R.string.settings_withdraw_testkudos_summary),
- icon = Icons.Default.LocalAtm,
- enabled = withdrawTestStatus !is TestWithdrawStatus.Withdrawing,
- onClick = {
- withdrawManager.withdrawTestBalance()
- scope.launch { snackbarHostState.showSnackbar(testWithdrawalMessage) }
- onNavigate(WalletDestination.Main, true)
- }
- )
+ SettingsItem(
+ title = stringResource(R.string.settings_withdraw_testkudos),
+ summary = stringResource(R.string.settings_withdraw_testkudos_summary),
+ icon = Icons.Default.LocalAtm,
+ enabled = withdrawTestStatus !is TestWithdrawStatus.Withdrawing,
+ onClick = {
+ withdrawManager.withdrawTestBalance()
+ scope.launch { snackbarHostState.showSnackbar(testWithdrawalMessage) }
+ onNavigate(WalletDestination.Main, true)
+ }
+ )
- SettingsItem(
- title = stringResource(R.string.settings_logcat),
- summary = stringResource(R.string.settings_logcat_summary),
- icon = ImageVector.vectorResource(R.drawable.ic_bug_report),
- onClick = { logLauncher.launch("taler-wallet-log-${System.currentTimeMillis()}.txt") }
- )
+ SettingsItem(
+ title = stringResource(R.string.settings_logcat),
+ summary = stringResource(R.string.settings_logcat_summary),
+ icon = ImageVector.vectorResource(R.drawable.ic_bug_report),
+ onClick = { logLauncher.launch("taler-wallet-log-${System.currentTimeMillis()}.txt") }
+ )
- SettingsItem(
- title = stringResource(R.string.settings_stats),
- summary = stringResource(R.string.settings_stats_summary),
- icon = ImageVector.vectorResource(R.drawable.ic_stats),
- onClick = { onNavigate(WalletDestination.PerformanceStats, false) }
- )
+ SettingsItem(
+ title = stringResource(R.string.settings_stats),
+ summary = stringResource(R.string.settings_stats_summary),
+ icon = ImageVector.vectorResource(R.drawable.ic_stats),
+ onClick = { onNavigate(WalletDestination.PerformanceStats, false) }
+ )
- SettingsItem(
- title = "Contacts",
- summary = "Contacts",
- icon = Icons.Default.Person,
- onClick = { onNavigate(WalletDestination.Contacts(), false) }
- )
+ SettingsItem(
+ title = "Contacts",
+ summary = "Contacts",
+ icon = Icons.Default.Person,
+ onClick = { onNavigate(WalletDestination.Contacts(), false) }
+ )
- SettingsItem(
- title = "Mailbox",
- summary = "P2P Messaging",
- icon = ImageVector.vectorResource(R.drawable.ic_adjust),
- onClick = { onNavigate(WalletDestination.Mailbox, false) }
- )
+ SettingsItem(
+ title = "Mailbox",
+ summary = "P2P Messaging",
+ icon = ImageVector.vectorResource(R.drawable.ic_adjust),
+ onClick = { onNavigate(WalletDestination.Mailbox, false) }
+ )
- SettingsItem(
- title = stringResource(R.string.alias_registration_title),
- summary = "Taldir Registration",
- icon = Icons.Default.Person,
- onClick = { onNavigate(WalletDestination.AliasRegistration, false) }
- )
+ SettingsItem(
+ title = stringResource(R.string.alias_registration_title),
+ summary = "Taldir Registration",
+ icon = Icons.Default.Person,
+ onClick = { onNavigate(WalletDestination.AliasRegistration, false) }
+ )
- SettingsItem(
- title = stringResource(R.string.settings_db_export),
- summary = stringResource(R.string.settings_db_export_summary),
- icon = ImageVector.vectorResource(R.drawable.ic_unarchive),
- onClick = { pendingDbExport = DbExportType.Json }
- )
+ SettingsItem(
+ title = stringResource(R.string.settings_db_export),
+ summary = stringResource(R.string.settings_db_export_summary),
+ icon = ImageVector.vectorResource(R.drawable.ic_unarchive),
+ onClick = { pendingDbExport = DbExportType.Json }
+ )
- SettingsItem(
- title = stringResource(R.string.settings_db_export_raw),
- summary = stringResource(R.string.settings_db_export_raw_summary),
- icon = ImageVector.vectorResource(R.drawable.ic_database),
- onClick = { pendingDbExport = DbExportType.Raw }
- )
+ SettingsItem(
+ title = stringResource(R.string.settings_db_export_raw),
+ summary = stringResource(R.string.settings_db_export_raw_summary),
+ icon = ImageVector.vectorResource(R.drawable.ic_database),
+ onClick = { pendingDbExport = DbExportType.Raw }
+ )
- SettingsItem(
- title = stringResource(R.string.settings_db_import),
- summary = stringResource(R.string.settings_db_import_summary),
- icon = ImageVector.vectorResource(R.drawable.ic_archive),
- onClick = {
- MaterialAlertDialogBuilder(context)
- .setMessage(R.string.settings_dialog_import_message)
- .setNegativeButton(R.string.import_db) { _, _ ->
- dbImportLauncher.launch(arrayOf("application/json"))
- }
- .setPositiveButton(R.string.cancel) { _, _ ->
- scope.launch { snackbarHostState.showSnackbar(importCanceledMessage) }
- }
- .show()
- }
- )
+ SettingsItem(
+ title = stringResource(R.string.settings_db_import),
+ summary = stringResource(R.string.settings_db_import_summary),
+ icon = ImageVector.vectorResource(R.drawable.ic_archive),
+ onClick = {
+ MaterialAlertDialogBuilder(context)
+ .setMessage(R.string.settings_dialog_import_message)
+ .setNegativeButton(R.string.import_db) { _, _ ->
+ dbImportLauncher.launch(arrayOf("application/json"))
+ }
+ .setPositiveButton(R.string.cancel) { _, _ ->
+ scope.launch { snackbarHostState.showSnackbar(importCanceledMessage) }
+ }
+ .show()
+ }
+ )
- if (databaseBackend == WalletDatabaseBackend.IndexedDB) SettingsItem(
- title = stringResource(R.string.settings_migrate_db),
- summary = stringResource(R.string.settings_migrate_db_summary),
- icon = Icons.Default.Memory,
- onClick = model::offerDatabaseMigration,
- )
+ if (databaseBackend == WalletDatabaseBackend.IndexedDB) SettingsItem(
+ title = stringResource(R.string.settings_migrate_db),
+ summary = stringResource(R.string.settings_migrate_db_summary),
+ icon = Icons.Default.Memory,
+ onClick = model::offerDatabaseMigration,
+ )
+
+ SettingsItem(
+ title = stringResource(R.string.settings_version_app),
+ summary = "${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR} ${BuildConfig.VERSION_CODE})",
+ icon = ImageVector.vectorResource(R.drawable.ic_account_balance_wallet),
+ )
+
+ SettingsItem(
+ title = stringResource(R.string.settings_version_core),
+ summary = "$walletVersion ($walletVersionHash)",
+ icon = ImageVector.vectorResource(R.drawable.ic_adjust),
+ )
+ if (exchangeVersion != null) {
SettingsItem(
- title = stringResource(R.string.settings_version_app),
- summary = "${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR} ${BuildConfig.VERSION_CODE})",
- icon = ImageVector.vectorResource(R.drawable.ic_account_balance_wallet),
+ title = stringResource(R.string.settings_version_protocol_exchange),
+ summary = exchangeVersion,
+ icon = ImageVector.vectorResource(R.drawable.ic_account_balance),
)
+ }
+ if (merchantVersion != null) {
SettingsItem(
- title = stringResource(R.string.settings_version_core),
- summary = "$walletVersion ($walletVersionHash)",
- icon = ImageVector.vectorResource(R.drawable.ic_adjust),
+ title = stringResource(R.string.settings_version_protocol_merchant),
+ summary = merchantVersion,
+ icon = ImageVector.vectorResource(R.drawable.ic_store_mall),
)
+ }
- if (exchangeVersion != null) {
- SettingsItem(
- title = stringResource(R.string.settings_version_protocol_exchange),
- summary = exchangeVersion,
- icon = ImageVector.vectorResource(R.drawable.ic_account_balance),
- )
+ SettingsItem(
+ title = stringResource(R.string.settings_test),
+ summary = stringResource(R.string.settings_test_summary),
+ icon = Icons.Default.VideoLibrary,
+ onClick = {
+ settingsManager.runIntegrationTest { onShowError(it) }
+ scope.launch { snackbarHostState.showSnackbar(testRunningMessage) }
+ onNavigate(WalletDestination.Main, true)
}
+ )
- if (merchantVersion != null) {
- SettingsItem(
- title = stringResource(R.string.settings_version_protocol_merchant),
- summary = merchantVersion,
- icon = ImageVector.vectorResource(R.drawable.ic_store_mall),
- )
+ SettingsItem(
+ title = stringResource(R.string.settings_reset),
+ summary = stringResource(R.string.settings_reset_summary),
+ icon = ImageVector.vectorResource(R.drawable.ic_nuke),
+ onClick = {
+ MaterialAlertDialogBuilder(context)
+ .setMessage(R.string.settings_dialog_reset_message)
+ .setNegativeButton(R.string.reset) { _, _ ->
+ settingsManager.clearDb {
+ model.dangerouslyReset()
+ }
+ scope.launch { snackbarHostState.showSnackbar(resetDoneMessage) }
+ }
+ .setPositiveButton(R.string.cancel) { _, _ ->
+ scope.launch { snackbarHostState.showSnackbar(resetCanceledMessage) }
+ }
+ .show()
}
+ )
- SettingsItem(
- title = stringResource(R.string.settings_test),
- summary = stringResource(R.string.settings_test_summary),
- icon = Icons.Default.VideoLibrary,
- onClick = {
- settingsManager.runIntegrationTest { onShowError(it) }
- scope.launch { snackbarHostState.showSnackbar(testRunningMessage) }
- onNavigate(WalletDestination.Main, true)
- }
- )
-
- SettingsItem(
- title = stringResource(R.string.settings_reset),
- summary = stringResource(R.string.settings_reset_summary),
- icon = ImageVector.vectorResource(R.drawable.ic_nuke),
- onClick = {
- MaterialAlertDialogBuilder(context)
- .setMessage(R.string.settings_dialog_reset_message)
- .setNegativeButton(R.string.reset) { _, _ ->
- settingsManager.clearDb {
- model.dangerouslyReset()
- }
- scope.launch { snackbarHostState.showSnackbar(resetDoneMessage) }
+ pendingDbExport?.let { exportType ->
+ AlertDialog(
+ onDismissRequest = { pendingDbExport = null },
+ title = { Text(stringResource(R.string.wallet_export_database)) },
+ text = { Text(stringResource(R.string.wallet_export_database_warning)) },
+ confirmButton = {
+ Button(onClick = {
+ pendingDbExport = null
+ when (exportType) {
+ DbExportType.Json ->
+ dbExportLauncher.launch("taler-wallet-db-${System.currentTimeMillis()}.json")
+ DbExportType.Raw ->
+ rawDbExportLauncher.launch("taler-wallet-db-${System.currentTimeMillis()}.sqlite3")
}
- .setPositiveButton(R.string.cancel) { _, _ ->
- scope.launch { snackbarHostState.showSnackbar(resetCanceledMessage) }
- }
- .show()
+ }) {
+ Text(stringResource(R.string.wallet_export_database_confirm))
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = { pendingDbExport = null }) {
+ Text(stringResource(R.string.cancel))
+ }
}
)
-
- pendingDbExport?.let { exportType ->
- AlertDialog(
- onDismissRequest = { pendingDbExport = null },
- title = { Text(stringResource(R.string.wallet_export_database)) },
- text = { Text(stringResource(R.string.wallet_export_database_warning)) },
- confirmButton = {
- Button(onClick = {
- pendingDbExport = null
- when (exportType) {
- DbExportType.Json ->
- dbExportLauncher.launch("taler-wallet-db-${System.currentTimeMillis()}.json")
- DbExportType.Raw ->
- rawDbExportLauncher.launch("taler-wallet-db-${System.currentTimeMillis()}.sqlite3")
- }
- }) {
- Text(stringResource(R.string.wallet_export_database_confirm))
- }
- },
- dismissButton = {
- TextButton(onClick = { pendingDbExport = null }) {
- Text(stringResource(R.string.cancel))
- }
- }
- )
- }
}
-
- BottomInsetsSpacer()
}
+
+ BottomInsetsSpacer()
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/LegacyTransactionsComposable.kt b/wallet/src/main/java/net/taler/wallet/transactions/LegacyTransactionsComposable.kt
@@ -0,0 +1,589 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2024 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.transactions
+
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.basicMarquee
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.consumeWindowInsets
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+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.filled.ArrowDropDown
+import androidx.compose.material.icons.rounded.CheckCircle
+import androidx.compose.material.icons.rounded.RadioButtonUnchecked
+import androidx.compose.material3.Badge
+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
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Alignment.Companion.Center
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalHapticFeedback
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.common.CurrencySpecification
+import net.taler.common.Timestamp
+import net.taler.lib.android.toRelativeTime
+import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorCode
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.balances.BalanceItem
+import net.taler.wallet.balances.LegacyWarningTooltip
+import net.taler.wallet.balances.ScopeInfo.Exchange
+import net.taler.wallet.balances.ScopeInfo.ExchangeLegacyKeys
+import net.taler.wallet.cleanExchange
+import net.taler.wallet.compose.Banner
+import net.taler.wallet.compose.LoadingScreen
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.compose.cardPaddings
+import net.taler.wallet.transactions.AmountType.Negative
+import net.taler.wallet.transactions.AmountType.Neutral
+import net.taler.wallet.transactions.AmountType.Positive
+import net.taler.wallet.transactions.TransactionAction.Abort
+import net.taler.wallet.transactions.TransactionAction.Retry
+import net.taler.wallet.transactions.TransactionAction.Suspend
+import net.taler.wallet.transactions.TransactionMajorState.Aborted
+import net.taler.wallet.transactions.TransactionMajorState.Aborting
+import net.taler.wallet.transactions.TransactionMajorState.Dialog
+import net.taler.wallet.transactions.TransactionMajorState.Done
+import net.taler.wallet.transactions.TransactionMajorState.Failed
+import net.taler.wallet.transactions.TransactionMajorState.Pending
+import net.taler.wallet.transactions.TransactionMinorState.BalanceKycRequired
+import net.taler.wallet.transactions.TransactionMinorState.BankConfirmTransfer
+import net.taler.wallet.transactions.TransactionMinorState.KycAuthRequired
+import net.taler.wallet.transactions.TransactionMinorState.KycInit
+import net.taler.wallet.transactions.TransactionMinorState.KycRequired
+import net.taler.wallet.transactions.TransactionMinorState.Repurchase
+import net.taler.wallet.transactions.TransactionStateFilter.Nonfinal
+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.ui.theme.TalerTheme
+
+@Composable
+fun LegacyTransactionsComposable(
+ innerPadding: PaddingValues,
+ stateFilter: TransactionStateFilter?,
+ balance: BalanceItem,
+ txResult: TransactionsResult,
+ onTransactionClick: (tx: Transaction) -> Unit,
+ onShowBalancesClicked: () -> Unit,
+ selectionMode: Boolean,
+ selectedItems: MutableList<String>,
+ onToggleSelection: (String) -> Unit,
+) {
+ Column(Modifier.fillMaxSize()) {
+ LazyColumn(
+ Modifier
+ .weight(1f)
+ .consumeWindowInsets(innerPadding),
+ contentPadding = innerPadding,
+ ) {
+ item {
+ LegacyTransactionsHeader(
+ balance = balance,
+ onShowBalancesClicked = onShowBalancesClicked,
+ )
+ }
+
+ when (stateFilter) {
+ 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 {
+ Box(Modifier.padding(placeholderPadding)) {
+ LegacyEmptyTransactionsComposable()
+ }
+ } else {
+ items(txResult.transactions, key = { it.transactionId }) { tx ->
+ val isSelected = selectedItems.contains(tx.transactionId)
+
+ LegacyTransactionRow(
+ tx, balance.available.spec,
+ isSelected = isSelected,
+ selectionMode = selectionMode,
+ onTransactionClick = {
+ if (selectionMode) {
+ onToggleSelection(tx.transactionId)
+ } else {
+ onTransactionClick(tx)
+ }
+ },
+ onTransactionSelect = {
+ onToggleSelection(tx.transactionId)
+ },
+ )
+ }
+ }
+
+ is None -> item {
+ Box(Modifier.padding(placeholderPadding)) {
+ LoadingScreen()
+ }
+ }
+ is Error -> item {
+ Box(Modifier.padding(placeholderPadding)) {
+ LegacyErrorTransactionsComposable(txResult.error)
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun LegacyEmptyTransactionsComposable() {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Center,
+ ) {
+ Text(
+ stringResource(R.string.transactions_empty),
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ }
+}
+
+@Composable
+fun LegacyErrorTransactionsComposable(error: TalerErrorInfo) {
+ Text(
+ text = stringResource(R.string.transactions_error, error.userFacingMsg),
+ style = MaterialTheme.typography.titleLarge,
+ color = MaterialTheme.colorScheme.error,
+ )
+}
+
+@Composable
+fun LegacyTransactionsHeader(
+ balance: BalanceItem,
+ onShowBalancesClicked: () -> Unit,
+) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ OutlinedCard(
+ Modifier
+ .weight(1f)
+ .cardPaddings()
+ .clickable { onShowBalancesClicked() },
+ ) {
+ ListItem(
+ modifier = Modifier.animateContentSize(),
+ headlineContent = {
+ Text(
+ getHeaderCurrency(balance, balance.available.spec),
+ modifier = Modifier.weight(1f, fill = false),
+ style = MaterialTheme.typography.titleMedium,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1,
+ )
+ },
+ supportingContent = {
+ if (balance.scopeInfo is Exchange) {
+ Text(
+ cleanExchange(balance.scopeInfo.url),
+ modifier = Modifier
+ .padding(top = 3.dp)
+ .basicMarquee(),
+ style = MaterialTheme.typography.bodySmall,
+ )
+ } else if (balance.scopeInfo is ExchangeLegacyKeys) {
+ Text(
+ cleanExchange(balance.scopeInfo.url),
+ modifier = Modifier
+ .padding(top = 3.dp)
+ .basicMarquee(),
+ style = MaterialTheme.typography.bodySmall,
+ )
+ }
+ },
+ trailingContent = {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ if (balance.scopeInfo is ExchangeLegacyKeys) {
+ LegacyWarningTooltip()
+ }
+ Icon(Icons.Default.ArrowDropDown, contentDescription = null)
+ }
+ }
+ )
+ }
+
+ Column(
+ modifier = Modifier.padding(16.dp),
+ horizontalAlignment = Alignment.End,
+ ) {
+ Text(
+ stringResource(R.string.transactions_balance),
+ modifier = Modifier.padding(bottom = 6.dp),
+ style = MaterialTheme.typography.bodySmall,
+ )
+
+ Text(
+ balance.available.toString(showSymbol = false),
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold,
+ textDecoration = if (balance.scopeInfo is ExchangeLegacyKeys) {
+ TextDecoration.LineThrough
+ } else {
+ TextDecoration.None
+ },
+ )
+ }
+ }
+}
+
+@Composable
+fun LegacyTransactionRow(
+ tx: Transaction,
+ spec: CurrencySpecification?,
+ isSelected: Boolean,
+ selectionMode: Boolean,
+ onTransactionClick: () -> Unit,
+ onTransactionSelect: () -> Unit,
+) {
+ val haptic = LocalHapticFeedback.current
+
+ Column {
+ ListItem(
+ modifier = Modifier
+ .height(IntrinsicSize.Min)
+ .defaultMinSize(minHeight = 80.dp)
+ .combinedClickable(
+ onClick = onTransactionClick,
+ onLongClick = {
+ haptic.performHapticFeedback(HapticFeedbackType.LongPress)
+ onTransactionSelect()
+ },
+ ),
+ trailingContent = {
+ Box(
+ modifier = Modifier
+ .fillMaxHeight()
+ .padding(8.dp),
+ contentAlignment = Center,
+ ) {
+ LegacyTransactionAmountInfo(tx, spec)
+ }
+ },
+ leadingContent = {
+ Box(
+ modifier = Modifier
+ .fillMaxHeight()
+ .padding(8.dp),
+ contentAlignment = Center,
+ ) {
+ if (!selectionMode) {
+ Icon(painterResource(tx.icon), contentDescription = null)
+ } else if (isSelected) {
+ Icon(
+ Icons.Rounded.CheckCircle,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary,
+ )
+ } else {
+ Icon(
+ Icons.Rounded.RadioButtonUnchecked,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.outline,
+ )
+ }
+ }
+ },
+ headlineContent = {
+ Column(Modifier.padding(vertical = 8.dp)) {
+ Text(
+ text = tx.timestamp.ms.toRelativeTime(LocalContext.current).toString(),
+ style = MaterialTheme.typography.labelSmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ Text(
+ tx.getTitle(),
+ modifier = Modifier.padding(vertical = 3.dp),
+ style = MaterialTheme.typography.titleMedium,
+ )
+ ProvideTextStyle(
+ MaterialTheme.typography.bodyMedium.copy(
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ ) {
+ LegacyTransactionExtraInfo(tx)
+ }
+ }
+ },
+ colors = ListItemDefaults.colors(
+ containerColor = if (isSelected) {
+ MaterialTheme.colorScheme.secondaryContainer
+ } else {
+ ListItemDefaults.containerColor
+ }
+ )
+ )
+
+ HorizontalDivider()
+ }
+}
+
+@Composable
+fun LegacyTransactionAmountInfo(
+ tx: Transaction,
+ spec: CurrencySpecification?,
+) {
+ Column(horizontalAlignment = Alignment.End) {
+ ProvideTextStyle(MaterialTheme.typography.titleLarge) {
+ val amountStr = tx.amountEffective.withSpec(spec).toString(showSymbol = false)
+ if (tx.amountEffective.isZero()) {
+ Text(amountStr)
+ } else when (tx.amountType) {
+ Positive -> Text(
+ stringResource(R.string.amount_positive, amountStr),
+ color = if (tx.txState.major == Pending || tx.txState.major == Dialog)
+ Color.Unspecified else TalerTheme.extraColors.success,
+ )
+ Negative -> Text(
+ stringResource(R.string.amount_negative, amountStr),
+ color = if (tx.txState.major == Pending || tx.txState.major == Dialog)
+ Color.Unspecified else MaterialTheme.colorScheme.error,
+ )
+ Neutral -> Text(amountStr)
+ }
+ }
+
+ if (tx.txState.major == Pending || tx.txState.major == Dialog) {
+ Badge(
+ Modifier.padding(top = 3.dp),
+ containerColor = MaterialTheme.colorScheme.secondary,
+ ) {
+ Text(stringResource(R.string.transaction_pending))
+ }
+ }
+ }
+}
+
+@Composable
+fun LegacyTransactionExtraInfo(tx: Transaction) {
+ when {
+ tx.txState.major == Aborted -> Text(
+ stringResource(R.string.payment_aborted),
+ color = MaterialTheme.colorScheme.error,
+ )
+
+ tx.txState.minor == Repurchase -> Text(
+ stringResource(R.string.payment_repurchase),
+ )
+
+ tx.txState.major == Failed -> Text(
+ stringResource(R.string.payment_failed),
+ color = MaterialTheme.colorScheme.error,
+ )
+
+ tx.txState.major == Aborting -> Text(
+ stringResource(R.string.payment_aborting),
+ color = MaterialTheme.colorScheme.error,
+ )
+
+ tx.txState.major == Pending -> when(tx.txState.minor) {
+ BankConfirmTransfer -> Text(stringResource(R.string.withdraw_waiting_confirm))
+ KycInit -> Text(stringResource(R.string.transaction_preparing_kyc))
+ KycRequired,
+ KycAuthRequired,
+ BalanceKycRequired -> Text(stringResource(R.string.transactions_required_kyc))
+ else -> {}
+ }
+
+ tx.txState.major == Dialog -> {}
+
+ tx is TransactionWithdrawal && !tx.confirmed -> Text(stringResource(R.string.withdraw_waiting_confirm))
+ tx is TransactionPeerPushCredit && tx.info.summary != null -> Text(tx.info.summary)
+ tx is TransactionPeerPushDebit && tx.info.summary != null -> Text(tx.info.summary)
+ tx is TransactionPeerPullCredit && tx.info.summary != null -> Text(tx.info.summary)
+ tx is TransactionPeerPullDebit && tx.info.summary != null -> Text(tx.info.summary)
+ }
+}
+
+@Composable
+private fun getHeaderCurrency(
+ balance: BalanceItem,
+ spec: CurrencySpecification?,
+) = if (spec != null) {
+ if (spec.symbol != null && spec.name != spec.symbol) {
+ // Name (symbol)
+ stringResource(R.string.transactions_currency, spec.name, spec.symbol!!)
+ } else if (spec.name != balance.currency) {
+ // Name (currency string)
+ stringResource(R.string.transactions_currency, spec.name, balance.currency)
+ } else balance.currency
+} else balance.currency
+
+private val previewBalance = BalanceItem(
+ scopeInfo = Exchange("MXN", "https://exchange.taler.banxico.org.mx"),
+ available = Amount.fromJSONString("MXN:5.50"),
+ pendingIncoming = Amount.fromJSONString("MXN:1.40"),
+ pendingOutgoing = Amount.fromJSONString("MXN:0"),
+ disablePeerPayments = false,
+)
+
+@Preview
+@Composable
+fun LegacyTransactionsComposableDonePreview() {
+ val t = TransactionWithdrawal(
+ transactionId = "transactionId",
+ timestamp = Timestamp.fromMillis(System.currentTimeMillis() - 360 * 60 * 1000),
+ txState = TransactionState(Done),
+ txActions = listOf(Retry, Suspend, Abort),
+ exchangeBaseUrl = "https://exchange.demo.taler.net/",
+ withdrawalDetails = WithdrawalDetails.TalerBankIntegrationApi(false),
+ amountRaw = Amount.fromString("TESTKUDOS", "42.23"),
+ amountEffective = Amount.fromString("TESTKUDOS", "42.1337"),
+ error = TalerErrorInfo(code = TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED),
+ scopes = listOf(Exchange(
+ currency = "TESTKUDOS",
+ url = "exchange.test.taler.net",
+ ))
+ )
+
+ val transactions = listOf(t)
+
+ TalerSurface {
+ LegacyTransactionsComposable(
+ innerPadding = PaddingValues(0.dp),
+ balance = previewBalance,
+ stateFilter = null,
+ txResult = Success(transactions),
+ onTransactionClick = {},
+ onShowBalancesClicked = {},
+ selectionMode = false,
+ selectedItems = mutableListOf(),
+ onToggleSelection = {},
+ )
+ }
+}
+
+@Preview
+@Composable
+fun LegacyTransactionsComposablePendingPreview() {
+ val t = TransactionWithdrawal(
+ transactionId = "transactionId",
+ timestamp = Timestamp.fromMillis(System.currentTimeMillis() - 360 * 60 * 1000),
+ txState = TransactionState(Pending),
+ txActions = listOf(Retry, Suspend, Abort),
+ exchangeBaseUrl = "https://exchange.demo.taler.net/",
+ withdrawalDetails = WithdrawalDetails.TalerBankIntegrationApi(false),
+ amountRaw = Amount.fromString("TESTKUDOS", "42.23"),
+ amountEffective = Amount.fromString("TESTKUDOS", "42.1337"),
+ error = TalerErrorInfo(code = TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED),
+ scopes = listOf(Exchange(
+ currency = "TESTKUDOS",
+ url = "exchange.test.taler.net",
+ ))
+ )
+
+ val transactions = listOf(t)
+
+ TalerSurface {
+ LegacyTransactionsComposable(
+ innerPadding = PaddingValues(0.dp),
+ balance = previewBalance,
+ stateFilter = Nonfinal,
+ txResult = Success(transactions),
+ onTransactionClick = {},
+ onShowBalancesClicked = {},
+ selectionMode = false,
+ selectedItems = mutableListOf(),
+ onToggleSelection = {},
+ )
+ }
+}
+
+@Preview
+@Composable
+fun LegacyTransactionsComposableEmptyPreview() {
+ TalerSurface {
+ LegacyTransactionsComposable(
+ innerPadding = PaddingValues(0.dp),
+ balance = previewBalance,
+ stateFilter = null,
+ txResult = Success(listOf()),
+ onTransactionClick = {},
+ onShowBalancesClicked = {},
+ selectionMode = false,
+ selectedItems = mutableListOf(),
+ onToggleSelection = {},
+ )
+ }
+}
+
+@Preview
+@Composable
+fun LegacyTransactionsComposableLoadingPreview() {
+ TalerSurface {
+ LegacyTransactionsComposable(
+ innerPadding = PaddingValues(0.dp),
+ balance = previewBalance,
+ stateFilter = null,
+ txResult = None,
+ onTransactionClick = {},
+ onShowBalancesClicked = {},
+ selectionMode = false,
+ selectedItems = mutableListOf(),
+ onToggleSelection = {},
+ )
+ }
+}
+\ No newline at end of file
diff --git a/wallet/src/main/proto/user_prefs.proto b/wallet/src/main/proto/user_prefs.proto
@@ -51,4 +51,6 @@ message UserPreferences {
optional bool biometricLockEnabled = 4;
optional PrefsViewMode viewMode = 5;
optional PrefsViewMode2 viewMode2 = 6;
+ // Temporary developer switch between the legacy and the new main screen.
+ optional bool newMainScreenEnabled = 7;
}
\ No newline at end of file
diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml
@@ -558,6 +558,8 @@ GNU Taler is immune to many types of fraud such as credit card data theft, phish
<string name="settings_db_migrate_complete">SQLite database migration complete</string>
<string name="settings_dev_mode">Developer mode</string>
<string name="settings_dev_mode_summary">Shows more information intended for debugging</string>
+ <string name="settings_new_main_screen">New main screen</string>
+ <string name="settings_new_main_screen_summary">Use the experimental paginated balances main screen (temporary)</string>
<string name="settings_dialog_import_message">This operation will overwrite your existing database. Do you want to continue?</string>
<string name="settings_dialog_reset_message">Do you really want to reset the wallet and lose all coins and purchases?</string>
<string name="settings_donau">Tax-deductible donations</string>