Updated on 2026-08-14
This commit is contained in:
parent
a6cff6210b
commit
4c3312186d
6 changed files with 413 additions and 144 deletions
|
|
@ -0,0 +1,337 @@
|
|||
package com.tangem.tap.features.wallet.ui.view
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.AbstractComposeView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.SelectorButton
|
||||
import com.tangem.core.ui.components.SpacerH12
|
||||
import com.tangem.core.ui.components.SpacerH4
|
||||
import com.tangem.core.ui.components.SpacerW16
|
||||
import com.tangem.core.ui.components.SpacerW4
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.common.extensions.formatWithSpaces
|
||||
import com.tangem.tap.features.wallet.models.TotalBalance
|
||||
import com.tangem.tap.features.wallet.redux.ProgressState
|
||||
import com.tangem.wallet.R
|
||||
import com.valentinilk.shimmer.shimmer
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.DecimalFormat
|
||||
import java.util.*
|
||||
|
||||
internal class TotalBalanceCard @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0,
|
||||
) : AbstractComposeView(context, attrs, defStyleAttr) {
|
||||
private var state by mutableStateOf<TotalBalanceCardState>(TotalBalanceCardState.Empty)
|
||||
|
||||
var status: TotalBalance? = null
|
||||
set(value) {
|
||||
if (field == value) return
|
||||
field = value
|
||||
updateState(value, onChangeFiatCurrencyClick)
|
||||
}
|
||||
|
||||
var onChangeFiatCurrencyClick: () -> Unit = { /* no-op */ }
|
||||
set(value) {
|
||||
if (field == value) return
|
||||
field = value
|
||||
updateState(status, value)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
TangemTheme {
|
||||
TotalBalanceCardContent(state = state)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAccessibilityClassName(): CharSequence {
|
||||
return javaClass.name
|
||||
}
|
||||
|
||||
private fun updateState(status: TotalBalance?, onChangeCurrencyClick: () -> Unit) {
|
||||
state = when (status?.state) {
|
||||
null -> TotalBalanceCardState.Empty
|
||||
ProgressState.Loading -> TotalBalanceCardState.Loading(
|
||||
fiatCurrency = status.fiatCurrency,
|
||||
onChangeFiatCurrencyClick = onChangeCurrencyClick,
|
||||
)
|
||||
ProgressState.Error -> TotalBalanceCardState.Failure(
|
||||
amount = status.fiatAmount,
|
||||
fiatCurrency = status.fiatCurrency,
|
||||
onChangeFiatCurrencyClick = onChangeCurrencyClick,
|
||||
)
|
||||
ProgressState.Refreshing,
|
||||
ProgressState.Done,
|
||||
-> TotalBalanceCardState.Success(
|
||||
amount = status.fiatAmount,
|
||||
fiatCurrency = status.fiatCurrency,
|
||||
onChangeFiatCurrencyClick = onChangeCurrencyClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TotalBalanceCardContent(
|
||||
state: TotalBalanceCardState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TotalBalanceCardScaffold(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.main_page_balance),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
},
|
||||
amount = {
|
||||
when (state) {
|
||||
is TotalBalanceCardState.Empty,
|
||||
is TotalBalanceCardState.Loading,
|
||||
-> LoadingAmount()
|
||||
is TotalBalanceCardState.Failure,
|
||||
is TotalBalanceCardState.Success,
|
||||
-> LoadedAmount(
|
||||
amount = buildAmountString(
|
||||
amount = state.amount,
|
||||
fiatCurrencySymbol = state.fiatCurrency.symbol,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
currencySelector = {
|
||||
if (state !is TotalBalanceCardState.Empty) {
|
||||
SelectorButton(
|
||||
text = state.fiatCurrency.code,
|
||||
onClick = state.onChangeFiatCurrencyClick,
|
||||
)
|
||||
}
|
||||
},
|
||||
failureText = {
|
||||
AnimatedVisibility(visible = state is TotalBalanceCardState.Failure) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(id = R.string.main_processing_full_amount),
|
||||
style = TangemTheme.typography.caption,
|
||||
color = TangemTheme.colors.text.attention,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TotalBalanceCardScaffold(
|
||||
title: @Composable () -> Unit,
|
||||
amount: @Composable () -> Unit,
|
||||
currencySelector: @Composable () -> Unit,
|
||||
failureText: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
amountWeight: Float = 0.8f,
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier,
|
||||
shape = TangemTheme.shapes.roundedCornersMedium,
|
||||
color = TangemTheme.colors.background.plain,
|
||||
elevation = TangemTheme.dimens.elevation1,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Top,
|
||||
) {
|
||||
SpacerW16()
|
||||
Column(
|
||||
modifier = Modifier.weight(amountWeight),
|
||||
) {
|
||||
SpacerH12()
|
||||
title()
|
||||
SpacerH4()
|
||||
amount()
|
||||
}
|
||||
currencySelector()
|
||||
SpacerW4()
|
||||
}
|
||||
SpacerH4()
|
||||
Box(
|
||||
modifier = Modifier.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
) {
|
||||
failureText()
|
||||
}
|
||||
SpacerH12()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingAmount(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier = modifier.shimmer()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(TangemTheme.dimens.size116)
|
||||
.height(TangemTheme.dimens.size32)
|
||||
.background(
|
||||
color = TangemTheme.colors.stroke.primary,
|
||||
shape = TangemTheme.shapes.roundedCornersSmall2,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadedAmount(
|
||||
amount: AnnotatedString,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier = modifier) {
|
||||
Text(
|
||||
text = amount,
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun buildAmountString(
|
||||
amount: BigDecimal,
|
||||
fiatCurrencySymbol: String,
|
||||
): AnnotatedString {
|
||||
val format = DecimalFormat.getInstance(Locale.getDefault()) as DecimalFormat
|
||||
val scaledAmount = amount
|
||||
.setScale(2, RoundingMode.HALF_UP)
|
||||
.formatWithSpaces()
|
||||
val integer = scaledAmount.substringBefore('.')
|
||||
val reminder = scaledAmount.substringAfter('.')
|
||||
|
||||
return buildAnnotatedString {
|
||||
append(integer)
|
||||
append(format.decimalFormatSymbols.decimalSeparator)
|
||||
append(
|
||||
AnnotatedString(
|
||||
text = "$reminder $fiatCurrencySymbol",
|
||||
spanStyle = TangemTheme.typography.h3.toSpanStyle(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private sealed interface TotalBalanceCardState {
|
||||
val amount: BigDecimal
|
||||
val onChangeFiatCurrencyClick: () -> Unit
|
||||
val fiatCurrency: FiatCurrency
|
||||
|
||||
object Empty : TotalBalanceCardState {
|
||||
override val amount: BigDecimal = BigDecimal.ZERO
|
||||
override val fiatCurrency: FiatCurrency = FiatCurrency.Default
|
||||
override val onChangeFiatCurrencyClick: () -> Unit = { /* no-op */ }
|
||||
}
|
||||
|
||||
data class Loading(
|
||||
override val fiatCurrency: FiatCurrency,
|
||||
override val onChangeFiatCurrencyClick: () -> Unit,
|
||||
) : TotalBalanceCardState {
|
||||
override val amount: BigDecimal = BigDecimal.ZERO
|
||||
}
|
||||
|
||||
data class Failure(
|
||||
override val amount: BigDecimal,
|
||||
override val fiatCurrency: FiatCurrency,
|
||||
override val onChangeFiatCurrencyClick: () -> Unit,
|
||||
) : TotalBalanceCardState
|
||||
|
||||
data class Success(
|
||||
override val amount: BigDecimal,
|
||||
override val fiatCurrency: FiatCurrency,
|
||||
override val onChangeFiatCurrencyClick: () -> Unit,
|
||||
) : TotalBalanceCardState
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
private fun TotalBalanceCardContentSample(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(all = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
TotalBalanceCardContent(state = TotalBalanceCardState.Empty)
|
||||
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
|
||||
TotalBalanceCardContent(
|
||||
state = TotalBalanceCardState.Loading(FiatCurrency("USD", "USD", "$")) {},
|
||||
)
|
||||
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
|
||||
TotalBalanceCardContent(
|
||||
state = TotalBalanceCardState.Failure(
|
||||
amount = BigDecimal("9917.72"),
|
||||
onChangeFiatCurrencyClick = {},
|
||||
fiatCurrency = FiatCurrency("USD", "USD", "$"),
|
||||
),
|
||||
)
|
||||
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
|
||||
TotalBalanceCardContent(
|
||||
state = TotalBalanceCardState.Success(
|
||||
amount = BigDecimal("9917.72"),
|
||||
onChangeFiatCurrencyClick = {},
|
||||
fiatCurrency = FiatCurrency("USD", "USD", "$"),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun TotalBalanceCardContentPreview_Light() {
|
||||
TangemTheme {
|
||||
TotalBalanceCardContentSample()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun TotalBalanceCardContentPreview_Dark() {
|
||||
TangemTheme(isDark = true) {
|
||||
TotalBalanceCardContentSample()
|
||||
}
|
||||
}
|
||||
// endregion Preview
|
||||
|
|
@ -3,7 +3,6 @@ package com.tangem.tap.features.wallet.ui.wallet
|
|||
import android.widget.Button
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.badoo.mvicore.DiffStrategy
|
||||
import com.badoo.mvicore.modelWatcher
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.domain.common.TapWorkarounds.derivationStyle
|
||||
|
|
@ -11,8 +10,6 @@ import com.tangem.domain.common.TapWorkarounds.isTestCard
|
|||
import com.tangem.tap.common.analytics.events.MainScreen
|
||||
import com.tangem.tap.common.analytics.events.ManageTokens
|
||||
import com.tangem.tap.common.analytics.events.Portfolio
|
||||
import com.tangem.tap.common.extensions.animateVisibility
|
||||
import com.tangem.tap.common.extensions.formatAmountAsSpannedString
|
||||
import com.tangem.tap.common.extensions.getQuantityString
|
||||
import com.tangem.tap.common.extensions.hide
|
||||
import com.tangem.tap.common.extensions.show
|
||||
|
|
@ -21,7 +18,6 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
|
|||
import com.tangem.tap.domain.tokens.CurrenciesRepository
|
||||
import com.tangem.tap.features.tokens.redux.TokensAction
|
||||
import com.tangem.tap.features.wallet.models.TotalBalance
|
||||
import com.tangem.tap.features.wallet.redux.ProgressState
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.features.wallet.redux.WalletState
|
||||
import com.tangem.tap.features.wallet.ui.BalanceStatus
|
||||
|
|
@ -36,14 +32,7 @@ class MultiWalletView : WalletView() {
|
|||
|
||||
private lateinit var walletsAdapter: WalletAdapter
|
||||
|
||||
private val watcher = modelWatcher<WalletState> {
|
||||
val totalBalanceStrategy: DiffStrategy<WalletState> = { old, new ->
|
||||
old.cardId != new.cardId ||
|
||||
old.totalBalance != new.totalBalance ||
|
||||
old.state != new.state ||
|
||||
old.walletsStores.size != new.walletsStores.size
|
||||
}
|
||||
|
||||
private val watcher = modelWatcher {
|
||||
// !!! Workaround !!!
|
||||
// Checking state properties instead of state params can reduce application performance,
|
||||
// but here it is necessary because the WalletStore has an unsuitable equals method
|
||||
|
|
@ -68,13 +57,11 @@ class MultiWalletView : WalletView() {
|
|||
handleBackupWarning(it, showBackupWarnings)
|
||||
}
|
||||
}
|
||||
watch({ it }, totalBalanceStrategy) { walletState ->
|
||||
WalletState::totalBalance { totalBalance ->
|
||||
binding?.let {
|
||||
handleTotalBalance(
|
||||
binding = it,
|
||||
totalBalance = walletState.totalBalance,
|
||||
progressState = walletState.state,
|
||||
walletsCount = walletState.walletsDataFromStores.size,
|
||||
totalBalance = totalBalance,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -95,7 +82,7 @@ class MultiWalletView : WalletView() {
|
|||
lAddress.root.hide()
|
||||
rowButtons.hide()
|
||||
lSingleWalletBalance.root.hide()
|
||||
lCardTotalBalance.root.show()
|
||||
lCardTotalBalance.show()
|
||||
rvMultiwallet.show()
|
||||
btnAddToken.show()
|
||||
}
|
||||
|
|
@ -180,40 +167,11 @@ class MultiWalletView : WalletView() {
|
|||
private fun handleTotalBalance(
|
||||
binding: FragmentWalletBinding,
|
||||
totalBalance: TotalBalance?,
|
||||
progressState: ProgressState,
|
||||
walletsCount: Int,
|
||||
) = with(binding.lCardTotalBalance) {
|
||||
if (walletsCount == 0) {
|
||||
root.isVisible = false
|
||||
} else {
|
||||
if (totalBalance == null) {
|
||||
if (progressState != ProgressState.Loading) {
|
||||
root.isVisible = false
|
||||
}
|
||||
} else {
|
||||
root.isVisible = true
|
||||
// Skip changes when on refreshing state
|
||||
if (totalBalance.state == ProgressState.Refreshing || progressState == ProgressState.Refreshing) {
|
||||
return@with
|
||||
}
|
||||
|
||||
if (totalBalance.state == ProgressState.Loading) {
|
||||
veilBalance.veil()
|
||||
} else {
|
||||
veilBalance.unVeil()
|
||||
}
|
||||
tvProcessing.animateVisibility(show = totalBalance.state == ProgressState.Error)
|
||||
|
||||
tvBalance.text = totalBalance.fiatAmount.formatAmountAsSpannedString(
|
||||
currencySymbol = totalBalance.fiatCurrency.symbol,
|
||||
)
|
||||
tvCurrencyName.text = totalBalance.fiatCurrency.code
|
||||
|
||||
tvCurrencyName.setOnClickListener {
|
||||
store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)
|
||||
}
|
||||
}
|
||||
onChangeFiatCurrencyClick = {
|
||||
store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)
|
||||
}
|
||||
status = totalBalance
|
||||
}
|
||||
|
||||
private fun handleErrorStates(
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class SingleWalletView : WalletView() {
|
|||
btnAddToken.hide()
|
||||
rvPendingTransaction.hide()
|
||||
pbLoadingUserTokens.hide()
|
||||
lCardTotalBalance.root.hide()
|
||||
lCardTotalBalance.hide()
|
||||
lSingleWalletBalance.root.hide()
|
||||
lWalletRescanWarning.root.hide()
|
||||
lWalletBackupWarning.root.hide()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue