Updated on 2026-08-14

This commit is contained in:
Tangem 2024-09-03 11:05:02 +03:00
parent d7374fb83f
commit 9de7841edd
38 changed files with 687 additions and 372 deletions

View file

@ -5,7 +5,6 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.NetworkAddress
@ -48,11 +47,6 @@ object TradeCryptoMiddleware {
is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId)
is TradeCryptoAction.Buy -> proceedBuyAction(state, action)
is TradeCryptoAction.Sell -> proceedSellAction(action)
is TradeCryptoAction.Stake -> openStaking(
userWalletId = action.userWalletId,
cryptoCurrencyId = action.cryptoCurrencyId,
yield = action.yield,
)
is TradeCryptoAction.SendToken -> handleNewSendToken(action = action)
is TradeCryptoAction.SendCoin -> handleNewSendCoin(action = action)
}
@ -140,18 +134,6 @@ object TradeCryptoMiddleware {
)?.let { store.dispatchOpenUrl(it) }
}
private fun openStaking(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID, yield: Yield) {
store.dispatchNavigationAction {
push(
AppRoute.Staking(
userWalletId = userWalletId,
cryptoCurrencyId = cryptoCurrencyId,
yield = yield,
),
)
}
}
private fun handleNewSendToken(action: TradeCryptoAction.SendToken) {
handleNewSend(
userWalletId = action.userWallet.walletId,

View file

@ -0,0 +1,69 @@
package com.tangem.common.ui.tokens
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
fun ScenarioUnavailabilityReason.getUnavailabilityReasonText(): TextReference {
return when (val unavailabilityReason = this) {
is ScenarioUnavailabilityReason.StakingUnavailable -> {
resourceReference(
id = R.string.token_button_unavailability_reason_staking_unavailable,
formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName),
)
}
is ScenarioUnavailabilityReason.PendingTransaction -> {
when (unavailabilityReason.withdrawalScenario) {
ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference(
id = R.string.token_button_unavailability_reason_pending_transaction_send,
formatArgs = wrappedList(unavailabilityReason.networkName),
)
ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference(
id = R.string.token_button_unavailability_reason_pending_transaction_sell,
formatArgs = wrappedList(unavailabilityReason.networkName),
)
}
}
is ScenarioUnavailabilityReason.EmptyBalance -> {
when (unavailabilityReason.withdrawalScenario) {
ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference(
id = R.string.token_button_unavailability_reason_empty_balance_send,
)
ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference(
id = R.string.token_button_unavailability_reason_empty_balance_sell,
)
}
}
is ScenarioUnavailabilityReason.BuyUnavailable -> {
resourceReference(
id = R.string.token_button_unavailability_reason_buy_unavailable,
formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName),
)
}
is ScenarioUnavailabilityReason.NotExchangeable -> {
resourceReference(
id = R.string.token_button_unavailability_reason_not_exchangeable,
formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName),
)
}
is ScenarioUnavailabilityReason.NotSupportedBySellService -> {
resourceReference(
id = R.string.token_button_unavailability_reason_sell_unavailable,
formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName),
)
}
ScenarioUnavailabilityReason.Unreachable -> {
resourceReference(
id = R.string.token_button_unavailability_generic_description,
)
}
ScenarioUnavailabilityReason.UnassociatedAsset -> resourceReference(
id = R.string.warning_receive_blocked_hedera_token_association_required_message,
)
ScenarioUnavailabilityReason.None -> {
throw IllegalArgumentException("The unavailability reason must be other than None")
}
}
}

View file

@ -11,4 +11,9 @@ data class TangemBottomSheetConfig(
val isShow: Boolean,
val onDismissRequest: () -> Unit,
val content: TangemBottomSheetConfigContent,
)
) {
companion object {
val Empty = TangemBottomSheetConfig(false, {}, TangemBottomSheetConfigContent.Empty)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.core.ui.components.token
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -47,12 +48,18 @@ private enum class LayoutId {
* >Figma Component</a>
*/
@Composable
fun TokenItem(state: TokenItemState, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
fun TokenItem(
state: TokenItemState,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
itemPaddingValues: PaddingValues = PaddingValues(horizontal = TangemTheme.dimens.spacing12),
) {
TokenItem(
state = state,
isBalanceHidden = isBalanceHidden,
modifier = modifier,
reorderableTokenListState = null,
itemPaddingValues = itemPaddingValues,
)
}
@ -63,6 +70,7 @@ fun TokenItem(state: TokenItemState, isBalanceHidden: Boolean, modifier: Modifie
* @param isBalanceHidden flag that shows/hides balance
* @param reorderableTokenListState reorderable token list state
* @param modifier modifier
* @param itemPaddingValues padding values
*
* @see <a href = "https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=1051-866&t=ew8mbGp2lacuJfFm-4"
* >Figma Component</a>
@ -73,12 +81,15 @@ fun TokenItem(
isBalanceHidden: Boolean,
reorderableTokenListState: ReorderableLazyListState?,
modifier: Modifier = Modifier,
itemPaddingValues: PaddingValues = PaddingValues(horizontal = TangemTheme.dimens.spacing12),
) {
val betweenRowsMargin = TangemTheme.dimens.spacing2
CustomContainer(
state = state,
modifier = modifier.tokenClickable(state = state),
modifier = modifier
.tokenClickable(state = state)
.padding(itemPaddingValues),
) {
CurrencyIcon(
state = state.iconState,
@ -163,10 +174,7 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier
Layout(content = content, modifier = modifier) { measurables, constraints ->
val layoutWidth = constraints.maxWidth
val horizontalPadding = with(density) { dimens.size12.roundToPx() }
val verticalPadding = with(density) { dimens.size15.roundToPx() }
val layoutWidthWithoutPaddings = layoutWidth - 2 * horizontalPadding
val titleMinWidth = (layoutWidth * TITLE_MIN_WIDTH_COEFFICIENT).toInt()
val priceChangeMinWidth = (layoutWidth * PRICE_MIN_WIDTH_COEFFICIENT).toInt()
@ -202,35 +210,35 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier
-> {
fiatAmount = measurables.measureFiatAmount(
state = state,
maxWidth = layoutWidthWithoutPaddings - icon.width - titleMinWidth,
maxWidth = layoutWidth - icon.width - titleMinWidth,
defaultConstraints = constraints,
)
cryptoAmount = measurables.measureCryptoAmount(
state = state,
maxWidth = layoutWidthWithoutPaddings - icon.width - priceChangeMinWidth,
maxWidth = layoutWidth - icon.width - priceChangeMinWidth,
defaultConstraints = constraints,
)
firstRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - fiatAmount.width
secondRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - cryptoAmount.width
firstRowRemainingFreeSpace = layoutWidth - icon.width - fiatAmount.width
secondRowRemainingFreeSpace = layoutWidth - icon.width - cryptoAmount.width
}
is TokenItemState.Draggable -> {
cryptoAmount = measurables.measureCryptoAmount(
state = state,
maxWidth = layoutWidthWithoutPaddings - icon.width - nonFiatContent.width,
maxWidth = layoutWidth - icon.width - nonFiatContent.width,
defaultConstraints = constraints,
)
firstRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - nonFiatContent.width
firstRowRemainingFreeSpace = layoutWidth - icon.width - nonFiatContent.width
}
is TokenItemState.NoAddress,
is TokenItemState.Unreachable,
-> {
firstRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - nonFiatContent.width
firstRowRemainingFreeSpace = layoutWidth - icon.width - nonFiatContent.width
if (state.subtitleState != null) {
secondRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - nonFiatContent.width
secondRowRemainingFreeSpace = layoutWidth - icon.width - nonFiatContent.width
}
}
}
@ -262,10 +270,10 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier
)
layout(width = constraints.maxWidth, height = layoutHeight) {
icon.placeRelative(x = horizontalPadding, y = (layoutHeight - icon.height).div(other = 2))
icon.placeRelative(x = 0, y = (layoutHeight - icon.height).div(other = 2))
title.placeRelative(
x = horizontalPadding + icon.width,
x = icon.width,
y = when (state) {
is TokenItemState.NoAddress,
is TokenItemState.Unreachable,
@ -280,23 +288,23 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier
},
)
fiatAmount?.placeRelative(x = layoutWidth - fiatAmount.width - horizontalPadding, y = verticalPadding)
fiatAmount?.placeRelative(x = layoutWidth - fiatAmount.width, y = verticalPadding)
priceChange?.placeRelative(
x = horizontalPadding + icon.width,
x = icon.width,
y = layoutHeight - priceChange.height - verticalPadding,
)
cryptoAmount?.placeRelative(
x = when (state) {
is TokenItemState.Draggable -> horizontalPadding + icon.width
else -> layoutWidth - cryptoAmount.width - horizontalPadding
is TokenItemState.Draggable -> icon.width
else -> layoutWidth - cryptoAmount.width
},
y = layoutHeight - cryptoAmount.height - verticalPadding,
)
nonFiatContent.placeRelative(
x = layoutWidth - nonFiatContent.width - horizontalPadding,
x = layoutWidth - nonFiatContent.width,
y = (layoutHeight - nonFiatContent.height).div(other = 2),
)
}

View file

@ -130,11 +130,24 @@ class GetCryptoCurrencyActionsUseCase(
// staking
if (stakingFeatureToggles.isStakingEnabled) {
if (isStakingAvailable(cryptoCurrency)) {
activeList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.None))
val yield = kotlin.runCatching {
stakingRepository.getYield(
cryptoCurrencyId = cryptoCurrency.id,
symbol = cryptoCurrency.symbol,
)
}.getOrNull()
activeList.add(
TokenActionsState.ActionState.Stake(
unavailabilityReason = ScenarioUnavailabilityReason.None,
yield = yield,
),
)
} else {
disabledList.add(
TokenActionsState.ActionState.Stake(
unavailabilityReason = ScenarioUnavailabilityReason.StakingUnavailable(cryptoCurrency.name),
yield = null,
),
)
}
@ -254,7 +267,7 @@ class GetCryptoCurrencyActionsUseCase(
actionsList.add(TokenActionsState.ActionState.Receive(scenario))
}
if (stakingFeatureToggles.isStakingEnabled) {
actionsList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.Unreachable))
actionsList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.Unreachable, null))
}
actionsList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None))

View file

@ -1,10 +1,8 @@
package com.tangem.domain.tokens.legacy
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import org.rekotlin.Action
import java.math.BigDecimal
@ -24,6 +22,7 @@ sealed class TradeCryptoAction : Action {
val appCurrencyCode: String,
) : TradeCryptoAction()
@Deprecated("Use AppRoute instead")
data class SendToken(
val userWallet: UserWallet,
val tokenCurrency: CryptoCurrency.Token,
@ -33,6 +32,7 @@ sealed class TradeCryptoAction : Action {
val transactionInfo: TransactionInfo? = null,
) : TradeCryptoAction()
@Deprecated("Use AppRoute instead")
data class SendCoin(
val userWallet: UserWallet,
val coinStatus: CryptoCurrencyStatus,
@ -40,12 +40,6 @@ sealed class TradeCryptoAction : Action {
val transactionInfo: TransactionInfo? = null,
) : TradeCryptoAction()
data class Stake(
val userWalletId: UserWalletId,
val cryptoCurrencyId: CryptoCurrency.ID,
val yield: Yield,
) : TradeCryptoAction()
data class TransactionInfo(
val transactionId: String,
val destinationAddress: String,

View file

@ -1,5 +1,6 @@
package com.tangem.domain.tokens.model
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.wallets.models.UserWalletId
data class TokenActionsState(
@ -20,7 +21,10 @@ data class TokenActionsState(
data class Receive(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState()
data class Stake(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState()
data class Stake(
override val unavailabilityReason: ScenarioUnavailabilityReason,
val yield: Yield?,
) : ActionState()
data class Swap(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState()

View file

@ -29,6 +29,13 @@ dependencies {
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
// FIXME [REDACTED_TASK_KEY]
// Remove the "Buy" and "Sell" actions from the redux middleware.
// Instead, create some kind of interface for such cases.
/* Redux -_- */
implementation(projects.domain.legacy)
implementation(deps.reKotlin)
/* Compose */
implementation(deps.compose.coil)
implementation(deps.compose.foundation)

View file

@ -346,9 +346,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
chartDataProducer.runTransaction {
updateLook {
it.copy(
type = getChartTypeByPercent(percent),
)
it.copy(type = percent.percentChangeType().toChartType())
}
}
}
@ -356,13 +354,12 @@ internal class MarketsTokenDetailsModel @Inject constructor(
private suspend fun updateQuotes(newQuotes: TokenQuotes) {
quotesStateUpdater.updateQuotes(newQuotes)
val percent = newQuotes.getPercentByInterval(interval = state.value.selectedInterval)
val percent = newQuotes
.getPercentByInterval(interval = state.value.selectedInterval)
chartDataProducer.runTransaction {
updateLook {
it.copy(
type = getChartTypeByPercent(percent),
)
it.copy(type = percent.percentChangeType().toChartType())
}
}
}
@ -430,7 +427,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
chartDataProducer.runTransaction {
updateLook {
it.copy(
type = getChartTypeByPercent(percent),
type = percent.percentChangeType().toChartType(),
)
}
}
@ -477,7 +474,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
launch {
while (true) {
delay(timeMillis)
// Update quotes only when content visible on the screen
// Update quotes only when content is visible on the screen
isVisibleOnScreen.first { it }
loadQuotes()

View file

@ -87,15 +87,4 @@ internal fun PriceChangeType.toChartType(): MarketChartLook.Type {
PriceChangeType.DOWN -> MarketChartLook.Type.Falling
PriceChangeType.NEUTRAL -> MarketChartLook.Type.Neutral
}
}
@Suppress("MagicNumber")
internal fun getChartTypeByPercent(percent: BigDecimal?): MarketChartLook.Type {
val scaled = percent?.setScale(4, RoundingMode.HALF_UP)
return when {
scaled == null -> return MarketChartLook.Type.Neutral
scaled > BigDecimal.ZERO -> MarketChartLook.Type.Growing
scaled < BigDecimal.ZERO -> MarketChartLook.Type.Falling
else -> MarketChartLook.Type.Neutral
}
}

View file

@ -4,6 +4,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
@ -11,7 +12,7 @@ import com.tangem.domain.wallets.models.UserWalletId
/**
* Portfolio data. Combined data from all flows that required to setup portfolio
*
* @property walletsWithCurrencyStatuses wallets with crypto currency statuses
* @property walletsWithCurrencies wallets with crypto currency statuses
* @property appCurrency app currency
* @property isBalanceHidden flag that indicates if balance should be hidden
* @property walletsWithBalance wallets with total balance
@ -19,8 +20,14 @@ import com.tangem.domain.wallets.models.UserWalletId
[REDACTED_AUTHOR]
*/
internal data class PortfolioData(
val walletsWithCurrencyStatuses: Map<UserWallet, List<CryptoCurrencyStatus>>,
val walletsWithCurrencies: Map<UserWallet, List<CryptoCurrencyData>>,
val appCurrency: AppCurrency,
val isBalanceHidden: Boolean,
val walletsWithBalance: Map<UserWalletId, Lce<TokenListError, TotalFiatBalance>>,
)
) {
data class CryptoCurrencyData(
val userWallet: UserWallet,
val status: CryptoCurrencyStatus,
val actions: List<TokenActionsState.ActionState>,
)
}

View file

@ -6,9 +6,9 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.tokens.GetAllWalletsCryptoCurrencyStatusesUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
@ -32,18 +32,19 @@ internal class PortfolioDataLoader @Inject constructor(
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase,
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase,
) {
/** Load data by [currencyRawId] */
@OptIn(ExperimentalCoroutinesApi::class)
fun load(currencyRawId: String): Flow<PortfolioData> {
return combine(
flow = getAllWalletsCryptoCurrencyStatusesFlow(currencyRawId = currencyRawId),
flow = getAllWalletsCryptoCurrenciesData(currencyRawId = currencyRawId),
flow2 = getSelectedAppCurrencyFlow(),
flow3 = getBalanceHidingSettingsFlow(),
) { walletsWithCurrencyStatuses, appCurrency, isBalanceHidden ->
) { walletsWithCurrencies, appCurrency, isBalanceHidden ->
PortfolioData(
walletsWithCurrencyStatuses = walletsWithCurrencyStatuses,
walletsWithCurrencies = walletsWithCurrencies,
appCurrency = appCurrency,
isBalanceHidden = isBalanceHidden,
walletsWithBalance = emptyMap(),
@ -52,15 +53,16 @@ internal class PortfolioDataLoader @Inject constructor(
// setup balances for wallets from walletsWithCurrencyStatuses
.flatMapLatest { portfolioData ->
getWalletsWithTotalBalanceFlow(
ids = portfolioData.walletsWithCurrencyStatuses.keys.map(UserWallet::walletId),
ids = portfolioData.walletsWithCurrencies.keys.map(UserWallet::walletId),
)
.map { portfolioData.copy(walletsWithBalance = it) }
}
}
private fun getAllWalletsCryptoCurrencyStatusesFlow(
@OptIn(ExperimentalCoroutinesApi::class)
private fun getAllWalletsCryptoCurrenciesData(
currencyRawId: String,
): Flow<Map<UserWallet, List<CryptoCurrencyStatus>>> {
): Flow<Map<UserWallet, List<PortfolioData.CryptoCurrencyData>>> {
return getAllWalletsCryptoCurrencyStatusesUseCase(currencyRawId)
.distinctUntilChanged()
.map { walletsWithMaybeStatuses ->
@ -68,6 +70,30 @@ internal class PortfolioDataLoader @Inject constructor(
entry.value.mapNotNull { it.getOrNull() }
}
}
.flatMapLatest { walletsWithStatuses ->
val actionsFlows = walletsWithStatuses.flatMap { (wallet, statuses) ->
statuses.map { status ->
getCryptoCurrencyActionsUseCase(wallet, status)
.map {
PortfolioData.CryptoCurrencyData(
userWallet = wallet,
status = status,
actions = it.states,
)
}
}
}
combine(actionsFlows) { actions ->
walletsWithStatuses.mapValues { entry ->
entry.value.mapNotNull { status ->
actions.firstOrNull {
it.userWallet == entry.key && it.status == status
}
}
}
}
}
}
private fun getSelectedAppCurrencyFlow(): Flow<AppCurrency> {

View file

@ -66,6 +66,8 @@ internal class AddToPortfolioBSContentUMFactory(
portfolioData = portfolioData,
selectedWalletId = selectedWallet.walletId,
),
// TODO maybe do not show wallets with already added token
isWalletBlockVisible = portfolioData.walletsWithBalance.size > 1,
),
)
}
@ -86,7 +88,7 @@ internal class AddToPortfolioBSContentUMFactory(
isShow = isShow,
onDismissRequest = { onWalletSelectorVisibilityChange(false) },
content = WalletSelectorBSContentUM(
userWallets = portfolioData.walletsWithCurrencyStatuses
userWallets = portfolioData.walletsWithCurrencies
.filterKeys(UserWallet::isMultiCurrency)
.map { it.key }
.map { userWallet ->

View file

@ -6,6 +6,8 @@ import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.card.HasMissedDerivationsUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
@ -14,20 +16,24 @@ import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent
import com.tangem.features.markets.portfolio.impl.loader.PortfolioDataLoader
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.*
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@Stable
@ComponentScoped
internal class MarketsPortfolioModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val portfolioDataLoader: PortfolioDataLoader,
private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase,
override val dispatchers: CoroutineDispatcherProvider,
private val tokenActionsIntentsFactory: TokenActionsHandler.Factory,
) : Model() {
val state: StateFlow<MyPortfolioUM> get() = _state
@ -45,7 +51,17 @@ internal class MarketsPortfolioModel @Inject constructor(
private val portfolioBSVisibilityModelFlow = MutableStateFlow(value = PortfolioBSVisibilityModel())
private val factory = MyPortfolioUMMFactory(
private val currentAppCurrency = getSelectedAppCurrencyUseCase()
.map { maybeAppCurrency ->
maybeAppCurrency.getOrElse { AppCurrency.Default }
}
.stateIn(
scope = modelScope,
started = SharingStarted.Eagerly,
initialValue = AppCurrency.Default,
)
private val factory = MyPortfolioUMFactory(
onAddClick = { onAddToPortfolioBSVisibilityChange(isShow = true) },
onTokenItemClick = ::onTokenItemClick,
addToPortfolioBSContentUMFactory = AddToPortfolioBSContentUMFactory(
@ -56,6 +72,15 @@ internal class MarketsPortfolioModel @Inject constructor(
onWalletSelect = ::onWalletSelect,
onContinueClick = ::onContinueClick,
),
tokenActionsHandler = tokenActionsIntentsFactory.create(
currentAppCurrency = Provider { currentAppCurrency.value },
updateTokenActionsBSConfig = { updateBlock ->
updateTokensState { it.copy(tokenActionsBSConfig = updateBlock(it.tokenActionsBSConfig)) }
},
updateTokenReceiveBSConfig = { updateBlock ->
updateTokensState { it.copy(tokenReceiveBSConfig = updateBlock(it.tokenReceiveBSConfig)) }
},
),
)
init {
@ -128,9 +153,7 @@ internal class MarketsPortfolioModel @Inject constructor(
}
private fun onTokenItemClick(index: Int, id: CryptoCurrency.ID) {
_state.update {
val state = _state.value as? MyPortfolioUM.Tokens ?: return@update it
updateTokensState { state ->
state.copy(
tokens = state.tokens
.mapIndexed { i, tokenUM ->
@ -138,7 +161,7 @@ internal class MarketsPortfolioModel @Inject constructor(
if (index == i && id.value == tokenUM.tokenItemState.id) {
tokenUM.copy(isQuickActionsShown = !tokenUM.isQuickActionsShown)
} else {
tokenUM
tokenUM.copy(isQuickActionsShown = false)
}
}
.toImmutableList(),
@ -194,4 +217,11 @@ internal class MarketsPortfolioModel @Inject constructor(
it.copy(addToPortfolioBSVisibility = true, walletSelectorBSVisibility = isShow)
}
}
private fun updateTokensState(block: (MyPortfolioUM.Tokens) -> MyPortfolioUM) {
_state.update { stateToUpdate ->
val tokensState = stateToUpdate as? MyPortfolioUM.Tokens ?: return@update stateToUpdate
block(tokensState)
}
}
}

View file

@ -16,10 +16,11 @@ import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
*
[REDACTED_AUTHOR]
*/
internal class MyPortfolioUMMFactory(
internal class MyPortfolioUMFactory(
private val onAddClick: () -> Unit,
private val onTokenItemClick: (Int, CryptoCurrency.ID) -> Unit,
private val addToPortfolioBSContentUMFactory: AddToPortfolioBSContentUMFactory,
private val tokenActionsHandler: TokenActionsHandler,
) {
fun create(
@ -31,16 +32,16 @@ internal class MyPortfolioUMMFactory(
if (availableNetworks.isEmpty()) return MyPortfolioUM.Unavailable
val walletsWithStatuses = portfolioData.walletsWithCurrencyStatuses
val walletsWithCurrencies = portfolioData.walletsWithCurrencies
.filterAvailableNetworks(networks = availableNetworks)
val isPortfolioEmpty = walletsWithStatuses.flatMap { it.value }.isEmpty()
val isPortfolioEmpty = walletsWithCurrencies.flatMap { it.value }.isEmpty()
if (isPortfolioEmpty) {
val hasMultiWallets = walletsWithStatuses.filterKeys(UserWallet::isMultiCurrency).isNotEmpty()
val hasMultiWallets = walletsWithCurrencies.filterKeys(UserWallet::isMultiCurrency).isNotEmpty()
return if (hasMultiWallets) {
MyPortfolioUM.AddFirstToken(
bsConfig = createAddToPortfolioBSConfig(
addToPortfolioBSConfig = createAddToPortfolioBSConfig(
portfolioData = portfolioData,
portfolioUIData = portfolioUIData,
availableNetworks = availableNetworks,
@ -55,7 +56,7 @@ internal class MyPortfolioUMMFactory(
return TokensPortfolioUMConverter(
appCurrency = portfolioData.appCurrency,
isBalanceHidden = portfolioData.isBalanceHidden,
isAllAvailableNetworksAdded = walletsWithStatuses.isAllAvailableNetworksAdded(availableNetworks),
isAllAvailableNetworksAdded = walletsWithCurrencies.isAllAvailableNetworksAdded(availableNetworks),
bsConfig = createAddToPortfolioBSConfig(
portfolioData = portfolioData,
portfolioUIData = portfolioUIData,
@ -63,8 +64,9 @@ internal class MyPortfolioUMMFactory(
),
onAddClick = onAddClick,
onTokenItemClick = onTokenItemClick,
quickActionsIntents = tokenActionsHandler,
)
.convert(walletsWithStatuses)
.convert(walletsWithCurrencies)
}
private fun createAddToPortfolioBSConfig(
@ -73,21 +75,21 @@ internal class MyPortfolioUMMFactory(
availableNetworks: List<TokenMarketInfo.Network>,
): TangemBottomSheetConfig {
val walletId = portfolioUIData.selectedWalletId
?: portfolioData.walletsWithCurrencyStatuses.keys.firstOrNull { it.isMultiCurrency }?.walletId
?: portfolioData.walletsWithCurrencies.keys.firstOrNull { it.isMultiCurrency }?.walletId
val selectedWallet = portfolioData.walletsWithCurrencyStatuses.keys
val selectedWallet = portfolioData.walletsWithCurrencies.keys
.firstOrNull { it.walletId == walletId }
?: error("portfolioModel.walletsWithCurrencyStatuses doesn't contain selected wallet: $walletId")
val changedNetworks = portfolioUIData.walletsWithChangedNetworks[portfolioUIData.selectedWalletId]
val alreadyAddedNetworks = requireNotNull(
value = portfolioData.walletsWithCurrencyStatuses[selectedWallet],
value = portfolioData.walletsWithCurrencies[selectedWallet],
lazyMessage = {
"portfolioModel.walletsWithCurrencyStatuses doesn't contain selected wallet: $walletId"
},
)
.filterAvailableNetworks(availableNetworks)
.map { it.currency.network.backendId }
.map { it.status.currency.network.backendId }
return addToPortfolioBSContentUMFactory.create(
portfolioData = portfolioData,
@ -117,7 +119,7 @@ internal class MyPortfolioUMMFactory(
}
}
private fun Map<UserWallet, List<CryptoCurrencyStatus>>.isAllAvailableNetworksAdded(
private fun Map<UserWallet, List<PortfolioData.CryptoCurrencyData>>.isAllAvailableNetworksAdded(
availableNetworks: List<TokenMarketInfo.Network>,
): Boolean {
val networkIds = availableNetworks.map { it.networkId }
@ -125,26 +127,26 @@ internal class MyPortfolioUMMFactory(
return this
// User can add currencies only in multi-currency wallets
.filterKeys(UserWallet::isMultiCurrency)
.mapValues { it.value.map { it.currency.network.backendId } }
.mapValues { entry -> entry.value.map { it.status.currency.network.backendId } }
// Each wallets contains all available networks?
.all { it.value.containsAll(networkIds) }
}
/** Filter map values by available networks [networks] */
private fun Map<UserWallet, List<CryptoCurrencyStatus>>.filterAvailableNetworks(
private fun Map<UserWallet, List<PortfolioData.CryptoCurrencyData>>.filterAvailableNetworks(
networks: List<TokenMarketInfo.Network>,
): Map<UserWallet, List<CryptoCurrencyStatus>> {
): Map<UserWallet, List<PortfolioData.CryptoCurrencyData>> {
return mapValues { entry -> entry.value.filterAvailableNetworks(networks) }
}
/** Filter list of [CryptoCurrencyStatus] by available networks [networks] */
private fun List<CryptoCurrencyStatus>.filterAvailableNetworks(
private fun List<PortfolioData.CryptoCurrencyData>.filterAvailableNetworks(
networks: List<TokenMarketInfo.Network>,
): List<CryptoCurrencyStatus> {
): List<PortfolioData.CryptoCurrencyData> {
val networkIds = networks.map(TokenMarketInfo.Network::networkId)
return mapNotNull {
it.takeIf { networkIds.contains(it.currency.network.backendId) }
it.takeIf { networkIds.contains(it.status.currency.network.backendId) }
}
}
}

View file

@ -4,9 +4,15 @@ import com.tangem.common.ui.tokens.TokenItemStateConverter
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.markets.portfolio.impl.loader.PortfolioData
import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM
import com.tangem.features.markets.portfolio.impl.ui.state.QuickActionUM
import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toImmutableList
/**
* Converter from [UserWallet] and [CryptoCurrencyStatus] to [PortfolioTokenUM]
@ -17,27 +23,75 @@ internal class PortfolioTokenUMConverter(
private val appCurrency: AppCurrency,
private val isBalanceHidden: Boolean,
private val onTokenItemClick: (CryptoCurrencyStatus) -> Unit,
) : Converter<Pair<UserWallet, CryptoCurrencyStatus>, PortfolioTokenUM> {
override fun convert(value: Pair<UserWallet, CryptoCurrencyStatus>): PortfolioTokenUM {
val (userWallet, cryptoCurrencyStatus) = value
private val tokenActionsHandler: TokenActionsHandler,
) : Converter<PortfolioData.CryptoCurrencyData, PortfolioTokenUM> {
override fun convert(value: PortfolioData.CryptoCurrencyData): PortfolioTokenUM {
val tokenItemStateConverter = TokenItemStateConverter(
appCurrency = appCurrency,
titleStateProvider = { TokenItemState.TitleState.Content(text = userWallet.name) },
titleStateProvider = { TokenItemState.TitleState.Content(text = value.userWallet.name) },
subtitleStateProvider = {
TokenItemState.SubtitleState.TextContent(value = cryptoCurrencyStatus.currency.name)
TokenItemState.SubtitleState.TextContent(value = value.status.currency.name)
},
onItemClick = onTokenItemClick,
onItemLongClick = null,
onItemLongClick = {
tokenActionsHandler.onTokenLongClick(cryptoCurrencyData = value)
},
)
return PortfolioTokenUM(
tokenItemState = tokenItemStateConverter.convert(value = cryptoCurrencyStatus),
tokenItemState = tokenItemStateConverter.convert(value = value.status),
isBalanceHidden = isBalanceHidden,
isQuickActionsShown = false,
quickActions = quickActions(cryptoData = value),
)
}
private fun quickActions(cryptoData: PortfolioData.CryptoCurrencyData): PortfolioTokenUM.QuickActions {
return PortfolioTokenUM.QuickActions(
actions = listOfNotNull(
QuickActionUM.Buy.takeIf {
cryptoData.actions.any {
it is TokenActionsState.ActionState.Buy &&
it.unavailabilityReason == ScenarioUnavailabilityReason.None
}
},
QuickActionUM.Exchange.takeIf {
cryptoData.actions.any {
it is TokenActionsState.ActionState.Swap &&
it.unavailabilityReason == ScenarioUnavailabilityReason.None
}
},
QuickActionUM.Receive.takeIf {
cryptoData.actions.any {
it is TokenActionsState.ActionState.Receive &&
it.unavailabilityReason == ScenarioUnavailabilityReason.None
}
},
).toImmutableList(),
onQuickActionClick = {
// TODO: [REDACTED_JIRA]
when (it) {
QuickActionUM.Buy -> tokenActionsHandler.handle(
action = TokenActionsBSContentUM.Action.Buy,
cryptoCurrencyData = cryptoData,
)
QuickActionUM.Exchange -> tokenActionsHandler.handle(
action = TokenActionsBSContentUM.Action.Exchange,
cryptoCurrencyData = cryptoData,
)
QuickActionUM.Receive -> tokenActionsHandler.handle(
action = TokenActionsBSContentUM.Action.Receive,
cryptoCurrencyData = cryptoData,
)
}
},
onQuickActionLongClick = {
if (it == QuickActionUM.Receive) {
tokenActionsHandler.handle(
action = TokenActionsBSContentUM.Action.CopyAddress,
cryptoCurrencyData = cryptoData,
)
}
},
)
}

View file

@ -0,0 +1,199 @@
package com.tangem.features.markets.portfolio.impl.model
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.portfolio.impl.loader.PortfolioData
import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM
import com.tangem.utils.Provider
import com.tangem.utils.isNullOrZero
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.collections.immutable.toImmutableList
@Suppress("LongParameterList")
@ComponentScoped
internal class TokenActionsHandler @AssistedInject constructor(
private val router: Router,
private val clipboardManager: ClipboardManager,
private val uiMessageSender: UiMessageSender,
private val reduxStateHolder: ReduxStateHolder,
@Assisted
private val currentAppCurrency: Provider<AppCurrency>,
@Assisted("updateTokenReceiveBSConfig")
private val updateTokenReceiveBSConfig: ((TangemBottomSheetConfig) -> TangemBottomSheetConfig) -> Unit,
@Assisted("updateTokenActionsBSConfig")
private val updateTokenActionsBSConfig: ((TangemBottomSheetConfig) -> TangemBottomSheetConfig) -> Unit,
) {
fun handle(action: TokenActionsBSContentUM.Action, cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
when (action) {
TokenActionsBSContentUM.Action.Buy -> onBuyClick(cryptoCurrencyData)
TokenActionsBSContentUM.Action.Exchange -> onExchangeClick(cryptoCurrencyData)
TokenActionsBSContentUM.Action.Receive -> onReceiveClick(cryptoCurrencyData)
TokenActionsBSContentUM.Action.CopyAddress -> onCopyAddress(cryptoCurrencyData)
TokenActionsBSContentUM.Action.Sell -> onSellClick(cryptoCurrencyData)
TokenActionsBSContentUM.Action.Send -> onSendClick(cryptoCurrencyData)
TokenActionsBSContentUM.Action.Stake -> onStakeClick(cryptoCurrencyData)
}
}
fun onTokenLongClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
updateTokenActionsBSConfig {
TangemBottomSheetConfig(
isShow = true,
onDismissRequest = {
updateTokenActionsBSConfig {
it.copy(isShow = false)
}
},
content = TokenActionsBSContentUM(
title = cryptoCurrencyData.userWallet.name,
actions = cryptoCurrencyData.actions
.mapNotNull { it.toAction(cryptoCurrencyData) }
.sortedBy { it.order }
.toImmutableList(),
onActionClick = { action ->
handle(action, cryptoCurrencyData)
updateTokenActionsBSConfig {
it.copy(isShow = false)
}
},
),
)
}
}
private fun onReceiveClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
val cryptoCurrencyStatus = cryptoCurrencyData.status
val currency = cryptoCurrencyStatus.currency
val networkAddress = cryptoCurrencyStatus.value.networkAddress ?: return
updateTokenReceiveBSConfig {
TangemBottomSheetConfig(
isShow = true,
onDismissRequest = {
updateTokenReceiveBSConfig {
it.copy(isShow = false)
}
},
content = TokenReceiveBottomSheetConfig(
name = currency.name,
symbol = currency.symbol,
network = currency.network.name,
addresses = networkAddress.availableAddresses
.mapToAddressModels(currency)
.toImmutableList(),
onCopyClick = {},
onShareClick = {},
),
)
}
}
private fun onCopyAddress(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
val cryptoCurrencyStatus = cryptoCurrencyData.status
val networkAddress = cryptoCurrencyStatus.value.networkAddress ?: return
val addresses = networkAddress.availableAddresses
.mapToAddressModels(cryptoCurrencyStatus.currency)
.toImmutableList()
val defaultAddress = addresses.firstOrNull()?.value ?: return
clipboardManager.setText(text = defaultAddress)
uiMessageSender.send(SnackbarMessage(resourceReference(R.string.wallet_notification_address_copied)))
}
private fun onBuyClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
reduxStateHolder.dispatch(
TradeCryptoAction.Buy(
userWallet = cryptoCurrencyData.userWallet,
cryptoCurrencyStatus = cryptoCurrencyData.status,
appCurrencyCode = currentAppCurrency().code,
),
)
}
private fun onSellClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
reduxStateHolder.dispatch(
TradeCryptoAction.Sell(
cryptoCurrencyStatus = cryptoCurrencyData.status,
appCurrencyCode = currentAppCurrency().code,
),
)
}
private fun onExchangeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
router.push(
AppRoute.Swap(
currency = cryptoCurrencyData.status.currency,
userWalletId = cryptoCurrencyData.userWallet.walletId,
),
)
}
private fun onSendClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
router.push(
AppRoute.Send(
userWalletId = cryptoCurrencyData.userWallet.walletId,
currency = cryptoCurrencyData.status.currency,
),
)
}
private fun onStakeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
val yield = cryptoCurrencyData.actions.firstOrNull { it is TokenActionsState.ActionState.Stake }
?.let { it as TokenActionsState.ActionState.Stake }
?.yield ?: return
router.push(
AppRoute.Staking(
userWalletId = cryptoCurrencyData.userWallet.walletId,
cryptoCurrencyId = cryptoCurrencyData.status.currency.id,
yield = yield,
),
)
}
private fun TokenActionsState.ActionState.toAction(
cryptoCurrencyData: PortfolioData.CryptoCurrencyData,
): TokenActionsBSContentUM.Action? = when (this) {
is TokenActionsState.ActionState.Buy -> TokenActionsBSContentUM.Action.Buy
is TokenActionsState.ActionState.CopyAddress -> TokenActionsBSContentUM.Action.CopyAddress
is TokenActionsState.ActionState.HideToken -> null
is TokenActionsState.ActionState.Receive -> TokenActionsBSContentUM.Action.Receive
is TokenActionsState.ActionState.Sell -> TokenActionsBSContentUM.Action.Sell
is TokenActionsState.ActionState.Send -> {
TokenActionsBSContentUM.Action.Send.takeIf {
cryptoCurrencyData.status.value.amount.isNullOrZero().not()
}
}
is TokenActionsState.ActionState.Stake -> TokenActionsBSContentUM.Action.Stake
is TokenActionsState.ActionState.Swap -> TokenActionsBSContentUM.Action.Exchange
}
.takeIf { this.unavailabilityReason == ScenarioUnavailabilityReason.None }
@AssistedFactory
interface Factory {
fun create(
currentAppCurrency: Provider<AppCurrency>,
@Assisted("updateTokenReceiveBSConfig")
updateTokenReceiveBSConfig: ((TangemBottomSheetConfig) -> TangemBottomSheetConfig) -> Unit,
@Assisted("updateTokenActionsBSConfig")
updateTokenActionsBSConfig: ((TangemBottomSheetConfig) -> TangemBottomSheetConfig) -> Unit,
): TokenActionsHandler
}
}

View file

@ -5,6 +5,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.markets.portfolio.impl.loader.PortfolioData
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState
import com.tangem.utils.converter.Converter
@ -15,6 +16,7 @@ import kotlinx.collections.immutable.toImmutableList
*
[REDACTED_AUTHOR]
*/
@Suppress("LongParameterList")
internal class TokensPortfolioUMConverter(
private val appCurrency: AppCurrency,
private val isBalanceHidden: Boolean,
@ -22,25 +24,27 @@ internal class TokensPortfolioUMConverter(
private val bsConfig: TangemBottomSheetConfig,
private val onAddClick: () -> Unit,
private val onTokenItemClick: (index: Int, id: CryptoCurrency.ID) -> Unit,
) : Converter<Map<UserWallet, List<CryptoCurrencyStatus>>, MyPortfolioUM.Tokens> {
private val quickActionsIntents: TokenActionsHandler,
) : Converter<Map<UserWallet, List<PortfolioData.CryptoCurrencyData>>, MyPortfolioUM.Tokens> {
override fun convert(value: Map<UserWallet, List<CryptoCurrencyStatus>>): MyPortfolioUM.Tokens {
override fun convert(value: Map<UserWallet, List<PortfolioData.CryptoCurrencyData>>): MyPortfolioUM.Tokens {
return MyPortfolioUM.Tokens(
tokens = value
.flatMap { entry ->
entry.value.map { entry.key to it }
}
.mapIndexed { index, status ->
.flatMap { entry -> entry.value }
.mapIndexed { index, cryptoData ->
PortfolioTokenUMConverter(
appCurrency = appCurrency,
isBalanceHidden = isBalanceHidden,
onTokenItemClick = { onTokenItemClick(index, it.currency.id) },
).convert(status)
tokenActionsHandler = quickActionsIntents,
).convert(cryptoData)
}
.toImmutableList(),
buttonState = if (isAllAvailableNetworksAdded) AddButtonState.Unavailable else AddButtonState.Available,
bsConfig = bsConfig,
addToPortfolioBSConfig = bsConfig,
onAddClick = onAddClick,
tokenReceiveBSConfig = TangemBottomSheetConfig.Empty,
tokenActionsBSConfig = TangemBottomSheetConfig.Empty,
)
}
}

View file

@ -73,15 +73,16 @@ private fun Content(state: AddToPortfolioBSContentUM, modifier: Modifier = Modif
.verticalScroll(state = scrollState)
.padding(horizontal = TangemTheme.dimens.spacing16),
) {
UserWalletItem(
state = state.selectedWallet,
blockColors = TangemBlockCardColors.copy(
containerColor = TangemTheme.colors.background.action,
disabledContainerColor = TangemTheme.colors.background.action,
),
)
SpacerH12()
if (state.isWalletBlockVisible) {
UserWalletItem(
state = state.selectedWallet,
blockColors = TangemBlockCardColors.copy(
containerColor = TangemTheme.colors.background.action,
disabledContainerColor = TangemTheme.colors.background.action,
),
)
SpacerH12()
}
NetworkSelection(
modifier = Modifier.fillMaxWidth(),

View file

@ -16,6 +16,7 @@ import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SmallButtonShimmer
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.block.information.InformationBlock
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet
import com.tangem.core.ui.components.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
@ -24,8 +25,8 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
@Composable
internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) {
@ -54,8 +55,8 @@ internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) {
}
when (state) {
is MyPortfolioUM.AddFirstToken -> AddToPortfolioBottomSheet(config = state.bsConfig)
is MyPortfolioUM.Tokens -> AddToPortfolioBottomSheet(config = state.bsConfig)
is MyPortfolioUM.AddFirstToken -> AddToPortfolioBottomSheet(config = state.addToPortfolioBSConfig)
is MyPortfolioUM.Tokens -> AddToPortfolioBottomSheet(config = state.addToPortfolioBSConfig)
else -> Unit
}
}
@ -105,6 +106,9 @@ private fun TokenList(state: MyPortfolioUM.Tokens, modifier: Modifier = Modifier
}
}
}
TokenReceiveBottomSheet(config = state.tokenReceiveBSConfig)
TokenActionsBottomSheet(config = state.tokenActionsBSConfig)
}
@Composable

View file

@ -3,6 +3,7 @@ package com.tangem.features.markets.portfolio.impl.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
@ -25,11 +26,15 @@ internal fun PortfolioItem(state: PortfolioTokenUM, lastInList: Boolean, modifie
state = state.tokenItemState,
isBalanceHidden = state.isBalanceHidden,
modifier = Modifier.background(color = TangemTheme.colors.background.action),
itemPaddingValues = PaddingValues(
start = TangemTheme.dimens.spacing10,
end = TangemTheme.dimens.spacing12,
),
)
PortfolioQuickActions(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing12)
.background(color = TangemTheme.colors.background.action)
.padding(
bottom = if (lastInList) {
TangemTheme.dimens.spacing12
@ -37,8 +42,10 @@ internal fun PortfolioItem(state: PortfolioTokenUM, lastInList: Boolean, modifie
TangemTheme.dimens.spacing24
},
),
actions = state.quickActions.actions,
isVisible = state.isQuickActionsShown,
onActionClick = state.onQuickActionClick,
onActionClick = state.quickActions.onQuickActionClick,
onActionLongClick = state.quickActions.onQuickActionLongClick,
)
}
}

View file

@ -4,9 +4,7 @@ import android.content.res.Configuration
import androidx.compose.animation.*
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Button
@ -15,7 +13,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalLayoutDirection
@ -23,6 +20,7 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.components.SpacerH4
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.haptic.TangemHapticEffect
@ -30,34 +28,33 @@ import com.tangem.core.ui.res.LocalHapticManager
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.markets.portfolio.impl.ui.state.QuickActionUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@Composable
internal fun PortfolioQuickActions(
actions: ImmutableList<QuickActionUM>,
isVisible: Boolean,
onActionClick: (QuickActionUM) -> Unit,
onActionLongClick: (QuickActionUM) -> Unit,
modifier: Modifier = Modifier,
) {
if (actions.isEmpty()) return
AnimatedVisibility(
visible = isVisible,
enter = expandVertically(expandFrom = Alignment.Top),
exit = shrinkVertically(shrinkTowards = Alignment.Top),
) {
Column(modifier = modifier) {
LineSeparator()
QuickActionItem(
state = QuickActionUM.Buy,
onClick = { onActionClick(QuickActionUM.Buy) },
)
LineSeparator()
QuickActionItem(
state = QuickActionUM.Exchange,
onClick = { onActionClick(QuickActionUM.Exchange) },
)
LineSeparator()
QuickActionItem(
state = QuickActionUM.Receive,
onClick = { onActionClick(QuickActionUM.Receive) },
)
actions.fastForEach { action ->
LineSeparator()
QuickActionItem(
state = action,
onClick = { onActionClick(action) },
onLongClick = { onActionLongClick(action) }.takeIf { action.longClickAvailable },
)
}
}
}
}
@ -68,10 +65,9 @@ private fun AnimatedVisibilityScope.LineSeparator(modifier: Modifier = Modifier)
val lineColor = TangemTheme.colors.stroke.primary
val strokeWidth = TangemTheme.dimens.size1
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
val verticalPadding = TangemTheme.dimens.spacing2
val startPadding = TangemTheme.dimens.spacing18
val startPadding = TangemTheme.dimens.spacing30
val height = TangemTheme.dimens.size16 + verticalPadding * 2
val height = TangemTheme.dimens.size16
Canvas(
modifier = modifier
@ -96,31 +92,42 @@ private fun AnimatedVisibilityScope.LineSeparator(modifier: Modifier = Modifier)
drawLine(
color = lineColor,
start = Offset(x, verticalPadding.toPx()),
end = Offset(x, size.height - verticalPadding.toPx()),
start = Offset(x, 0f),
end = Offset(x, size.height),
strokeWidth = strokeWidth.toPx(),
)
}
}
@OptIn(ExperimentalAnimationApi::class)
@OptIn(ExperimentalAnimationApi::class, ExperimentalFoundationApi::class)
@Composable
private fun AnimatedVisibilityScope.QuickActionItem(
state: QuickActionUM,
onClick: () -> Unit,
onLongClick: (() -> Unit)?,
modifier: Modifier = Modifier,
) {
val hapticManager = LocalHapticManager.current
val onLongClickInternal: (() -> Unit)? = if (onLongClick != null) {
{
hapticManager.perform(TangemHapticEffect.View.LongPress)
onLongClick()
}
} else {
null
}
Row(
modifier = modifier
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersMedium)
.clickable {
hapticManager.perform(TangemHapticEffect.View.SegmentTick)
onClick()
}
.padding(horizontal = TangemTheme.dimens.spacing2, vertical = TangemTheme.dimens.spacing3),
.combinedClickable(
onLongClick = onLongClickInternal,
onClick = {
hapticManager.perform(TangemHapticEffect.View.SegmentTick)
onClick()
},
)
.padding(horizontal = TangemTheme.dimens.spacing14, vertical = TangemTheme.dimens.spacing4),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing18),
) {
@ -190,8 +197,10 @@ private fun Preview() {
modifier = Modifier.background(color = TangemTheme.colors.background.action),
) {
PortfolioQuickActions(
actions = QuickActionUM.entries.toImmutableList(),
isVisible = isVisible,
onActionClick = {},
onActionLongClick = {},
)
}
}
@ -205,8 +214,10 @@ private fun PreviewRtl() {
TangemThemePreview(rtl = true) {
Box(modifier = Modifier.background(color = TangemTheme.colors.background.action)) {
PortfolioQuickActions(
actions = QuickActionUM.entries.toImmutableList(),
isVisible = true,
onActionClick = {},
onActionLongClick = {},
)
}
}

View file

@ -2,7 +2,6 @@ package com.tangem.features.markets.portfolio.impl.ui
import android.content.res.Configuration
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.padding
@ -10,12 +9,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.SimpleSettingsRow
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle
import com.tangem.core.ui.components.inputrow.InputRowChecked
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.components.rows.CornersToRound
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM
@ -50,16 +49,15 @@ private fun Content(content: TokenActionsBSContentUM) {
else -> CornersToRound.ZERO
}
DividerContainer(
Box(
modifier = Modifier
.clip(cornersToRound.getShape())
.background(TangemTheme.colors.background.action)
.clickable { content.onActionClick(action) },
showDivider = index != content.actions.lastIndex,
.background(TangemTheme.colors.background.action),
) {
InputRowChecked(
text = action.text,
checked = false,
SimpleSettingsRow(
title = action.text.resolveReference(),
icon = action.iconRes,
onItemsClick = { content.onActionClick(action) },
)
}
}

View file

@ -3,7 +3,6 @@ package com.tangem.features.markets.portfolio.impl.ui.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.wallets.models.UserWalletId
@ -54,13 +53,10 @@ internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider
iconUrl = null,
),
isScanCardNotificationVisible = true,
isWalletBlockVisible = true,
continueButtonEnabled = true,
onContinueButtonClick = {},
walletSelectorConfig = TangemBottomSheetConfig(
isShow = false,
onDismissRequest = {},
content = TangemBottomSheetConfigContent.Empty,
),
walletSelectorConfig = TangemBottomSheetConfig.Empty,
),
AddToPortfolioBSContentUM(
selectedWallet = userWallet,
@ -81,13 +77,10 @@ internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider
iconUrl = null,
),
isScanCardNotificationVisible = true,
isWalletBlockVisible = false,
continueButtonEnabled = false,
onContinueButtonClick = {},
walletSelectorConfig = TangemBottomSheetConfig(
isShow = false,
onDismissRequest = {},
content = TangemBottomSheetConfigContent.Empty,
),
walletSelectorConfig = TangemBottomSheetConfig.Empty,
),
)
}

View file

@ -2,12 +2,13 @@ package com.tangem.features.markets.portfolio.impl.ui.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM
import com.tangem.features.markets.portfolio.impl.ui.state.QuickActionUM
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider<MyPortfolioUM> {
@ -16,39 +17,29 @@ internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider<MyPortfol
MyPortfolioUM.Tokens(
tokens = persistentListOf(sampleToken, sampleToken),
buttonState = MyPortfolioUM.Tokens.AddButtonState.Available,
bsConfig = TangemBottomSheetConfig(
isShow = false,
onDismissRequest = {},
content = TangemBottomSheetConfigContent.Empty,
),
addToPortfolioBSConfig = TangemBottomSheetConfig.Empty,
tokenReceiveBSConfig = TangemBottomSheetConfig.Empty,
tokenActionsBSConfig = TangemBottomSheetConfig.Empty,
onAddClick = {},
),
MyPortfolioUM.Tokens(
tokens = persistentListOf(sampleToken, sampleToken.copy(isQuickActionsShown = true)),
buttonState = MyPortfolioUM.Tokens.AddButtonState.Unavailable,
bsConfig = TangemBottomSheetConfig(
isShow = false,
onDismissRequest = {},
content = TangemBottomSheetConfigContent.Empty,
),
addToPortfolioBSConfig = TangemBottomSheetConfig.Empty,
tokenReceiveBSConfig = TangemBottomSheetConfig.Empty,
tokenActionsBSConfig = TangemBottomSheetConfig.Empty,
onAddClick = {},
),
MyPortfolioUM.Tokens(
tokens = persistentListOf(sampleToken.copy(isQuickActionsShown = true), sampleToken),
buttonState = MyPortfolioUM.Tokens.AddButtonState.Loading,
bsConfig = TangemBottomSheetConfig(
isShow = false,
onDismissRequest = {},
content = TangemBottomSheetConfigContent.Empty,
),
addToPortfolioBSConfig = TangemBottomSheetConfig.Empty,
tokenReceiveBSConfig = TangemBottomSheetConfig.Empty,
tokenActionsBSConfig = TangemBottomSheetConfig.Empty,
onAddClick = {},
),
MyPortfolioUM.AddFirstToken(
bsConfig = TangemBottomSheetConfig(
isShow = false,
onDismissRequest = {},
content = TangemBottomSheetConfigContent.Empty,
),
addToPortfolioBSConfig = TangemBottomSheetConfig.Empty,
onAddClick = {},
),
MyPortfolioUM.Loading,
@ -67,7 +58,11 @@ internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider<MyPortfol
onItemLongClick = {},
),
isQuickActionsShown = false,
onQuickActionClick = {},
quickActions = PortfolioTokenUM.QuickActions(
actions = QuickActionUM.entries.toImmutableList(),
onQuickActionClick = {},
onQuickActionLongClick = {},
),
isBalanceHidden = false,
)
}

View file

@ -7,6 +7,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
internal data class AddToPortfolioBSContentUM(
val selectedWallet: UserWalletItemUM,
val selectNetworkUM: SelectNetworkUM,
val isWalletBlockVisible: Boolean,
val isScanCardNotificationVisible: Boolean,
val continueButtonEnabled: Boolean,
val onContinueButtonClick: () -> Unit,

View file

@ -10,7 +10,9 @@ internal sealed class MyPortfolioUM {
data class Tokens(
val tokens: ImmutableList<PortfolioTokenUM>,
val buttonState: AddButtonState,
val bsConfig: TangemBottomSheetConfig,
val addToPortfolioBSConfig: TangemBottomSheetConfig,
val tokenReceiveBSConfig: TangemBottomSheetConfig,
val tokenActionsBSConfig: TangemBottomSheetConfig,
val onAddClick: () -> Unit,
) : MyPortfolioUM() {
@ -22,7 +24,7 @@ internal sealed class MyPortfolioUM {
}
data class AddFirstToken(
val bsConfig: TangemBottomSheetConfig,
val addToPortfolioBSConfig: TangemBottomSheetConfig,
val onAddClick: () -> Unit,
) : MyPortfolioUM()

View file

@ -1,10 +1,18 @@
package com.tangem.features.markets.portfolio.impl.ui.state
import com.tangem.core.ui.components.token.state.TokenItemState
import kotlinx.collections.immutable.ImmutableList
internal data class PortfolioTokenUM(
val tokenItemState: TokenItemState,
val isBalanceHidden: Boolean,
val isQuickActionsShown: Boolean,
val onQuickActionClick: (QuickActionUM) -> Unit,
)
val quickActions: QuickActions,
) {
data class QuickActions(
val actions: ImmutableList<QuickActionUM>,
val onQuickActionClick: (QuickActionUM) -> Unit,
val onQuickActionLongClick: (QuickActionUM) -> Unit,
)
}

View file

@ -11,6 +11,7 @@ internal enum class QuickActionUM(
val title: TextReference,
val description: TextReference,
@DrawableRes val icon: Int,
val longClickAvailable: Boolean = false,
) {
Buy(
title = resourceReference(R.string.common_buy),
@ -26,5 +27,6 @@ internal enum class QuickActionUM(
title = resourceReference(R.string.common_receive),
description = resourceReference(R.string.receive_token_description),
icon = R.drawable.ic_arrow_down_24,
longClickAvailable = true,
),
}

View file

@ -1,5 +1,6 @@
package com.tangem.features.markets.portfolio.impl.ui.state
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.TextReference
@ -16,13 +17,38 @@ internal data class TokenActionsBSContentUM(
@Immutable
enum class Action(
val text: TextReference,
@DrawableRes val iconRes: Int,
) {
CopyAddress(text = resourceReference(R.string.common_copy_address)),
Receive(text = resourceReference(R.string.common_receive)),
Sell(text = resourceReference(R.string.common_sell)),
Buy(text = resourceReference(R.string.common_buy)),
Send(text = resourceReference(R.string.common_send)),
Exchange(text = resourceReference(R.string.common_exchange)),
Stake(text = resourceReference(R.string.common_stake)),
CopyAddress(
text = resourceReference(R.string.common_copy_address),
iconRes = R.drawable.ic_copy_24,
),
Send(
text = resourceReference(R.string.common_send),
iconRes = R.drawable.ic_arrow_up_24,
),
Receive(
text = resourceReference(R.string.common_receive),
iconRes = R.drawable.ic_arrow_down_24,
),
Buy(
text = resourceReference(R.string.common_buy),
iconRes = R.drawable.ic_plus_24,
),
Sell(
text = resourceReference(R.string.common_sell),
iconRes = R.drawable.ic_currency_24,
),
Exchange(
text = resourceReference(R.string.common_exchange),
iconRes = R.drawable.ic_exchange_horizontal_24,
),
Stake(
text = resourceReference(R.string.common_stake),
iconRes = R.drawable.ic_staking_24,
),
;
val order: Int = ordinal
}
}

View file

@ -121,16 +121,18 @@ internal class MarketsTokenItemConverter(
}
}
@Suppress("MagicNumber")
private fun TokenMarket.getTrendType(): PriceChangeType {
val percent = when (currentTrendInterval) {
TrendInterval.H24 -> tokenQuotesShort.h24ChangePercent
TrendInterval.D7 -> tokenQuotesShort.weekChangePercent
TrendInterval.M1 -> tokenQuotesShort.monthChangePercent
}.setScale(2, RoundingMode.UP)
return when (percent.compareTo(BigDecimal.ZERO)) {
1 -> PriceChangeType.UP
-1 -> PriceChangeType.DOWN
}
val scaled = percent.setScale(4, RoundingMode.HALF_UP)
return when {
scaled == null -> PriceChangeType.NEUTRAL
scaled > BigDecimal.ZERO -> PriceChangeType.UP
scaled < BigDecimal.ZERO -> PriceChangeType.DOWN
else -> PriceChangeType.NEUTRAL
}
}

View file

@ -56,6 +56,7 @@ dependencies {
implementation(projects.core.deepLinks.global)
implementation(projects.core.featuretoggles)
implementation(projects.core.decompose)
implementation(projects.common.ui)
implementation(projects.libs.crypto)

View file

@ -2,6 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
import androidx.paging.PagingData
import arrow.core.Either
import com.tangem.common.ui.tokens.getUnavailabilityReasonText
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig
@ -12,7 +13,6 @@ import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.event.triggeredEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.card.NetworkHasDerivationUseCase
@ -216,7 +216,7 @@ internal class TokenDetailsStateFactory(
isShow = true,
onDismissRequest = clickIntents::onDismissDialog,
content = TokenDetailsDialogConfig.DialogContentConfig.DisabledButtonReasonDialogConfig(
text = getUnavailabilityReasonText(unavailabilityReason),
text = unavailabilityReason.getUnavailabilityReasonText(),
onConfirmClick = clickIntents::onDismissDialog,
),
),
@ -408,66 +408,4 @@ internal class TokenDetailsStateFactory(
}.toImmutableList(),
)
}
private fun getUnavailabilityReasonText(unavailabilityReason: ScenarioUnavailabilityReason): TextReference {
return when (unavailabilityReason) {
is ScenarioUnavailabilityReason.StakingUnavailable -> {
resourceReference(
id = R.string.token_button_unavailability_reason_staking_unavailable,
formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName),
)
}
is ScenarioUnavailabilityReason.PendingTransaction -> {
when (unavailabilityReason.withdrawalScenario) {
ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference(
id = R.string.token_button_unavailability_reason_pending_transaction_send,
formatArgs = wrappedList(unavailabilityReason.networkName),
)
ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference(
id = R.string.token_button_unavailability_reason_pending_transaction_sell,
formatArgs = wrappedList(unavailabilityReason.networkName),
)
}
}
is ScenarioUnavailabilityReason.EmptyBalance -> {
when (unavailabilityReason.withdrawalScenario) {
ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference(
id = R.string.token_button_unavailability_reason_empty_balance_send,
)
ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference(
id = R.string.token_button_unavailability_reason_empty_balance_sell,
)
}
}
is ScenarioUnavailabilityReason.BuyUnavailable -> {
resourceReference(
id = R.string.token_button_unavailability_reason_buy_unavailable,
formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName),
)
}
is ScenarioUnavailabilityReason.NotExchangeable -> {
resourceReference(
id = R.string.token_button_unavailability_reason_not_exchangeable,
formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName),
)
}
is ScenarioUnavailabilityReason.NotSupportedBySellService -> {
resourceReference(
id = R.string.token_button_unavailability_reason_sell_unavailable,
formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName),
)
}
ScenarioUnavailabilityReason.Unreachable -> {
resourceReference(
id = R.string.token_button_unavailability_generic_description,
)
}
ScenarioUnavailabilityReason.UnassociatedAsset -> resourceReference(
id = R.string.warning_receive_blocked_hedera_token_association_required_message,
)
ScenarioUnavailabilityReason.None -> {
throw IllegalArgumentException("The unavailability reason must be other than None")
}
}
}
}

View file

@ -78,7 +78,7 @@ dependencies {
implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.analytics)
implementation(projects.domain.visa)
implementation(projects.domain.staking)
implementation(projects.domain.staking.models)
//TODO: Create api/impl modules for onboarding [REDACTED_JIRA]
implementation(projects.features.onboarding)

View file

@ -5,6 +5,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.defaultComponentContext
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.utils.asRouter
import com.tangem.core.decompose.context.DefaultAppComponentContext
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.ui.UiDependencies
@ -44,6 +46,9 @@ internal class WalletFragment : ComposeFragment() {
@Inject
internal lateinit var marketsFeatureToggles: MarketsFeatureToggles
@Inject
internal lateinit var appRouter: AppRouter
private var marketsEntryComponent: MarketsEntryComponent? = null
private val _walletRouter: InnerWalletRouter
@ -60,6 +65,7 @@ internal class WalletFragment : ComposeFragment() {
messageHandler = uiDependencies.eventMessageHandler,
dispatchers = coroutineDispatcherProvider,
hiltComponentBuilder = componentBuilder,
replaceRouter = appRouter.asRouter(),
)
marketsEntryComponent = marketsEntryComponentFactory.create(appContext)

View file

@ -62,7 +62,7 @@ internal class MultiWalletCurrencyActionsConverter(
is TokenActionsState.ActionState.Stake -> {
title = resourceReference(R.string.common_stake)
icon = R.drawable.ic_staking_24
action = { clickIntents.onStakeClick(cryptoCurrencyStatus) }
action = { clickIntents.onStakeClick(cryptoCurrencyStatus, actionsState.yield) }
}
is TokenActionsState.ActionState.Sell -> {
title = resourceReference(R.string.common_sell)

View file

@ -1,9 +1,9 @@
package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents
import arrow.core.getOrElse
import com.tangem.blockchain.common.address.AddressType
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.tokens.getUnavailabilityReasonText
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
@ -14,7 +14,6 @@ import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModel
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.WrappedList
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.haptic.VibratorHapticManager
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
@ -22,7 +21,7 @@ import com.tangem.domain.appcurrency.extenstions.unwrap
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.staking.GetYieldUseCase
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.CryptoCurrency
@ -67,7 +66,7 @@ interface WalletCurrencyActionsClickIntents {
fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus)
fun onStakeClick(cryptoCurrencyStatus: CryptoCurrencyStatus)
fun onStakeClick(cryptoCurrencyStatus: CryptoCurrencyStatus, yield: Yield?)
fun onCopyAddressLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus): TextReference?
@ -100,7 +99,6 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
private val reduxStateHolder: ReduxStateHolder,
private val vibratorHapticManager: VibratorHapticManager,
private val clipboardManager: ClipboardManager,
private val getYieldUseCase: GetYieldUseCase,
private val appRouter: AppRouter,
) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents {
@ -380,25 +378,19 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
showErrorIfDemoModeOrElse(action = ::openExplorer)
}
override fun onStakeClick(cryptoCurrencyStatus: CryptoCurrencyStatus) {
override fun onStakeClick(cryptoCurrencyStatus: CryptoCurrencyStatus, yield: Yield?) {
val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return
stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWallet.walletId))
viewModelScope.launch {
val userWalletId = stateHolder.getSelectedWalletId()
val cryptoCurrency = cryptoCurrencyStatus.currency
val yield = getYieldUseCase.invoke(
cryptoCurrencyId = cryptoCurrency.id,
symbol = cryptoCurrency.symbol,
).getOrElse {
error("Staking is unavailable for ${cryptoCurrency.name}")
}
reduxStateHolder.dispatch(
TradeCryptoAction.Stake(
appRouter.push(
AppRoute.Staking(
userWalletId = userWalletId,
cryptoCurrencyId = cryptoCurrency.id,
yield = yield,
yield = yield ?: return@launch,
),
)
}
@ -487,14 +479,12 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
private fun handleUnavailabilityReason(unavailabilityReason: ScenarioUnavailabilityReason): Boolean {
if (unavailabilityReason == ScenarioUnavailabilityReason.None) return false
val unavailabilityReasonText = getUnavailabilityReasonText(unavailabilityReason)
viewModelScope.launch(dispatchers.main) {
walletEventSender.send(
event = WalletEvent.ShowAlert(
state = WalletAlertState.DefaultAlert(
title = null,
message = unavailabilityReasonText,
message = unavailabilityReason.getUnavailabilityReasonText(),
onConfirmClick = null,
),
),
@ -503,66 +493,4 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
return true
}
private fun getUnavailabilityReasonText(unavailabilityReason: ScenarioUnavailabilityReason): TextReference {
return when (unavailabilityReason) {
is ScenarioUnavailabilityReason.StakingUnavailable -> {
resourceReference(
id = R.string.token_button_unavailability_reason_staking_unavailable,
formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName),
)
}
is ScenarioUnavailabilityReason.PendingTransaction -> {
when (unavailabilityReason.withdrawalScenario) {
ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference(
id = R.string.token_button_unavailability_reason_pending_transaction_send,
formatArgs = wrappedList(unavailabilityReason.networkName),
)
ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference(
id = R.string.token_button_unavailability_reason_pending_transaction_sell,
formatArgs = wrappedList(unavailabilityReason.networkName),
)
}
}
is ScenarioUnavailabilityReason.EmptyBalance -> {
when (unavailabilityReason.withdrawalScenario) {
ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference(
id = R.string.token_button_unavailability_reason_empty_balance_send,
)
ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference(
id = R.string.token_button_unavailability_reason_empty_balance_sell,
)
}
}
is ScenarioUnavailabilityReason.BuyUnavailable -> {
resourceReference(
id = R.string.token_button_unavailability_reason_buy_unavailable,
formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName),
)
}
is ScenarioUnavailabilityReason.NotExchangeable -> {
resourceReference(
id = R.string.token_button_unavailability_reason_not_exchangeable,
formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName),
)
}
is ScenarioUnavailabilityReason.NotSupportedBySellService -> {
resourceReference(
id = R.string.token_button_unavailability_reason_sell_unavailable,
formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName),
)
}
ScenarioUnavailabilityReason.Unreachable -> {
resourceReference(
id = R.string.token_button_unavailability_generic_description,
)
}
ScenarioUnavailabilityReason.UnassociatedAsset -> resourceReference(
id = R.string.warning_receive_blocked_hedera_token_association_required_message,
)
ScenarioUnavailabilityReason.None -> {
throw IllegalArgumentException("The unavailability reason must be other than None")
}
}
}
}