commit 19c3365b8a6e460459e1caeb90a324a43fc1c0ae parent a7033d9b940a6e574012f699001c66af25f57fb7 Author: Iván Ávalos <avalos@disroot.org> Date: Thu, 6 Feb 2025 15:28:57 +0100 [wallet] use tx `scopes' field for amount rendering Diffstat:
20 files changed, 102 insertions(+), 8 deletions(-)
diff --git a/wallet/src/main/java/net/taler/wallet/balances/BalanceManager.kt b/wallet/src/main/java/net/taler/wallet/balances/BalanceManager.kt @@ -129,6 +129,11 @@ class BalanceManager( return state.balances.find { it.currency == currency }?.available?.spec } + fun getSpecForCurrency(currency: String, scopes: List<ScopeInfo>) = + scopes.find { it.currency == currency }?.let { scope -> + getSpecForScopeInfo(scope) + } + fun getSpecForScopeInfo(scopeInfo: ScopeInfo): CurrencySpecification? { val state = mState.value if (state !is BalanceState.Success) return null diff --git a/wallet/src/main/java/net/taler/wallet/deposit/TransactionDepositComposable.kt b/wallet/src/main/java/net/taler/wallet/deposit/TransactionDepositComposable.kt @@ -39,6 +39,7 @@ import net.taler.wallet.BottomInsetsSpacer import net.taler.wallet.R import net.taler.wallet.backend.TalerErrorCode.EXCHANGE_GENERIC_KYC_REQUIRED import net.taler.wallet.backend.TalerErrorInfo +import net.taler.wallet.balances.ScopeInfo import net.taler.wallet.transactions.AmountType import net.taler.wallet.transactions.ErrorTransactionButton import net.taler.wallet.transactions.TransactionAction @@ -119,6 +120,10 @@ fun TransactionDepositComposablePreview() { amountEffective = Amount.fromString("TESTKUDOS", "42.23"), targetPaytoUri = "https://exchange.example.org/peer/pull/credit", error = TalerErrorInfo(code = EXCHANGE_GENERIC_KYC_REQUIRED), + scopes = listOf(ScopeInfo.Exchange( + currency = "TESTKUDOS", + url = "exchange.test.taler.net", + )) ) Surface { TransactionDepositComposable(t, true, null) {} diff --git a/wallet/src/main/java/net/taler/wallet/payment/TransactionPaymentComposable.kt b/wallet/src/main/java/net/taler/wallet/payment/TransactionPaymentComposable.kt @@ -39,6 +39,7 @@ import net.taler.wallet.BottomInsetsSpacer import net.taler.wallet.R import net.taler.wallet.backend.TalerErrorCode import net.taler.wallet.backend.TalerErrorInfo +import net.taler.wallet.balances.ScopeInfo import net.taler.wallet.compose.TalerSurface import net.taler.wallet.transactions.AmountType import net.taler.wallet.transactions.ErrorTransactionButton @@ -174,6 +175,10 @@ fun TransactionPaymentComposablePreview() { amountRaw = Amount.fromString("TESTKUDOS", "42.1337"), amountEffective = Amount.fromString("TESTKUDOS", "42.23"), error = TalerErrorInfo(code = TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED), + scopes = listOf(ScopeInfo.Exchange( + currency = "TESTKUDOS", + url = "exchange.test.taler.net", + )) ) TalerSurface { TransactionPaymentComposable(t = t, devMode = true, spec = null, onFulfill = {}) {} diff --git a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullCredit.kt b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullCredit.kt @@ -27,6 +27,7 @@ import net.taler.common.Timestamp import net.taler.wallet.R import net.taler.wallet.backend.TalerErrorCode.EXCHANGE_GENERIC_KYC_REQUIRED import net.taler.wallet.backend.TalerErrorInfo +import net.taler.wallet.balances.ScopeInfo import net.taler.wallet.transactions.ActionButton import net.taler.wallet.transactions.ActionListener import net.taler.wallet.transactions.AmountType @@ -103,6 +104,10 @@ fun TransactionPeerPullCreditPreview(loading: Boolean = false) { ), talerUri = "https://exchange.example.org/peer/pull/credit", error = TalerErrorInfo(code = EXCHANGE_GENERIC_KYC_REQUIRED), + scopes = listOf(ScopeInfo.Exchange( + currency = "TESTKUDOS", + url = "exchange.test.taler.net", + )) ) Surface { TransactionPeerComposable(t, true, null, object: ActionListener { diff --git a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullDebit.kt b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullDebit.kt @@ -26,6 +26,7 @@ import net.taler.common.Timestamp import net.taler.wallet.R import net.taler.wallet.backend.TalerErrorCode.EXCHANGE_GENERIC_KYC_REQUIRED import net.taler.wallet.backend.TalerErrorInfo +import net.taler.wallet.balances.ScopeInfo import net.taler.wallet.transactions.ActionListener import net.taler.wallet.transactions.AmountType import net.taler.wallet.transactions.PeerInfoShort @@ -85,6 +86,10 @@ fun TransactionPeerPullDebitPreview() { summary = "test invoice", ), error = TalerErrorInfo(code = EXCHANGE_GENERIC_KYC_REQUIRED), + scopes = listOf(ScopeInfo.Exchange( + currency = "TESTKUDOS", + url = "exchange.test.taler.net", + )) ) Surface { TransactionPeerComposable(t, true, null, object: ActionListener { diff --git a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushCredit.kt b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushCredit.kt @@ -26,6 +26,7 @@ import net.taler.common.Timestamp import net.taler.wallet.R import net.taler.wallet.backend.TalerErrorCode.EXCHANGE_GENERIC_KYC_REQUIRED import net.taler.wallet.backend.TalerErrorInfo +import net.taler.wallet.balances.ScopeInfo import net.taler.wallet.transactions.ActionButton import net.taler.wallet.transactions.ActionListener import net.taler.wallet.transactions.AmountType @@ -92,6 +93,10 @@ fun TransactionPeerPushCreditPreview() { summary = "test invoice", ), error = TalerErrorInfo(code = EXCHANGE_GENERIC_KYC_REQUIRED), + scopes = listOf(ScopeInfo.Exchange( + currency = "TESTKUDOS", + url = "exchange.test.taler.net", + )) ) Surface { TransactionPeerComposable(t, true, null, object : ActionListener { diff --git a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushDebit.kt b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushDebit.kt @@ -35,6 +35,7 @@ import net.taler.common.Timestamp import net.taler.wallet.R import net.taler.wallet.backend.TalerErrorCode.EXCHANGE_GENERIC_KYC_REQUIRED import net.taler.wallet.backend.TalerErrorInfo +import net.taler.wallet.balances.ScopeInfo import net.taler.wallet.compose.QrCodeUriComposable import net.taler.wallet.compose.TalerSurface import net.taler.wallet.compose.getQrCodeSize @@ -147,6 +148,10 @@ fun TransactionPeerPushDebitPreview(loading: Boolean = false) { ), talerUri = "https://exchange.example.org/peer/pull/credit", error = TalerErrorInfo(code = EXCHANGE_GENERIC_KYC_REQUIRED), + scopes = listOf(ScopeInfo.Exchange( + currency = "TESTKUDOS", + url = "exchange.test.taler.net", + )) ) TalerSurface { diff --git a/wallet/src/main/java/net/taler/wallet/refund/TransactionRefundComposable.kt b/wallet/src/main/java/net/taler/wallet/refund/TransactionRefundComposable.kt @@ -38,6 +38,7 @@ import net.taler.wallet.BottomInsetsSpacer import net.taler.wallet.R import net.taler.wallet.backend.TalerErrorCode import net.taler.wallet.backend.TalerErrorInfo +import net.taler.wallet.balances.ScopeInfo import net.taler.wallet.compose.TalerSurface import net.taler.wallet.transactions.AmountType import net.taler.wallet.transactions.ErrorTransactionButton @@ -129,6 +130,10 @@ fun TransactionRefundComposablePreview() { amountRaw = Amount.fromString("TESTKUDOS", "42.23"), amountEffective = Amount.fromString("TESTKUDOS", "42.1337"), error = TalerErrorInfo(code = TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED), + scopes = listOf(ScopeInfo.Exchange( + currency = "TESTKUDOS", + url = "exchange.test.taler.net", + )) ) TalerSurface { TransactionRefundComposable(t = t, devMode = true, spec = null) {} diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionDepositFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionDepositFragment.kt @@ -41,7 +41,7 @@ class TransactionDepositFragment : TransactionDetailFragment() { if (tx is TransactionDeposit) TransactionDepositComposable( t = tx, devMode = devMode, - spec = balanceManager.getSpecForCurrency(tx.amountRaw.currency), + spec = balanceManager.getSpecForCurrency(tx.amountRaw.currency, tx.scopes), ) { onTransitionButtonClicked(tx, it) } diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionLossFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionLossFragment.kt @@ -30,6 +30,7 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView @@ -45,6 +46,7 @@ import net.taler.wallet.BottomInsetsSpacer import net.taler.wallet.R import net.taler.wallet.backend.TalerErrorCode import net.taler.wallet.backend.TalerErrorInfo +import net.taler.wallet.balances.ScopeInfo import net.taler.wallet.compose.TalerSurface import net.taler.wallet.compose.collectAsStateLifecycleAware import net.taler.wallet.transactions.LossEventType.DenomExpired @@ -65,10 +67,12 @@ class TransactionLossFragment: TransactionDetailFragment() { ): View = ComposeView(requireContext()).apply { setContent { val t by transactionManager.selectedTransaction.collectAsStateLifecycleAware() - val spec = scope?.let { balanceManager.getSpecForScopeInfo(it) } TalerSurface { (t as? TransactionDenomLoss)?.let { tx -> + val spec = remember(tx.amountRaw.currency, tx.scopes) { + balanceManager.getSpecForCurrency(tx.amountRaw.currency, tx.scopes) + } TransitionLossComposable(tx, devMode, spec) { onTransitionButtonClicked(tx, it) } @@ -139,6 +143,10 @@ fun previewLossTransaction(lossEventType: LossEventType) = amountEffective = Amount.fromString("TESTKUDOS", "0.3"), error = TalerErrorInfo(code = TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED), lossEventType = lossEventType, + scopes = listOf(ScopeInfo.Exchange( + currency = "TESTKUDOS", + url = "exchange.test.taler.net", + )) ) @Composable diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionPaymentFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionPaymentFragment.kt @@ -39,7 +39,7 @@ class TransactionPaymentFragment : TransactionDetailFragment() { val t by transactionManager.selectedTransaction.collectAsStateLifecycleAware() (t as? TransactionPayment)?.let { tx -> TransactionPaymentComposable(tx, devMode, - balanceManager.getSpecForCurrency(tx.amountRaw.currency), + balanceManager.getSpecForCurrency(tx.amountRaw.currency, tx.scopes), onFulfill = { url -> launchInAppBrowser(requireContext(), url) }, diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionPeerFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionPeerFragment.kt @@ -70,7 +70,7 @@ class TransactionPeerFragment : TransactionDetailFragment(), ActionListener { t?.let { tx -> TransactionPeerComposable( tx, devMode, - balanceManager.getSpecForCurrency(tx.amountRaw.currency), + balanceManager.getSpecForCurrency(tx.amountRaw.currency, tx.scopes), this@TransactionPeerFragment, ) { onTransitionButtonClicked(tx, it) diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionRefreshFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionRefreshFragment.kt @@ -45,6 +45,7 @@ import net.taler.wallet.BottomInsetsSpacer import net.taler.wallet.R import net.taler.wallet.backend.TalerErrorCode import net.taler.wallet.backend.TalerErrorInfo +import net.taler.wallet.balances.ScopeInfo import net.taler.wallet.compose.TalerSurface import net.taler.wallet.compose.collectAsStateLifecycleAware import net.taler.wallet.transactions.TransactionAction.Abort @@ -64,7 +65,7 @@ class TransactionRefreshFragment : TransactionDetailFragment() { val t by transactionManager.selectedTransaction.collectAsStateLifecycleAware() (t as? TransactionRefresh)?.let { tx -> TransactionRefreshComposable(tx, devMode, - balanceManager.getSpecForCurrency(tx.amountRaw.currency), + balanceManager.getSpecForCurrency(tx.amountRaw.currency, tx.scopes), ) { onTransitionButtonClicked(tx, it) } @@ -124,6 +125,10 @@ private fun TransactionRefreshComposablePreview() { amountRaw = Amount.fromString("TESTKUDOS", "42.23"), amountEffective = Amount.fromString("TESTKUDOS", "42.1337"), error = TalerErrorInfo(code = TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED), + scopes = listOf(ScopeInfo.Exchange( + currency = "TESTKUDOS", + url = "exchange.test.taler.net", + )) ) Surface { TransactionRefreshComposable(t, true, null) {} diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionRefundFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionRefundFragment.kt @@ -38,7 +38,7 @@ class TransactionRefundFragment : TransactionDetailFragment() { val t by transactionManager.selectedTransaction.collectAsStateLifecycleAware() (t as? TransactionRefund)?.let { tx -> TransactionRefundComposable(tx, devMode, - balanceManager.getSpecForCurrency(tx.amountRaw.currency) + balanceManager.getSpecForCurrency(tx.amountRaw.currency, tx.scopes) ) { onTransitionButtonClicked(tx, it) } diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionStateComposable.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionStateComposable.kt @@ -37,6 +37,7 @@ import net.taler.common.RelativeTime import net.taler.common.Timestamp import net.taler.common.toAbsoluteTime import net.taler.wallet.R +import net.taler.wallet.balances.ScopeInfo import net.taler.wallet.compose.TalerSurface import net.taler.wallet.transactions.TransactionMajorState.Aborted import net.taler.wallet.transactions.TransactionMajorState.Aborting @@ -141,6 +142,10 @@ fun TransactionStateComposablePreview() { ), amountRaw = Amount.zero("KUDOS"), amountEffective = Amount.zero("KUDOS"), + scopes = listOf(ScopeInfo.Exchange( + currency = "KUDOS", + url = "exchange.demo.taler.net", + )) )) } } diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionWithdrawalFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionWithdrawalFragment.kt @@ -51,7 +51,7 @@ class TransactionWithdrawalFragment : TransactionDetailFragment(), ActionListene TransactionWithdrawalComposable( t = tx, devMode = devMode, - spec = balanceManager.getSpecForCurrency(tx.amountRaw.currency), + spec = balanceManager.getSpecForCurrency(tx.amountRaw.currency, tx.scopes), actionListener = this@TransactionWithdrawalFragment, ) { onTransitionButtonClicked(tx, it) diff --git a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt @@ -44,6 +44,7 @@ import net.taler.wallet.backend.TalerErrorCode import net.taler.wallet.backend.TalerErrorInfo import net.taler.common.CurrencySpecification import net.taler.common.RelativeTime +import net.taler.wallet.balances.ScopeInfo import net.taler.wallet.refund.RefundPaymentInfo import net.taler.wallet.transactions.TransactionMajorState.Done import net.taler.wallet.transactions.TransactionMajorState.None @@ -108,6 +109,7 @@ sealed class Transaction { abstract val error: TalerErrorInfo? abstract val amountRaw: Amount abstract val amountEffective: Amount + abstract val scopes: List<ScopeInfo> @get:DrawableRes abstract val icon: Int @@ -164,6 +166,7 @@ class TransactionWithdrawal( override val error: TalerErrorInfo? = null, override val amountRaw: Amount, override val amountEffective: Amount, + override val scopes: List<ScopeInfo>, ) : Transaction() { override val icon = R.drawable.transaction_withdrawal @@ -309,6 +312,7 @@ class TransactionPayment( override val error: TalerErrorInfo? = null, override val amountRaw: Amount, override val amountEffective: Amount, + override val scopes: List<ScopeInfo>, val posConfirmation: String? = null, ) : Transaction() { override val icon = R.drawable.transaction_payment @@ -351,6 +355,7 @@ class TransactionRefund( override val error: TalerErrorInfo? = null, override val amountRaw: Amount, override val amountEffective: Amount, + override val scopes: List<ScopeInfo>, ) : Transaction() { override val icon = R.drawable.transaction_refund override val detailPageNav = R.id.action_nav_transactions_detail_refund @@ -372,6 +377,7 @@ class TransactionRefresh( override val error: TalerErrorInfo? = null, override val amountRaw: Amount, override val amountEffective: Amount, + override val scopes: List<ScopeInfo>, ) : Transaction() { override val icon = R.drawable.transaction_refresh override val detailPageNav = R.id.action_nav_transactions_detail_refresh @@ -395,6 +401,7 @@ class TransactionDeposit( override val error: TalerErrorInfo? = null, override val amountRaw: Amount, override val amountEffective: Amount, + override val scopes: List<ScopeInfo>, val targetPaytoUri: String, val depositGroupId: String, ) : Transaction() { @@ -433,6 +440,7 @@ class TransactionPeerPullDebit( override val error: TalerErrorInfo? = null, override val amountRaw: Amount, override val amountEffective: Amount, + override val scopes: List<ScopeInfo>, val info: PeerInfoShort, ) : Transaction() { override val icon = R.drawable.transaction_p2p_outgoing @@ -466,6 +474,7 @@ class TransactionPeerPullCredit( override val error: TalerErrorInfo? = null, override val amountRaw: Amount, override val amountEffective: Amount, + override val scopes: List<ScopeInfo>, val info: PeerInfoShort, val talerUri: String, // val completed: Boolean, maybe @@ -495,6 +504,7 @@ class TransactionPeerPushDebit( override val error: TalerErrorInfo? = null, override val amountRaw: Amount, override val amountEffective: Amount, + override val scopes: List<ScopeInfo>, val info: PeerInfoShort, val talerUri: String? = null, // val completed: Boolean, definitely @@ -530,6 +540,7 @@ class TransactionPeerPushCredit( override val error: TalerErrorInfo? = null, override val amountRaw: Amount, override val amountEffective: Amount, + override val scopes: List<ScopeInfo>, val info: PeerInfoShort, ) : Transaction() { override val icon = R.drawable.transaction_p2p_incoming @@ -562,6 +573,7 @@ class TransactionDenomLoss( override val error: TalerErrorInfo? = null, override val amountRaw: Amount, override val amountEffective: Amount, + override val scopes: List<ScopeInfo>, val lossEventType: LossEventType, ): Transaction() { override val icon: Int = R.drawable.transaction_loss @@ -605,6 +617,10 @@ class DummyTransaction( override val detailPageNav: Int = R.id.nav_transactions_detail_dummy override val amountType: AmountType = AmountType.Neutral override val generalTitleRes: Int = R.string.transaction_dummy_title + override val scopes: List<ScopeInfo> = listOf(ScopeInfo.Exchange( + currency = "TESTKUDOS", + url = "exchange.test.taler.net", + )) override fun getTitle(context: Context): String { return context.getString(R.string.transaction_dummy_title) } diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionsComposable.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionsComposable.kt @@ -76,6 +76,7 @@ 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.ScopeInfo import net.taler.wallet.balances.ScopeInfo.Exchange import net.taler.wallet.cleanExchange import net.taler.wallet.compose.LoadingScreen @@ -492,6 +493,10 @@ fun TransactionsComposableDonePreview() { 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) @@ -522,6 +527,10 @@ fun TransactionsComposablePendingPreview() { 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) diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/TransactionWithdrawalComposable.kt b/wallet/src/main/java/net/taler/wallet/withdraw/TransactionWithdrawalComposable.kt @@ -40,6 +40,7 @@ import net.taler.wallet.BottomInsetsSpacer import net.taler.wallet.R import net.taler.wallet.backend.TalerErrorCode import net.taler.wallet.backend.TalerErrorInfo +import net.taler.wallet.balances.ScopeInfo import net.taler.wallet.cleanExchange import net.taler.wallet.transactions.ActionButton import net.taler.wallet.transactions.ActionListener @@ -154,6 +155,10 @@ fun TransactionWithdrawalComposablePreview() { amountRaw = Amount.fromString("TESTKUDOS", "42.23"), amountEffective = Amount.fromString("TESTKUDOS", "42.1337"), error = TalerErrorInfo(code = TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED), + scopes = listOf(ScopeInfo.Exchange( + currency = "TESTKUDOS", + url = "exchange.test.taler.net", + )) ) val listener = object : ActionListener { diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawSuccessFragment.kt b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawSuccessFragment.kt @@ -67,7 +67,13 @@ class ManualWithdrawSuccessFragment : Fragment() { status = status, qrCodes = qrCodes ?: emptyList(), getQrCodes = { withdrawManager.getQrCodesForPayto(it.paytoUri) }, - spec = status.amountInfo?.amountRaw?.currency?.let { balanceManager.getSpecForCurrency(it) }, + spec = status.amountInfo?.amountRaw?.currency?.let { + selectedTx?.scopes?.let { selectedScopes -> + balanceManager.getSpecForCurrency(it, selectedScopes) + } ?: run { + balanceManager.getSpecForCurrency(it) + } + }, bankAppClick = { onBankAppClick(it) }, shareClick = { onShareClick(it) }, )