Updated on 2026-08-14
This commit is contained in:
commit
bc84d8f5f8
579 changed files with 13604 additions and 3422 deletions
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
)
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue