Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-24 10:50:03 +01:00
commit bc84d8f5f8
579 changed files with 13604 additions and 3422 deletions

View file

@ -38,13 +38,14 @@ dependencies {
implementation(deps.tangem.card.core)
implementation(deps.timber)
implementation(deps.lifecycle.compose)
implementation(deps.kotlin.serialization)
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
/** Core modules */
implementation(projects.common)
implementation(projects.common.routing)
implementation(projects.core.navigation)
implementation(projects.core.ui)
implementation(projects.core.utils)
@ -85,4 +86,6 @@ dependencies {
/** Feature Apis */
implementation(projects.features.tokendetails.api)
implementation(projects.features.send.api)
implementation(projects.features.staking.api)
}

View file

@ -1,6 +1,8 @@
package com.tangem.feature.tokendetails.di
import com.tangem.core.navigation.ReduxNavController
import com.tangem.common.routing.AppRouter
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.feature.tokendetails.presentation.router.DefaultTokenDetailsRouter
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
import dagger.Module
@ -15,7 +17,11 @@ internal object TokenDetailsRouterModule {
@Provides
@ActivityScoped
fun provideTokenDetailsRouter(reduxNavController: ReduxNavController): TokenDetailsRouter {
return DefaultTokenDetailsRouter(reduxNavController)
fun provideTokenDetailsRouter(
appRouter: AppRouter,
urlOpener: UrlOpener,
shareManager: ShareManager,
): TokenDetailsRouter {
return DefaultTokenDetailsRouter(appRouter, urlOpener, shareManager)
}
}

View file

@ -6,8 +6,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.components.NavigationBar3ButtonsScrim
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen
@ -34,14 +33,8 @@ internal class TokenDetailsFragment : ComposeFragment() {
override fun ScreenContent(modifier: Modifier) {
val viewModel = hiltViewModel<TokenDetailsViewModel>()
viewModel.router = this@TokenDetailsFragment.internalTokenDetailsRouter
LocalLifecycleOwner.current.lifecycle.addObserver(viewModel)
val systemBarsColor = TangemTheme.colors.background.secondary
SystemBarsEffect {
setSystemBarsColor(systemBarsColor)
}
NavigationBar3ButtonsScrim()
TokenDetailsScreen(state = viewModel.uiState.collectAsStateWithLifecycle().value)
}
}

View file

@ -1,41 +1,50 @@
package com.tangem.feature.tokendetails.presentation.router
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.ReduxNavController
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.staking.model.Yield
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.tokendetails.presentation.TokenDetailsFragment
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
internal class DefaultTokenDetailsRouter(
private val reduxNavController: ReduxNavController,
private val router: AppRouter,
private val urlOpener: UrlOpener,
private val shareManager: ShareManager,
) : InnerTokenDetailsRouter {
override fun getEntryFragment(): Fragment = TokenDetailsFragment()
override fun popBackStack() {
reduxNavController.navigate(NavigationAction.PopBackTo())
router.pop()
}
override fun openUrl(url: String) {
reduxNavController.navigate(NavigationAction.OpenUrl(url = url))
urlOpener.openUrl(url)
}
override fun share(text: String) {
reduxNavController.navigate(NavigationAction.Share(text))
shareManager.shareText(text)
}
override fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) {
reduxNavController.navigate(
action = NavigationAction.NavigateTo(
screen = AppScreen.WalletDetails,
bundle = bundleOf(
TokenDetailsRouter.USER_WALLET_ID_KEY to userWalletId.stringValue,
TokenDetailsRouter.CRYPTO_CURRENCY_KEY to currency,
),
router.push(
AppRoute.CurrencyDetails(
userWalletId = userWalletId,
currency = currency,
),
)
}
override fun openStaking(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, yield: Yield) {
router.push(
AppRoute.Staking(
userWalletId = userWalletId,
cryptoCurrencyId = cryptoCurrency.id,
yield = yield,
),
)
}

View file

@ -1,5 +1,6 @@
package com.tangem.feature.tokendetails.presentation.router
import com.tangem.domain.staking.model.Yield
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
@ -16,4 +17,6 @@ internal interface InnerTokenDetailsRouter : TokenDetailsRouter {
fun share(text: String)
fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency)
fun openStaking(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, yield: Yield)
}

View file

@ -9,7 +9,9 @@ import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton
@ -18,6 +20,7 @@ import com.tangem.features.tokendetails.impl.R
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
@Suppress("LargeClass")
internal object TokenDetailsPreviewData {
val tokenDetailsTopAppBarConfig = TokenDetailsTopAppBarConfig(
@ -100,17 +103,47 @@ internal object TokenDetailsPreviewData {
TokenDetailsActionButton.Swap(dimContent = false, onClick = {}),
)
val balanceLoading = TokenDetailsBalanceBlockState.Loading(actionButtons = actionButtons)
private val balanceSegmentedButtonConfig = persistentListOf(
TokenBalanceSegmentedButtonConfig(
title = resourceReference(R.string.common_all),
type = BalanceType.ALL,
),
TokenBalanceSegmentedButtonConfig(
title = resourceReference(R.string.staking_details_available),
type = BalanceType.AVAILABLE,
),
)
val balanceLoading = TokenDetailsBalanceBlockState.Loading(
actionButtons = actionButtons,
balanceSegmentedButtonConfig = balanceSegmentedButtonConfig,
selectedBalanceType = BalanceType.ALL,
)
val balanceContent = TokenDetailsBalanceBlockState.Content(
actionButtons = actionButtons,
fiatBalance = "91,50$",
cryptoBalance = "966,96 XLM",
isStakingEnabled = true,
balanceSegmentedButtonConfig = balanceSegmentedButtonConfig,
selectedBalanceType = BalanceType.ALL,
onBalanceSelect = {},
)
val balanceError = TokenDetailsBalanceBlockState.Error(
actionButtons = actionButtons,
balanceSegmentedButtonConfig = balanceSegmentedButtonConfig,
selectedBalanceType = BalanceType.ALL,
)
val balanceError = TokenDetailsBalanceBlockState.Error(actionButtons = actionButtons)
private val marketPriceLoading = MarketPriceBlockState.Loading(currencySymbol = "USDT")
private val stakingLoading = StakingBlockState.Loading(iconState = iconState)
private val stakingLoading = StakingBlocksState(
stakingAvailable = StakingAvailable.Loading(iconState),
stakingBalance = StakingBalance.Content(
cryptoAmount = stringReference("5 SOL"),
fiatAmount = stringReference("456.34 $"),
rewardAmount = resourceReference(R.string.staking_details_no_rewards_to_claim, wrappedList("0.43 $")),
),
)
private val pullToRefreshConfig = TokenDetailsPullToRefreshConfig(
isRefreshing = false,
@ -246,7 +279,7 @@ internal object TokenDetailsPreviewData {
tokenInfoBlockState = tokenInfoBlockState,
tokenBalanceBlockState = balanceLoading,
marketPriceBlockState = marketPriceLoading,
stakingBlockState = stakingLoading,
stakingBlocksState = stakingLoading,
notifications = persistentListOf(),
txHistoryState = TxHistoryState.Content(
contentItems = MutableStateFlow(
@ -260,7 +293,7 @@ internal object TokenDetailsPreviewData {
bottomSheetConfig = null,
isBalanceHidden = false,
isMarketPriceAvailable = false,
isStakingAvailable = false,
isStakingBlockShown = false,
event = consumedEvent(),
)
@ -278,11 +311,19 @@ internal object TokenDetailsPreviewData {
type = PriceChangeType.UP,
),
),
stakingBlockState = StakingBlockState.Content(
interestRate = "7.38",
periodInDays = 4,
tokenSymbol = "XLM",
iconState = iconState,
stakingBlocksState = StakingBlocksState(
stakingAvailable = StakingAvailable.Content(
interestRate = "7.38",
periodInDays = 4,
tokenSymbol = "XLM",
iconState = iconState,
onStakeClicked = {},
),
stakingBalance = StakingBalance.Content(
cryptoAmount = stringReference("5 SOL"),
fiatAmount = stringReference("456.34 $"),
rewardAmount = resourceReference(R.string.staking_details_rewards_to_claim, wrappedList("0.43 $")),
),
),
notifications = persistentListOf(),
txHistoryState = TxHistoryState.NotSupported(
@ -296,7 +337,7 @@ internal object TokenDetailsPreviewData {
bottomSheetConfig = null,
isBalanceHidden = false,
isMarketPriceAvailable = true,
isStakingAvailable = true,
isStakingBlockShown = true,
event = consumedEvent(),
)

View file

@ -1,20 +1,37 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
internal data class StakingBlocksState(
val stakingAvailable: StakingAvailable,
val stakingBalance: StakingBalance,
)
@Immutable
internal sealed interface StakingBlockState {
internal sealed interface StakingAvailable {
val iconState: IconState
data class Error(override val iconState: IconState) : StakingBlockState
data class Error(override val iconState: IconState) : StakingAvailable
data class Loading(override val iconState: IconState) : StakingBlockState
data class Loading(override val iconState: IconState) : StakingAvailable
data class Content(
override val iconState: IconState,
val interestRate: String,
val periodInDays: Int,
val tokenSymbol: String,
) : StakingBlockState
val onStakeClicked: () -> Unit,
) : StakingAvailable
}
@Immutable
sealed class StakingBalance {
data object Empty : StakingBalance()
data class Content(
val cryptoAmount: TextReference,
val fiatAmount: TextReference,
val rewardAmount: TextReference,
) : StakingBalance()
}

View file

@ -0,0 +1,13 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
import com.tangem.core.ui.extensions.TextReference
data class TokenBalanceSegmentedButtonConfig(
val title: TextReference,
val type: BalanceType,
)
enum class BalanceType {
ALL,
AVAILABLE,
}

View file

@ -6,19 +6,29 @@ import kotlinx.collections.immutable.ImmutableList
internal sealed class TokenDetailsBalanceBlockState {
abstract val actionButtons: ImmutableList<TokenDetailsActionButton>
abstract val balanceSegmentedButtonConfig: ImmutableList<TokenBalanceSegmentedButtonConfig>
abstract val selectedBalanceType: BalanceType
data class Loading(
override val actionButtons: ImmutableList<TokenDetailsActionButton>,
override val balanceSegmentedButtonConfig: ImmutableList<TokenBalanceSegmentedButtonConfig>,
override val selectedBalanceType: BalanceType,
) : TokenDetailsBalanceBlockState()
data class Content(
override val actionButtons: ImmutableList<TokenDetailsActionButton>,
override val balanceSegmentedButtonConfig: ImmutableList<TokenBalanceSegmentedButtonConfig>,
override val selectedBalanceType: BalanceType,
val fiatBalance: String,
val cryptoBalance: String,
val isStakingEnabled: Boolean,
val onBalanceSelect: (TokenBalanceSegmentedButtonConfig) -> Unit,
) : TokenDetailsBalanceBlockState()
data class Error(
override val actionButtons: ImmutableList<TokenDetailsActionButton>,
override val balanceSegmentedButtonConfig: ImmutableList<TokenBalanceSegmentedButtonConfig>,
override val selectedBalanceType: BalanceType,
) : TokenDetailsBalanceBlockState()
fun copyActionButtons(buttons: ImmutableList<TokenDetailsActionButton>): TokenDetailsBalanceBlockState {

View file

@ -17,7 +17,7 @@ internal data class TokenDetailsState(
val tokenInfoBlockState: TokenInfoBlockState,
val tokenBalanceBlockState: TokenDetailsBalanceBlockState,
val marketPriceBlockState: MarketPriceBlockState,
val stakingBlockState: StakingBlockState,
val stakingBlocksState: StakingBlocksState,
val notifications: ImmutableList<TokenDetailsNotification>,
val pendingTxs: PersistentList<TransactionState>,
val swapTxs: PersistentList<SwapTransactionsState>,
@ -27,6 +27,6 @@ internal data class TokenDetailsState(
val bottomSheetConfig: TangemBottomSheetConfig?,
val isBalanceHidden: Boolean,
val isMarketPriceAvailable: Boolean,
val isStakingAvailable: Boolean,
val isStakingBlockShown: Boolean,
val event: StateEvent<TextReference>,
)

View file

@ -91,4 +91,19 @@ internal sealed class TokenDetailsActionButton(val config: ActionButtonConfig) {
dimContent = dimContent,
),
)
/**
* Staking
*
* @property dimContent determines whether the button content will be dimmed
* @property onClick lambda be invoked when Swap button is clicked
*/
data class Stake(val dimContent: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton(
config = ActionButtonConfig(
text = TextReference.Res(id = R.string.common_stake),
iconResId = R.drawable.ic_arrow_down_24, // TODO staking
onClick = onClick,
dimContent = dimContent,
),
)
}

View file

@ -38,6 +38,12 @@ internal class TokenDetailsActionButtonsConverter(
onLongClick = clickIntents::onCopyAddress,
)
}
is TokenActionsState.ActionState.Stake -> {
TokenDetailsActionButton.Stake(
dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None,
onClick = { clickIntents.onStakeClick(action.unavailabilityReason) },
)
}
is TokenActionsState.ActionState.Sell -> {
TokenDetailsActionButton.Sell(
dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None,

View file

@ -10,6 +10,7 @@ import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.BalanceType
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.components.TokenDetailsNotification
@ -23,6 +24,7 @@ import kotlinx.collections.immutable.toPersistentList
internal class TokenDetailsLoadedBalanceConverter(
private val currentStateProvider: Provider<TokenDetailsState>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val isStakingEnabled: Boolean,
private val symbol: String,
private val decimals: Int,
private val clickIntents: TokenDetailsClickIntents,
@ -39,7 +41,11 @@ internal class TokenDetailsLoadedBalanceConverter(
private fun convertError(): TokenDetailsState {
val state = currentStateProvider()
return state.copy(
tokenBalanceBlockState = TokenDetailsBalanceBlockState.Error(state.tokenBalanceBlockState.actionButtons),
tokenBalanceBlockState = TokenDetailsBalanceBlockState.Error(
actionButtons = state.tokenBalanceBlockState.actionButtons,
balanceSegmentedButtonConfig = state.tokenBalanceBlockState.balanceSegmentedButtonConfig,
selectedBalanceType = state.tokenBalanceBlockState.selectedBalanceType,
),
marketPriceBlockState = MarketPriceBlockState.Error(state.marketPriceBlockState.currencySymbol),
notifications = persistentListOf(TokenDetailsNotification.NetworksUnreachable),
)
@ -74,12 +80,24 @@ internal class TokenDetailsLoadedBalanceConverter(
actionButtons = currentState.actionButtons,
fiatBalance = formatFiatAmount(status.value, appCurrencyProvider()),
cryptoBalance = formatCryptoAmount(status),
isStakingEnabled = isStakingEnabled,
balanceSegmentedButtonConfig = currentState.balanceSegmentedButtonConfig,
onBalanceSelect = clickIntents::onBalanceSelect,
selectedBalanceType = currentState.selectedBalanceType,
)
is CryptoCurrencyStatus.Loading -> TokenDetailsBalanceBlockState.Loading(
actionButtons = currentState.actionButtons,
balanceSegmentedButtonConfig = currentState.balanceSegmentedButtonConfig,
selectedBalanceType = BalanceType.ALL,
)
is CryptoCurrencyStatus.Loading -> TokenDetailsBalanceBlockState.Loading(currentState.actionButtons)
is CryptoCurrencyStatus.MissedDerivation,
is CryptoCurrencyStatus.Unreachable,
is CryptoCurrencyStatus.NoAmount,
-> TokenDetailsBalanceBlockState.Error(currentState.actionButtons)
-> TokenDetailsBalanceBlockState.Error(
actionButtons = currentState.actionButtons,
balanceSegmentedButtonConfig = currentState.balanceSegmentedButtonConfig,
selectedBalanceType = BalanceType.ALL,
)
}
}

View file

@ -7,7 +7,6 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
@ -17,7 +16,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.Toke
import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles
import com.tangem.features.tokendetails.impl.R
import com.tangem.lib.crypto.BlockchainUtils.isBitcoin
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -27,7 +25,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
internal class TokenDetailsSkeletonStateConverter(
private val clickIntents: TokenDetailsClickIntents,
private val featureToggles: TokenDetailsFeatureToggles,
private val stakingAvailabilityProvider: Provider<StakingAvailability>,
) : Converter<CryptoCurrency, TokenDetailsState> {
private val iconStateConverter by lazy { TokenDetailsIconStateConverter() }
@ -51,9 +48,16 @@ internal class TokenDetailsSkeletonStateConverter(
)
},
),
tokenBalanceBlockState = TokenDetailsBalanceBlockState.Loading(actionButtons = createButtons()),
tokenBalanceBlockState = TokenDetailsBalanceBlockState.Loading(
actionButtons = createButtons(),
balanceSegmentedButtonConfig = createBalanceSegmentedButtonConfig(),
selectedBalanceType = BalanceType.ALL,
),
marketPriceBlockState = MarketPriceBlockState.Loading(value.symbol),
stakingBlockState = StakingBlockState.Loading(iconState = iconState),
stakingBlocksState = StakingBlocksState(
stakingAvailable = StakingAvailable.Loading(iconState),
stakingBalance = StakingBalance.Empty,
),
notifications = persistentListOf(),
pendingTxs = persistentListOf(),
swapTxs = persistentListOf(),
@ -67,7 +71,7 @@ internal class TokenDetailsSkeletonStateConverter(
bottomSheetConfig = null,
isBalanceHidden = true,
isMarketPriceAvailable = value.id.rawCurrencyId != null,
isStakingAvailable = stakingAvailabilityProvider.invoke() is StakingAvailability.Available,
isStakingBlockShown = false,
event = consumedEvent(),
)
}
@ -102,6 +106,19 @@ internal class TokenDetailsSkeletonStateConverter(
)
}
private fun createBalanceSegmentedButtonConfig(): ImmutableList<TokenBalanceSegmentedButtonConfig> {
return persistentListOf(
TokenBalanceSegmentedButtonConfig(
title = resourceReference(R.string.common_all),
type = BalanceType.ALL,
),
TokenBalanceSegmentedButtonConfig(
title = resourceReference(R.string.staking_details_available),
type = BalanceType.AVAILABLE,
),
)
}
private fun createPullToRefresh(): TokenDetailsPullToRefreshConfig = TokenDetailsPullToRefreshConfig(
isRefreshing = false,
onRefresh = clickIntents::onRefreshSwipe,

View file

@ -24,9 +24,7 @@ import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.txhistory.models.TxHistoryListError
import com.tangem.domain.txhistory.models.TxHistoryStateError
import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsAppBarMenuConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadedTxHistoryConverter
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter
@ -44,9 +42,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
internal class TokenDetailsStateFactory(
private val currentStateProvider: Provider<TokenDetailsState>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val stakingAvailabilityProvider: Provider<StakingAvailability>,
private val clickIntents: TokenDetailsClickIntents,
private val featureToggles: TokenDetailsFeatureToggles,
private val isStakingEnabled: Boolean,
symbol: String,
decimals: Int,
) {
@ -55,7 +53,6 @@ internal class TokenDetailsStateFactory(
TokenDetailsSkeletonStateConverter(
clickIntents = clickIntents,
featureToggles = featureToggles,
stakingAvailabilityProvider = stakingAvailabilityProvider,
)
}
@ -67,6 +64,7 @@ internal class TokenDetailsStateFactory(
TokenDetailsLoadedBalanceConverter(
currentStateProvider = currentStateProvider,
appCurrencyProvider = appCurrencyProvider,
isStakingEnabled = isStakingEnabled,
symbol = symbol,
decimals = decimals,
clickIntents = clickIntents,
@ -105,6 +103,7 @@ internal class TokenDetailsStateFactory(
private val stakingStateConverter by lazy {
TokenStakingStateConverter(
currentStateProvider = currentStateProvider,
clickIntents = clickIntents,
)
}
@ -212,9 +211,15 @@ internal class TokenDetailsStateFactory(
)
}
fun getStateWithUpdatedStakingAvailability(stakingAvailability: StakingAvailability): TokenDetailsState {
return currentStateProvider().copy(
isStakingBlockShown = stakingAvailability != StakingAvailability.Unavailable,
)
}
fun getStateWithStaking(stakingEither: Either<Throwable, StakingEntryInfo>): TokenDetailsState {
return currentStateProvider().copy(
stakingBlockState = stakingStateConverter.convert(stakingEither),
stakingBlocksState = stakingStateConverter.convert(stakingEither),
)
}
@ -344,6 +349,17 @@ internal class TokenDetailsStateFactory(
}
}
fun getStateWithUpdatedBalanceSegmentedButtonConfig(
buttonConfig: TokenBalanceSegmentedButtonConfig,
): TokenDetailsState {
return with(currentStateProvider()) {
val updatedState = (tokenBalanceBlockState as? TokenDetailsBalanceBlockState.Content)
?.copy(selectedBalanceType = buttonConfig.type)
?: tokenBalanceBlockState
copy(tokenBalanceBlockState = updatedState)
}
}
private fun TokenDetailsAppBarMenuConfig.updateMenu(
cardTypesResolver: CardTypesResolver,
isBitcoin: Boolean,
@ -370,6 +386,12 @@ internal class TokenDetailsStateFactory(
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(

View file

@ -3,31 +3,42 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
import arrow.core.Either
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingAvailable
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBalance
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlocksState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
internal class TokenStakingStateConverter(
private val clickIntents: TokenDetailsClickIntents,
private val currentStateProvider: Provider<TokenDetailsState>,
) : Converter<Either<Throwable, StakingEntryInfo>, StakingBlockState> {
) : Converter<Either<Throwable, StakingEntryInfo>, StakingBlocksState> {
override fun convert(value: Either<Throwable, StakingEntryInfo>): StakingBlockState {
override fun convert(value: Either<Throwable, StakingEntryInfo>): StakingBlocksState {
value.fold(
ifLeft = {
return StakingBlockState.Error(
iconState = currentStateProvider().tokenInfoBlockState.iconState,
return StakingBlocksState(
stakingAvailable = StakingAvailable.Error(
iconState = currentStateProvider().tokenInfoBlockState.iconState,
),
stakingBalance = StakingBalance.Empty,
)
},
ifRight = {
return StakingBlockState.Content(
interestRate = BigDecimalFormatter.formatPercent(
percent = it.interestRate,
useAbsoluteValue = true,
return StakingBlocksState(
stakingAvailable = StakingAvailable.Content(
interestRate = BigDecimalFormatter.formatPercent(
percent = it.interestRate,
useAbsoluteValue = true,
),
periodInDays = it.periodInDays,
tokenSymbol = it.tokenSymbol,
iconState = currentStateProvider().tokenInfoBlockState.iconState,
onStakeClicked = clickIntents::onStakeBannerClick,
),
periodInDays = it.periodInDays,
tokenSymbol = it.tokenSymbol,
iconState = currentStateProvider().tokenInfoBlockState.iconState,
stakingBalance = StakingBalance.Empty,
)
},
)

View file

@ -3,10 +3,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui
import android.content.res.Configuration
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.ExperimentalMaterialApi
@ -14,6 +11,7 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScaffoldDefaults
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
@ -21,6 +19,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
@ -42,10 +41,10 @@ import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingAvailable
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBalance
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.*
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlock
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsDialogs
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar
@ -53,6 +52,8 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.T
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheet
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.swapTransactionsItems
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.StakingBalanceBlock
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.TokenStakingBlock
// TODO: Split to blocks [REDACTED_JIRA]
@Suppress("LongMethod")
@ -60,11 +61,13 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.e
@Composable
internal fun TokenDetailsScreen(state: TokenDetailsState) {
BackHandler(onBack = state.topAppBarConfig.onBackClick)
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
val snackbarHostState = remember { SnackbarHostState() }
Scaffold(
topBar = { TokenDetailsTopAppBar(config = state.topAppBarConfig) },
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.navigationBars),
containerColor = TangemTheme.colors.background.secondary,
) { scaffoldPaddings ->
val pullRefreshState = rememberPullRefreshState(
@ -90,7 +93,9 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) {
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = TangemTheme.dimens.spacing16),
contentPadding = PaddingValues(
bottom = TangemTheme.dimens.spacing16 + bottomBarHeight,
),
) {
item {
TokenInfoBlock(
@ -135,12 +140,29 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) {
)
}
if (state.isStakingAvailable) {
if (state.isStakingBlockShown) {
item(
key = StakingBlockState::class.java,
contentType = StakingBlockState::class.java,
content = { TokenStakingBlock(modifier = itemModifier, state = state.stakingBlockState) },
key = StakingAvailable::class.java,
contentType = StakingAvailable::class.java,
content = {
TokenStakingBlock(
modifier = itemModifier,
state = state.stakingBlocksState.stakingAvailable,
)
},
)
if (state.stakingBlocksState.stakingBalance is StakingBalance.Content) {
item(
key = StakingBalance::class.java,
contentType = StakingBalance::class.java,
content = {
StakingBalanceBlock(
state = state.stakingBlocksState.stakingBalance,
modifier = itemModifier,
)
},
)
}
}
swapTransactionsItems(

View file

@ -11,9 +11,10 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.common.Strings.STARS
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.buttons.HorizontalActionChips
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.BigDecimalFormatter
@ -21,6 +22,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPre
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 com.tangem.utils.Strings.STARS
import kotlinx.collections.immutable.toImmutableList
@Composable
@ -35,20 +37,23 @@ internal fun TokenDetailsBalanceBlock(
color = TangemTheme.colors.background.primary,
) {
Column {
Box(
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing12)
.padding(horizontal = TangemTheme.dimens.spacing12)
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.spacing24),
contentAlignment = Alignment.CenterStart,
) {
Text(
text = stringResource(id = R.string.common_balance_title),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.subtitle2,
maxLines = 1,
modifier = Modifier
.weight(1f)
.padding(top = TangemTheme.dimens.spacing12),
)
BalanceButtons(state)
}
FiatBalance(
state = state,
@ -130,6 +135,35 @@ private fun CryptoBalance(
}
}
@Composable
private fun BalanceButtons(state: TokenDetailsBalanceBlockState) {
if (state !is TokenDetailsBalanceBlockState.Content) return
SegmentedButtons(
config = state.balanceSegmentedButtonConfig,
onClick = state.onBalanceSelect,
showIndication = false,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing11)
.width(IntrinsicSize.Min),
) { config ->
Text(
text = config.title.resolveReference(),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.caption1,
maxLines = 1,
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing5,
end = TangemTheme.dimens.spacing5,
top = TangemTheme.dimens.spacing3,
bottom = TangemTheme.dimens.spacing3,
)
.align(Alignment.Center),
)
}
}
@Preview(widthDp = 328, heightDp = 152)
@Preview(widthDp = 328, heightDp = 152, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable

View file

@ -0,0 +1,98 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBalance
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.Strings
@Composable
fun StakingBalanceBlock(state: StakingBalance.Content, modifier: Modifier = Modifier) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.padding(TangemTheme.dimens.spacing12),
) {
Column(
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
modifier = Modifier.weight(1f),
) {
Text(
text = stringResource(R.string.staking_native),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing2),
)
Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
Text(
text = state.fiatAmount.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
Text(
text = Strings.DOT,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
Text(
text = state.cryptoAmount.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
Text(
text = state.rewardAmount.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
Icon(
painter = painterResource(id = R.drawable.ic_chevron_right_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
)
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun StakingBalanceBlock_Preview(
@PreviewParameter(StakingBalanceBlockPreviewProvider::class) data: StakingBalance,
) {
TangemThemePreview {
StakingBalanceBlock(
state = data as StakingBalance.Content,
modifier = Modifier.padding(TangemTheme.dimens.spacing16),
)
}
}
private class StakingBalanceBlockPreviewProvider : PreviewParameterProvider<StakingBalance> {
override val values: Sequence<StakingBalance>
get() = sequenceOf(
TokenDetailsPreviewData.tokenDetailsState_1.stakingBlocksState.stakingBalance,
TokenDetailsPreviewData.tokenDetailsState_2.stakingBlocksState.stakingBalance,
)
}
// endregion

View file

@ -1,11 +1,12 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -14,14 +15,17 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.*
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.SpacerW8
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.GRAY_SCALE_ALPHA
import com.tangem.core.ui.utils.GrayscaleColorFilter
import com.tangem.core.ui.utils.NORMAL_ALPHA
import com.tangem.feature.tokendetails.presentation.tokendetails.state.IconState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingAvailable
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.CurrencyIcon
import com.tangem.features.tokendetails.impl.R
/**
@ -31,7 +35,7 @@ import com.tangem.features.tokendetails.impl.R
* @param modifier modifier
*/
@Composable
internal fun TokenStakingBlock(state: StakingBlockState, modifier: Modifier = Modifier) {
internal fun TokenStakingBlock(state: StakingAvailable, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.background(
@ -49,7 +53,7 @@ internal fun TokenStakingBlock(state: StakingBlockState, modifier: Modifier = Mo
}
@Composable
private fun Content(state: StakingBlockState, modifier: Modifier = Modifier) {
private fun Content(state: StakingAvailable, modifier: Modifier = Modifier) {
AnimatedContent(
modifier = modifier.heightIn(min = TangemTheme.dimens.size60),
targetState = state,
@ -57,24 +61,24 @@ private fun Content(state: StakingBlockState, modifier: Modifier = Modifier) {
label = "Update the content",
) { stakingBlockState ->
when (stakingBlockState) {
is StakingBlockState.Content -> {
is StakingAvailable.Content -> {
StakingContent(
stakingBlockState = stakingBlockState,
iconState = stakingBlockState.iconState,
)
}
is StakingBlockState.Loading -> {
is StakingAvailable.Loading -> {
StakingLoading(
iconState = stakingBlockState.iconState,
)
}
is StakingBlockState.Error -> Row {} // TODO staking
is StakingAvailable.Error -> Row {} // TODO staking
}
}
}
@Composable
private fun StakingContent(stakingBlockState: StakingBlockState.Content, iconState: IconState) {
private fun StakingContent(stakingBlockState: StakingAvailable.Content, iconState: IconState) {
Column {
Row {
val (alpha, colorFilter) = remember(iconState.isGrayscale) {
@ -121,8 +125,8 @@ private fun StakingContent(stakingBlockState: StakingBlockState.Content, iconSta
}
SecondaryButton(
modifier = Modifier.fillMaxWidth(),
text = "Stake",
onClick = { /* [REDACTED_TODO_COMMENT] */ },
text = stringResource(id = R.string.common_stake),
onClick = stakingBlockState.onStakeClicked,
)
}
}
@ -177,23 +181,24 @@ private fun StakingLoading(iconState: IconState) {
@Composable
private fun Preview_TokenStakingBlock(
@PreviewParameter(StakingBlockStateProvider::class)
state: StakingBlockState,
state: StakingAvailable,
) {
TangemThemePreview {
TokenStakingBlock(state = state)
}
}
private class StakingBlockStateProvider : CollectionPreviewParameterProvider<StakingBlockState>(
private class StakingBlockStateProvider : CollectionPreviewParameterProvider<StakingAvailable>(
collection = listOf(
StakingBlockState.Content(
StakingAvailable.Content(
iconState = iconState,
interestRate = "10",
periodInDays = 4,
tokenSymbol = "SOL",
onStakeClicked = {},
),
StakingBlockState.Loading(iconState = iconState),
StakingBlockState.Error(iconState = iconState),
StakingAvailable.Loading(iconState = iconState),
StakingAvailable.Error(iconState = iconState),
),
)

View file

@ -4,6 +4,7 @@ import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig
@Suppress("TooManyFunctions")
interface TokenDetailsClickIntents {
@ -12,6 +13,8 @@ interface TokenDetailsClickIntents {
fun onReceiveClick(unavailabilityReason: ScenarioUnavailabilityReason)
fun onStakeClick(unavailabilityReason: ScenarioUnavailabilityReason)
fun onSendClick(unavailabilityReason: ScenarioUnavailabilityReason)
fun onSwapClick(unavailabilityReason: ScenarioUnavailabilityReason)
@ -55,4 +58,8 @@ interface TokenDetailsClickIntents {
fun onCopyAddress(): TextReference?
fun onAssociateClick()
fun onStakeBannerClick()
fun onBalanceSelect(config: TokenBalanceSegmentedButtonConfig)
}

View file

@ -1,9 +1,12 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels
import android.os.Bundle
import androidx.lifecycle.*
import androidx.paging.cachedIn
import arrow.core.getOrElse
import com.tangem.blockchain.common.address.AddressType
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.bundle.unbundle
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.deeplink.DeepLinksRegistry
import com.tangem.core.deeplink.global.BuyCurrencyDeepLink
@ -27,6 +30,7 @@ import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase
import com.tangem.domain.staking.GetStakingAvailabilityUseCase
import com.tangem.domain.staking.GetStakingEntryInfoUseCase
import com.tangem.domain.staking.GetYieldUseCase
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.legacy.TradeCryptoAction
@ -57,12 +61,13 @@ import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRout
import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsCurrencyStatusAnalyticsSender
import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsNotificationsAnalyticsSender
import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles
import com.tangem.features.tokendetails.impl.R
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
import com.tangem.lib.crypto.BlockchainUtils.isBitcoin
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.*
@ -99,7 +104,9 @@ internal class TokenDetailsViewModel @Inject constructor(
private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase,
private val getExtendedPublicKeyForCurrencyUseCase: GetExtendedPublicKeyForCurrencyUseCase,
private val getStakingEntryInfoUseCase: GetStakingEntryInfoUseCase,
private val stakingFeatureToggles: StakingFeatureToggles,
private val getStakingAvailabilityUseCase: GetStakingAvailabilityUseCase,
private val getYieldUseCase: GetYieldUseCase,
private val swapRepository: SwapRepository,
private val swapTransactionRepository: SwapTransactionRepository,
private val quotesRepository: QuotesRepository,
@ -116,12 +123,14 @@ internal class TokenDetailsViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents {
private val userWalletId: UserWalletId = savedStateHandle.get<String>(TokenDetailsRouter.USER_WALLET_ID_KEY)
?.let { stringValue -> UserWalletId(stringValue) }
private val userWalletId: UserWalletId = savedStateHandle.get<Bundle>(AppRoute.CurrencyDetails.USER_WALLET_ID_KEY)
?.unbundle(UserWalletId.serializer())
?: error("This screen can't open without `UserWalletId`")
private val cryptoCurrency: CryptoCurrency = savedStateHandle[TokenDetailsRouter.CRYPTO_CURRENCY_KEY]
?: error("This screen can't open without `CryptoCurrency`")
private val cryptoCurrency: CryptoCurrency =
savedStateHandle.get<Bundle>(AppRoute.CurrencyDetails.CRYPTO_CURRENCY_KEY)
?.unbundle(CryptoCurrency.serializer())
?: error("This screen can't open without `CryptoCurrency`")
private val userWallet: UserWallet
@ -140,13 +149,11 @@ internal class TokenDetailsViewModel @Inject constructor(
private val stateFactory = TokenDetailsStateFactory(
currentStateProvider = Provider { uiState.value },
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
stakingAvailabilityProvider = Provider {
getStakingAvailabilityUseCase.invoke(cryptoCurrency.network.id.value)
},
clickIntents = this,
symbol = cryptoCurrency.symbol,
decimals = cryptoCurrency.decimals,
featureToggles = tokenDetailsFeatureToggles,
isStakingEnabled = stakingFeatureToggles.isStakingEnabled,
)
private val exchangeStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
@ -213,7 +220,10 @@ internal class TokenDetailsViewModel @Inject constructor(
subscribeOnCurrencyStatusUpdates()
subscribeOnExchangeTransactionsUpdates()
updateTxHistory(refresh = false, showItemsLoading = true)
updateStakingInfo()
if (stakingFeatureToggles.isStakingEnabled) {
updateStakingInfo()
}
}
private fun handleBalanceHiding(owner: LifecycleOwner) {
@ -368,8 +378,12 @@ internal class TokenDetailsViewModel @Inject constructor(
}
private fun updateStakingInfo() {
viewModelScope.launch(dispatchers.main) {
val stakingAvailability = getStakingAvailabilityUseCase(cryptoCurrency.network.id.value)
viewModelScope.launch {
val stakingAvailability = getStakingAvailabilityUseCase(
cryptoCurrencyId = cryptoCurrency.id,
symbol = cryptoCurrency.symbol,
)
internalUiState.value = stateFactory.getStateWithUpdatedStakingAvailability(stakingAvailability)
if (stakingAvailability is StakingAvailability.Available) {
val stakingInfo = getStakingEntryInfoUseCase(stakingAvailability.integrationId)
internalUiState.value = stateFactory.getStateWithStaking(stakingInfo)
@ -427,6 +441,15 @@ internal class TokenDetailsViewModel @Inject constructor(
router.openTokenDetails(userWalletId = userWalletId, currency = cryptoCurrency)
}
override fun onStakeBannerClick() {
viewModelScope.launch {
val yield = getYieldUseCase.invoke(cryptoCurrency.id, cryptoCurrency.symbol).getOrNull()
yield ?: error("Staking is unavailable")
router.openStaking(userWalletId, cryptoCurrency, yield)
}
}
override fun onReloadClick() {
analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonReload(cryptoCurrency.symbol))
internalUiState.value = stateFactory.getLoadingTxHistoryState()
@ -524,6 +547,10 @@ internal class TokenDetailsViewModel @Inject constructor(
}
}
override fun onStakeClick(unavailabilityReason: ScenarioUnavailabilityReason) {
Timber.e("Not implemented yet")
}
override fun onGenerateExtendedKey() {
viewModelScope.launch(dispatchers.main) {
val extendedKey = getExtendedPublicKeyForCurrencyUseCase(
@ -767,6 +794,10 @@ internal class TokenDetailsViewModel @Inject constructor(
}
}
override fun onBalanceSelect(config: TokenBalanceSegmentedButtonConfig) {
internalUiState.value = stateFactory.getStateWithUpdatedBalanceSegmentedButtonConfig(config)
}
private fun handleUnavailabilityReason(unavailabilityReason: ScenarioUnavailabilityReason): Boolean {
if (unavailabilityReason == ScenarioUnavailabilityReason.None) return false