Updated on 2026-08-14

This commit is contained in:
Tangem 2023-08-29 17:59:44 +05:00
parent 29fb436bc2
commit 2fd73f5a50
14 changed files with 262 additions and 54 deletions

View file

@ -31,6 +31,7 @@ dependencies {
implementation(deps.arrow.core)
implementation(deps.jodatime)
implementation(deps.kotlin.immutable.collections)
implementation(deps.reKotlin)
implementation(deps.tangem.blockchain)
implementation(deps.tangem.card.core)
implementation(deps.timber)

View file

@ -1,16 +1,14 @@
package com.tangem.feature.tokendetails.presentation.tokendetails
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton
import com.tangem.features.tokendetails.impl.R
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.flow.MutableStateFlow
internal object TokenDetailsPreviewData {
@ -42,40 +40,20 @@ internal object TokenDetailsPreviewData {
),
)
// TODO: [REDACTED_JIRA]
val actionButtons = persistentListOf(
ActionButtonConfig(
text = TextReference.Str(value = "Buy"),
iconResId = R.drawable.ic_plus_24,
onClick = {},
),
ActionButtonConfig(
text = TextReference.Str(value = "Send"),
iconResId = R.drawable.ic_arrow_up_24,
onClick = {},
),
ActionButtonConfig(
text = TextReference.Str(value = "Receive"),
iconResId = R.drawable.ic_arrow_down_24,
onClick = {},
),
ActionButtonConfig(
text = TextReference.Str(value = "Exchange"),
iconResId = R.drawable.ic_exchange_vertical_24,
onClick = {},
),
private val actionButtons = persistentListOf(
TokenDetailsActionButton.Buy(enabled = true, onClick = {}),
TokenDetailsActionButton.Send(enabled = true, onClick = {}),
TokenDetailsActionButton.Receive(onClick = {}),
TokenDetailsActionButton.Swap(enabled = true, onClick = {}),
)
// TODO: [REDACTED_JIRA]
val disabledActionButtons = actionButtons.map { it.copy(enabled = false) }.toPersistentList()
val balanceLoading = TokenDetailsBalanceBlockState.Loading(actionButtons = disabledActionButtons)
val balanceLoading = TokenDetailsBalanceBlockState.Loading(actionButtons = actionButtons)
val balanceContent = TokenDetailsBalanceBlockState.Content(
actionButtons = actionButtons,
fiatBalance = "123,00$",
cryptoBalance = "866,96 USDT",
)
val balanceError = TokenDetailsBalanceBlockState.Error(actionButtons = disabledActionButtons)
val balanceError = TokenDetailsBalanceBlockState.Error(actionButtons = actionButtons)
private val marketPriceLoading = MarketPriceBlockState.Loading(currencyName = "USDT")

View file

@ -1,23 +1,31 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton
import kotlinx.collections.immutable.ImmutableList
sealed class TokenDetailsBalanceBlockState {
internal sealed class TokenDetailsBalanceBlockState {
abstract val actionButtons: ImmutableList<ActionButtonConfig>
abstract val actionButtons: ImmutableList<TokenDetailsActionButton>
data class Loading(
override val actionButtons: ImmutableList<ActionButtonConfig>,
override val actionButtons: ImmutableList<TokenDetailsActionButton>,
) : TokenDetailsBalanceBlockState()
data class Content(
override val actionButtons: ImmutableList<ActionButtonConfig>,
override val actionButtons: ImmutableList<TokenDetailsActionButton>,
val fiatBalance: String,
val cryptoBalance: String,
) : TokenDetailsBalanceBlockState()
data class Error(
override val actionButtons: ImmutableList<ActionButtonConfig>,
override val actionButtons: ImmutableList<TokenDetailsActionButton>,
) : TokenDetailsBalanceBlockState()
fun copyActionButtons(buttons: ImmutableList<TokenDetailsActionButton>): TokenDetailsBalanceBlockState {
return when (this) {
is Content -> this.copy(actionButtons = buttons)
is Error -> this.copy(actionButtons = buttons)
is Loading -> this.copy(actionButtons = buttons)
}
}
}

View file

@ -3,7 +3,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
data class TokenDetailsState(
internal data class TokenDetailsState(
val topAppBarConfig: TokenDetailsTopAppBarConfig,
val tokenInfoBlockState: TokenInfoBlockState,
val tokenBalanceBlockState: TokenDetailsBalanceBlockState,

View file

@ -0,0 +1,87 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.components
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.tokendetails.impl.R
@Immutable
internal sealed class TokenDetailsActionButton(val config: ActionButtonConfig) {
/** Lambda be invoked when manage button is clicked */
abstract val onClick: () -> Unit
/**
* Buy
*
* @property enabled button click availability
* @property onClick lambda be invoked when Buy button is clicked
*/
data class Buy(val enabled: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton(
config = ActionButtonConfig(
text = TextReference.Res(id = R.string.common_buy),
iconResId = R.drawable.ic_plus_24,
onClick = onClick,
enabled = enabled,
),
)
/**
* Send
*
* @property enabled button click availability
* @property onClick lambda be invoked when Send button is clicked
*/
data class Send(val enabled: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton(
config = ActionButtonConfig(
text = TextReference.Res(id = R.string.common_send),
iconResId = R.drawable.ic_arrow_up_24,
onClick = onClick,
enabled = enabled,
),
)
/**
* Receive
*
* @property onClick lambda be invoked when Receive button is clicked
*/
data class Receive(override val onClick: () -> Unit) : TokenDetailsActionButton(
config = ActionButtonConfig(
text = TextReference.Res(id = R.string.common_receive),
iconResId = R.drawable.ic_arrow_down_24,
onClick = onClick,
enabled = true,
),
)
/**
* Sell
*
* @property enabled button click availability
* @property onClick lambda be invoked when Sell button is clicked
*/
data class Sell(val enabled: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton(
config = ActionButtonConfig(
text = TextReference.Res(id = R.string.common_sell),
iconResId = R.drawable.ic_currency_24,
onClick = onClick,
enabled = enabled,
),
)
/**
* Swap
*
* @property enabled button click availability
* @property onClick lambda be invoked when Swap button is clicked
*/
data class Swap(val enabled: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton(
config = ActionButtonConfig(
text = TextReference.Res(id = R.string.common_swap),
iconResId = R.drawable.ic_exchange_vertical_24,
onClick = onClick,
enabled = enabled,
),
)
}

View file

@ -0,0 +1,47 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
import com.tangem.common.Provider
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
internal class TokenDetailsActionButtonsConverter(
private val currentStateProvider: Provider<TokenDetailsState>,
private val clickIntents: TokenDetailsClickIntents,
) : Converter<List<TokenActionsState.ActionState>, TokenDetailsState> {
override fun convert(value: List<TokenActionsState.ActionState>): TokenDetailsState {
val state = currentStateProvider()
return state.copy(
tokenBalanceBlockState = state.tokenBalanceBlockState.copyActionButtons(value.mapToManageButtons()),
)
}
private fun List<TokenActionsState.ActionState>.mapToManageButtons(): ImmutableList<TokenDetailsActionButton> {
return this
.map { action ->
when (action) {
is TokenActionsState.ActionState.Buy -> {
TokenDetailsActionButton.Buy(enabled = action.enabled, onClick = clickIntents::onBuyClick)
}
is TokenActionsState.ActionState.Receive -> {
TokenDetailsActionButton.Receive(onClick = clickIntents::onReceiveClick)
}
is TokenActionsState.ActionState.Sell -> {
TokenDetailsActionButton.Sell(enabled = action.enabled, onClick = clickIntents::onSellClick)
}
is TokenActionsState.ActionState.Send -> {
TokenDetailsActionButton.Send(enabled = action.enabled, onClick = clickIntents::onSendClick)
}
is TokenActionsState.ActionState.Swap -> {
TokenDetailsActionButton.Swap(enabled = action.enabled, onClick = clickIntents::onSwapClick)
}
}
}
.toImmutableList()
}
}

View file

@ -8,13 +8,12 @@ import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.error.CurrencyError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.utils.converter.Converter
import java.math.BigDecimal
class TokenDetailsLoadedBalanceConverter(
internal class TokenDetailsLoadedBalanceConverter(
private val currentStateProvider: Provider<TokenDetailsState>,
private val appCurrencyProvider: Provider<AppCurrency>,
) : Converter<Either<CurrencyError, CryptoCurrencyStatus>, TokenDetailsState> {
@ -32,24 +31,27 @@ class TokenDetailsLoadedBalanceConverter(
val state = currentStateProvider()
val currencyName = state.marketPriceBlockState.currencyName
return state.copy(
tokenBalanceBlockState = getBalanceState(status),
tokenBalanceBlockState = getBalanceState(state.tokenBalanceBlockState, status),
marketPriceBlockState = getMarketPriceState(status = status.value, currencyName = currencyName),
)
}
private fun getBalanceState(status: CryptoCurrencyStatus): TokenDetailsBalanceBlockState {
private fun getBalanceState(
currentState: TokenDetailsBalanceBlockState,
status: CryptoCurrencyStatus,
): TokenDetailsBalanceBlockState {
return when (status.value) {
is CryptoCurrencyStatus.NoQuote,
is CryptoCurrencyStatus.Loaded,
-> {
TokenDetailsBalanceBlockState.Content(
actionButtons = TokenDetailsPreviewData.actionButtons,
actionButtons = currentState.actionButtons,
fiatBalance = formatFiatAmount(status.value, appCurrencyProvider()),
cryptoBalance = formatCryptoAmount(status),
)
}
is CryptoCurrencyStatus.Loading -> {
TokenDetailsBalanceBlockState.Loading(TokenDetailsPreviewData.disabledActionButtons)
TokenDetailsBalanceBlockState.Loading(currentState.actionButtons)
}
is CryptoCurrencyStatus.MissedDerivation,
is CryptoCurrencyStatus.NoAccount,
@ -57,7 +59,7 @@ class TokenDetailsLoadedBalanceConverter(
// TODO: [REDACTED_JIRA]
is CryptoCurrencyStatus.Unreachable,
-> {
TokenDetailsBalanceBlockState.Error(TokenDetailsPreviewData.actionButtons)
TokenDetailsBalanceBlockState.Error(currentState.actionButtons)
}
}
}

View file

@ -4,14 +4,16 @@ import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.extensions.iconResId
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSkeletonStateConverter.SkeletonModel
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
internal class TokenDetailsSkeletonStateConverter(
@ -37,7 +39,7 @@ internal class TokenDetailsSkeletonStateConverter(
},
),
tokenBalanceBlockState = TokenDetailsBalanceBlockState.Loading(
TokenDetailsPreviewData.disabledActionButtons,
actionButtons = createButtons(),
),
marketPriceBlockState = MarketPriceBlockState.Loading(value.cryptoCurrency.name),
txHistoryState = TxHistoryState.Content(
@ -48,5 +50,15 @@ internal class TokenDetailsSkeletonStateConverter(
)
}
private fun createButtons(): ImmutableList<TokenDetailsActionButton> {
return persistentListOf(
TokenDetailsActionButton.Buy(enabled = false, onClick = {}),
TokenDetailsActionButton.Send(enabled = false, onClick = {}),
TokenDetailsActionButton.Receive(onClick = {}),
TokenDetailsActionButton.Sell(enabled = false, onClick = {}),
TokenDetailsActionButton.Swap(enabled = false, onClick = {}),
)
}
data class SkeletonModel(val cryptoCurrency: CryptoCurrency)
}

View file

@ -6,6 +6,7 @@ import com.tangem.common.Provider
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.error.CurrencyError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.txhistory.models.TxHistoryListError
@ -35,6 +36,13 @@ internal class TokenDetailsStateFactory(
)
}
private val tokenDetailsButtonsConverter by lazy {
TokenDetailsActionButtonsConverter(
currentStateProvider = currentStateProvider,
clickIntents = clickIntents,
)
}
private val loadingTransactionsStateConverter by lazy {
TokenDetailsLoadingTxHistoryConverter(currentStateProvider = currentStateProvider, clickIntents = clickIntents)
}
@ -60,6 +68,10 @@ internal class TokenDetailsStateFactory(
return tokenDetailsLoadedBalanceConverter.convert(cryptoCurrencyEither)
}
fun getManageButtonsState(actions: List<TokenActionsState.ActionState>): TokenDetailsState {
return tokenDetailsButtonsConverter.convert(actions)
}
fun getLoadingTxHistoryState(itemsCountEither: Either<TxHistoryStateError, Int>): TokenDetailsState {
return loadingTransactionsStateConverter.convert(value = itemsCountEither)
}

View file

@ -15,7 +15,9 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton
import com.tangem.features.tokendetails.impl.R
import kotlinx.collections.immutable.toImmutableList
@Composable
internal fun TokenDetailsBalanceBlock(state: TokenDetailsBalanceBlockState, modifier: Modifier = Modifier) {
@ -51,7 +53,7 @@ internal fun TokenDetailsBalanceBlock(state: TokenDetailsBalanceBlockState, modi
)
HorizontalActionChips(
buttons = state.actionButtons,
buttons = state.actionButtons.map(TokenDetailsActionButton::config).toImmutableList(),
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12),
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing12),
)

View file

@ -7,4 +7,12 @@ interface TokenDetailsClickIntents : TxHistoryClickIntents {
fun onBackClick()
fun onMoreClick()
fun onSendClick()
fun onReceiveClick()
fun onSellClick()
fun onSwapClick()
}

View file

@ -9,11 +9,16 @@ import arrow.core.getOrElse
import com.tangem.common.Provider
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
import com.tangem.domain.tokens.GetCurrencyUseCase
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter
@ -39,6 +44,8 @@ internal class TokenDetailsViewModel @Inject constructor(
private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase,
private val getExploreUrlUseCase: GetExploreUrlUseCase,
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase,
private val reduxStateHolder: ReduxStateHolder,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents {
@ -48,6 +55,8 @@ internal class TokenDetailsViewModel @Inject constructor(
var router by Delegates.notNull<InnerTokenDetailsRouter>()
private val marketPriceJobHolder = JobHolder()
private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null
private var wallet by Delegates.notNull<UserWallet>()
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
private val stateFactory = TokenDetailsStateFactory(
@ -61,26 +70,39 @@ internal class TokenDetailsViewModel @Inject constructor(
private set
override fun onCreate(owner: LifecycleOwner) {
updateContent(selectedWallet = getWallet(), refresh = false)
getWallet()
updateContent(selectedWallet = wallet, refresh = false)
}
private fun getWallet(): UserWallet {
private fun getWallet() {
return getSelectedWalletUseCase()
.fold(
ifLeft = { error("Can not get selected wallet $it") },
ifRight = { it },
ifRight = { wallet = it },
)
}
private fun updateContent(selectedWallet: UserWallet, refresh: Boolean) {
updateMarketPrice(selectedWallet = selectedWallet, refresh = refresh)
updateButtons(userWalletId = selectedWallet.walletId, currencyId = cryptoCurrency.id.value)
updateTxHistory()
}
private fun updateButtons(userWalletId: UserWalletId, currencyId: String) {
getCryptoCurrencyActionsUseCase(userWalletId = userWalletId, tokenId = currencyId)
.distinctUntilChanged()
.onEach { uiState = stateFactory.getManageButtonsState(actions = it.states) }
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
}
private fun updateMarketPrice(selectedWallet: UserWallet, refresh: Boolean) {
getCurrencyUseCase(userWalletId = selectedWallet.walletId, currencyId = cryptoCurrency.id, refresh = refresh)
.distinctUntilChanged()
.onEach { uiState = stateFactory.getCurrencyLoadedBalanceState(it) }
.onEach { either ->
uiState = stateFactory.getCurrencyLoadedBalanceState(either)
either.onRight { status -> cryptoCurrencyStatus = status }
}
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
.saveIn(marketPriceJobHolder)
@ -129,16 +151,45 @@ internal class TokenDetailsViewModel @Inject constructor(
}
override fun onBuyClick() {
// TODO: [REDACTED_JIRA]
val status = cryptoCurrencyStatus ?: return
reduxStateHolder.dispatch(
TradeCryptoAction.New.Buy(
userWallet = wallet,
cryptoCurrencyStatus = status,
appCurrencyCode = selectedAppCurrencyFlow.value.code,
),
)
}
override fun onReloadClick() {
updateTxHistory()
}
override fun onSendClick() {
reduxStateHolder.dispatch(TradeCryptoAction.New.Send)
}
override fun onReceiveClick() {
// TODO: [REDACTED_JIRA]
}
override fun onSellClick() {
val status = cryptoCurrencyStatus ?: return
reduxStateHolder.dispatch(
TradeCryptoAction.New.Sell(
cryptoCurrencyStatus = status,
appCurrencyCode = selectedAppCurrencyFlow.value.code,
),
)
}
override fun onSwapClick() {
reduxStateHolder.dispatch(TradeCryptoAction.New.Swap(cryptoCurrency))
}
override fun onExploreClick() {
viewModelScope.launch {
val wallet = getWallet()
router.openUrl(
url = getExploreUrlUseCase(
userWalletId = wallet.walletId,

View file

@ -13,7 +13,7 @@ import com.tangem.feature.wallet.impl.R
[REDACTED_AUTHOR]
*/
@Immutable
sealed class WalletManageButton(val config: ActionButtonConfig) {
internal sealed class WalletManageButton(val config: ActionButtonConfig) {
/** Lambda be invoked when manage button is clicked */
abstract val onClick: () -> Unit