Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-14 07:30:12 +03:00
commit 7a306916aa
36 changed files with 765 additions and 181 deletions

View file

@ -4,6 +4,7 @@ import com.tangem.domain.settings.*
import com.tangem.domain.staking.GetStakingAvailabilityUseCase
import com.tangem.domain.staking.GetStakingEntryInfoUseCase
import com.tangem.domain.staking.FetchStakingTokensUseCase
import com.tangem.domain.staking.GetYieldUseCase
import com.tangem.domain.staking.repositories.StakingRepository
import dagger.Module
import dagger.Provides
@ -31,6 +32,14 @@ internal object StakingDomainModule {
)
}
@Provides
@Singleton
fun provideGetYieldUseCase(stakingRepository: StakingRepository): GetYieldUseCase {
return GetYieldUseCase(
stakingRepository = stakingRepository,
)
}
@Provides
@Singleton
fun provideGetStakingTokensUseCase(stakingRepository: StakingRepository): FetchStakingTokensUseCase {

View file

@ -20,6 +20,7 @@ import com.tangem.domain.staking.FetchStakingTokensUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.features.main.model.MainScreenState
import com.tangem.tap.store
@ -45,6 +46,7 @@ internal class MainViewModel @Inject constructor(
private val sendFeatureToggles: SendFeatureToggles,
private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles,
private val dispatchers: CoroutineDispatcherProvider,
private val stakingFeatureToggles: StakingFeatureToggles,
private val fetchStakingTokensUseCase: FetchStakingTokensUseCase,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
) : ViewModel(), MainIntents {
@ -71,7 +73,10 @@ internal class MainViewModel @Inject constructor(
observeFlips()
displayBalancesHidingStatusToast()
displayHiddenBalancesModalNotification()
fetchStakingTokens()
if (stakingFeatureToggles.isStakingEnabled) {
fetchStakingTokens()
}
viewModelScope.launch(dispatchers.main) {
deleteDeprecatedLogsUseCase()

View file

@ -75,7 +75,7 @@ data class YieldDTO(
@Json(name = "website")
val website: String?,
@Json(name = "apr")
val apr: Double?,
val apr: BigDecimal?,
@Json(name = "commission")
val commission: Double?,
@Json(name = "stakedBalance")

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="add_custom_token_choose_network">Выберите сеть</string>
<string name="add_custom_token_title">Добавить токен</string>
<string name="add_tokens_title">Валюты</string>
<string name="address_qr_code_message_format">Отправляйте только %1$s (%2$s) в сети %3$s на этот адрес. Использование другой сети может привести к утрате средств.</string>
@ -300,6 +301,7 @@
<item quantity="other">%1$d из %2$d кошельков</item>
</plurals>
<string name="manage_tokens_search_placeholder">например Bitcoin</string>
<string name="manage_tokens_toast_portfolio_updated">Ваш портфель был обновлен</string>
<string name="manage_tokens_unavailable_description">Выбранный токен не доступен в кошельке на данный момент. Но не переживайте, вы можете выразить свой интерес проголосовав за его добавление.</string>
<string name="manage_tokens_unavailable_vote">Голосовать</string>
<string name="manage_tokens_wallet_selector_title">Выберите кошелек</string>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="add_custom_token_choose_network">Choose network</string>
<string name="add_custom_token_title">Add custom token</string>
<string name="add_tokens_title">Manage tokens</string>
<string name="address_qr_code_message_format">Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds.</string>
@ -64,6 +65,7 @@
<string name="cardano_max_amount_has_token_title">Not enough ADA</string>
<string name="common_accept">Accept</string>
<string name="common_access_denied">Access denied</string>
<string name="common_allow">Allow</string>
<string name="common_apply">Apply</string>
<string name="common_approval">Approval</string>
<string name="common_attention">Attention</string>
@ -111,6 +113,7 @@
<string name="common_origin_card">Primary Card</string>
<string name="common_passphrase">Passphrase</string>
<string name="common_paste">Paste</string>
<string name="common_percent_range" formatted="false">%1$s-%2$s</string>
<string name="common_read_more">Read more</string>
<string name="common_receive">Receive</string>
<string name="common_reject">Reject</string>
@ -300,10 +303,13 @@
<item quantity="other">%1$d of %2$d wallets</item>
</plurals>
<string name="manage_tokens_search_placeholder">e.g. BTC I trust, hodl I must</string>
<string name="manage_tokens_toast_portfolio_updated">Your portfolio has been updated</string>
<string name="manage_tokens_unavailable_description">The selected token is currently unavailable for actions within the crypto wallet. But worry not, you can express your interest by upvoting it.</string>
<string name="manage_tokens_unavailable_vote">Upvote</string>
<string name="manage_tokens_wallet_selector_title">Choose wallet</string>
<string name="manage_tokens_wallet_support_only_one_network_title">The wallet doesn\'t support more than one network</string>
<string name="markets_common_title">Market</string>
<string name="markets_sort_by_title">Sort By</string>
<string name="onboarding_access_code_feature_1_description">You have to set up a single access code to protect all your cards</string>
<string name="onboarding_access_code_feature_1_title">Protect</string>
<string name="onboarding_access_code_feature_2_description">You can set up an individual access code on each card later</string>
@ -536,9 +542,13 @@
<string name="settings_forget_wallet">Forget wallet</string>
<string name="settings_forget_wallet_footer">This will remove the wallet from the application. The wallet itself can be added again.</string>
<string name="settings_wallet_name_title">Name</string>
<string name="staking_details_apr">APR</string>
<string name="staking_details_apy">APY</string>
<string name="staking_details_available">Available</string>
<string name="staking_details_average_reward_rate">Average Reward Rate</string>
<string name="staking_details_estimated_profit">%s est. profit</string>
<string name="staking_details_market_rating">Market rating</string>
<string name="staking_details_metrics_block_header">Metrics</string>
<string name="staking_details_minimum_requirement">Minimum Requirement</string>
<string name="staking_details_no_rewards_to_claim">No rewards to claim</string>
<string name="staking_details_on_stake">On stake</string>
@ -627,6 +637,9 @@
<string name="twins_recreate_warning">This action is irreversible. You will not have access to the old wallet.</string>
<string name="twins_scan_twin_with_number">Tap the twin card with number %s and do not remove until the end of the operation</string>
<string name="unlock_wallet_description_full">Use %s or scan a card to have an access to your wallet</string>
<string name="user_push_notification_agreement_argument_one">Stay up to date with the latest features and news</string>
<string name="user_push_notification_agreement_argument_two">Be able to get important market notifications</string>
<string name="user_push_notification_agreement_header">Would you like to usePush-notifications?</string>
<string name="user_wallet_list_add_button">Add new wallet</string>
<string name="user_wallet_list_delete_prompt">Are you sure you want to delete this wallet?</string>
<string name="user_wallet_list_error_unable_to_unlock">An error has occurred, please scan your card to log in</string>

View file

@ -0,0 +1,147 @@
package com.tangem.core.ui.components.rows
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.R
@Suppress("LongParameterList")
@Composable
fun RoundableCornersRow(
startText: String,
startTextColor: Color,
startTextStyle: TextStyle,
endText: String,
endTextColor: Color,
endTextStyle: TextStyle,
cornersToRound: CornersToRound,
iconResId: Int? = null,
) {
Surface(
shape = cornersToRound.getShape(),
color = TangemTheme.colors.background.primary,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(TangemTheme.dimens.size48)
.padding(
horizontal = TangemTheme.dimens.spacing16,
vertical = TangemTheme.dimens.spacing12,
),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = startText,
color = startTextColor,
maxLines = 1,
style = startTextStyle,
)
if (iconResId != null) {
Icon(
modifier = Modifier
.padding(TangemTheme.dimens.spacing4)
.size(TangemTheme.dimens.size16),
painter = painterResource(id = R.drawable.ic_alert_24),
contentDescription = null,
tint = TangemTheme.colors.text.tertiary,
)
}
Spacer(modifier = Modifier.weight(1f))
Text(
text = endText,
color = endTextColor,
maxLines = 1,
style = endTextStyle,
)
}
}
}
enum class CornersToRound {
ALL_4,
TOP_2,
BOTTOM_2,
ZERO,
;
@Suppress("TopLevelComposableFunctions")
@Composable
fun getShape(): RoundedCornerShape {
val radius = TangemTheme.dimens.radius12
return when (this) {
ALL_4 -> RoundedCornerShape(radius)
TOP_2 -> RoundedCornerShape(topStart = radius, topEnd = radius)
BOTTOM_2 -> RoundedCornerShape(bottomStart = radius, bottomEnd = radius)
ZERO -> RoundedCornerShape(0.dp)
}
}
}
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_RoundableCornersRow(
@PreviewParameter(RoundableCornersRowDataProvider::class) previewData: RoundableCornersRowPreviewData,
) {
TangemThemePreview {
Box(modifier = Modifier.background(color = TangemTheme.colors.icon.attention)) {
RoundableCornersRow(
startText = previewData.startText,
startTextColor = TangemTheme.colors.text.tertiary,
startTextStyle = TangemTheme.typography.subtitle2,
endText = previewData.endText,
endTextColor = TangemTheme.colors.text.primary1,
endTextStyle = TangemTheme.typography.subtitle2,
cornersToRound = previewData.cornersToRound,
iconResId = previewData.iconResId,
)
}
}
}
private data class RoundableCornersRowPreviewData(
val startText: String,
val endText: String,
val cornersToRound: CornersToRound,
val iconResId: Int? = null,
)
private class RoundableCornersRowDataProvider :
PreviewParameterProvider<RoundableCornersRowPreviewData> {
override val values: Sequence<RoundableCornersRowPreviewData>
get() = sequenceOf(
getPreviewData(cornersToRound = CornersToRound.ZERO),
getPreviewData(cornersToRound = CornersToRound.TOP_2),
getPreviewData(cornersToRound = CornersToRound.BOTTOM_2),
getPreviewData(cornersToRound = CornersToRound.ZERO, iconResId = R.drawable.ic_alert_24),
getPreviewData(cornersToRound = CornersToRound.TOP_2, iconResId = R.drawable.ic_alert_24),
getPreviewData(cornersToRound = CornersToRound.BOTTOM_2, iconResId = R.drawable.ic_alert_24),
)
private fun getPreviewData(cornersToRound: CornersToRound, iconResId: Int? = null) = RoundableCornersRowPreviewData(
startText = "startText",
endText = "endText",
cornersToRound = cornersToRound,
iconResId = iconResId,
)
}

View file

@ -16,7 +16,6 @@ dependencies {
implementation(projects.core.utils)
implementation(projects.domain.tokens.models)
implementation(projects.domain.staking)
implementation(projects.features.staking.api)
// region DI

View file

@ -11,28 +11,62 @@ import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.model.Yield
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
internal class DefaultStakingRepository(
private val stakeKitApi: StakeKitApi,
private val stakingFeatureToggles: StakingFeatureToggles,
private val stakingYieldsStore: StakingYieldsStore,
private val dispatchers: CoroutineDispatcherProvider,
) : StakingRepository {
private val yieldConverter = YieldConverter()
override fun isStakingSupported(currencyId: String): Boolean {
return integrationIds.contains(currencyId)
}
override suspend fun fetchEnabledYields() {
withContext(dispatchers.io) {
val stakingTokensWithYields = stakeKitApi.getMultipleYields().getOrThrow()
stakingYieldsStore.store(stakingTokensWithYields.data)
}
}
override suspend fun getYield(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Yield {
return withContext(dispatchers.io) {
val yields = getEnabledYields() ?: error("No yields found")
val rawCurrencyId = cryptoCurrencyId.rawCurrencyId ?: error("Staking custom tokens is not available")
val prefetchedYield = findPrefetchedYield(
yields = yields,
currencyId = rawCurrencyId,
symbol = symbol,
)
prefetchedYield ?: error("Staking is unavailable")
}
}
override suspend fun getEntryInfo(integrationId: String): StakingEntryInfo {
return withContext(dispatchers.io) {
val yield = stakeKitApi.getSingleYield(integrationId).getOrThrow()
StakingEntryInfo(
interestRate = yield.apy,
periodInDays = yield.metadata.cooldownPeriod.days,
tokenSymbol = yield.token.symbol,
)
}
}
override suspend fun getStakingAvailabilityForActions(
cryptoCurrencyId: CryptoCurrency.ID,
symbol: String,
): StakingAvailability {
val rawCurrencyId = cryptoCurrencyId.rawCurrencyId ?: return StakingAvailability.Unavailable
if (!stakingFeatureToggles.isStakingEnabled) {
return StakingAvailability.Unavailable
}
return withContext(dispatchers.io) {
val yields = getEnabledYields() ?: return@withContext StakingAvailability.Unavailable
@ -56,34 +90,6 @@ internal class DefaultStakingRepository(
.find { it.token.coinGeckoId == currencyId && it.token.symbol == symbol }
}
override fun isStakingSupported(currencyId: String): Boolean {
return integrationIds.contains(currencyId)
}
override suspend fun getEntryInfo(integrationId: String): StakingEntryInfo {
return withContext(dispatchers.io) {
val yield = stakeKitApi.getSingleYield(integrationId).getOrThrow()
StakingEntryInfo(
interestRate = yield.apy,
periodInDays = yield.metadata.cooldownPeriod.days,
tokenSymbol = yield.token.symbol,
)
}
}
override suspend fun fetchEnabledYields() {
if (!stakingFeatureToggles.isStakingEnabled) {
return
}
withContext(dispatchers.io) {
val stakingTokensWithYields = stakeKitApi.getMultipleYields().getOrThrow()
stakingYieldsStore.store(stakingTokensWithYields.data)
}
}
private suspend fun getEnabledYields(): List<Yield>? {
val yields = stakingYieldsStore.getSyncOrNull() ?: return null
return yields.map { yieldConverter.convert(it) }

View file

@ -4,7 +4,6 @@ import com.tangem.data.staking.DefaultStakingRepository
import com.tangem.datasource.api.stakekit.StakeKitApi
import com.tangem.datasource.local.token.StakingYieldsStore
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -20,13 +19,11 @@ internal object StakingDataModule {
@Singleton
fun provideStakingRepository(
stakeKitApi: StakeKitApi,
stakingFeatureToggles: StakingFeatureToggles,
stakingTokenStore: StakingYieldsStore,
dispatchers: CoroutineDispatcherProvider,
): StakingRepository {
return DefaultStakingRepository(
stakeKitApi = stakeKitApi,
stakingFeatureToggles = stakingFeatureToggles,
stakingYieldsStore = stakingTokenStore,
dispatchers = dispatchers,
)

View file

@ -12,5 +12,8 @@ android {
dependencies {
implementation(deps.kotlin.coroutines)
implementation(deps.arrow.core)
implementation(deps.kotlin.serialization)
implementation(projects.domain.tokens.models)
}

View file

@ -0,0 +1,16 @@
package com.tangem.domain.staking
import arrow.core.Either
import com.tangem.domain.staking.model.Yield
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.CryptoCurrency
/**
* Use case for getting staking yield for staking scenario start.
*/
class GetYieldUseCase(private val stakingRepository: StakingRepository) {
suspend operator fun invoke(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Either<Throwable, Yield> {
return Either.catch { stakingRepository.getYield(cryptoCurrencyId, symbol) }
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.domain.staking.model
import java.io.Serializable
import java.math.BigDecimal
data class Yield(
@ -14,24 +15,24 @@ data class Yield(
val metadata: Metadata,
val validators: List<Validator>,
val isAvailable: Boolean,
) {
) : Serializable {
data class Status(
val enter: Boolean,
val exit: Boolean?,
)
) : Serializable
data class Args(
val enter: Enter,
val exit: Enter?,
) {
) : Serializable {
data class Enter(
val addresses: Addresses,
val args: Map<String, AddressArgument>,
) {
) : Serializable {
data class Addresses(
val address: AddressArgument,
val additionalAddresses: Map<String, AddressArgument>? = null,
)
) : Serializable
}
}
@ -41,12 +42,12 @@ data class Yield(
val name: String,
val image: String?,
val website: String?,
val apr: Double?,
val apr: BigDecimal?,
val commission: Double?,
val stakedBalance: String?,
val votingPower: Double?,
val preferred: Boolean,
)
) : Serializable
data class Metadata(
val name: String,
@ -66,14 +67,14 @@ data class Yield(
val supportsMultipleValidators: Boolean,
val revshare: Enabled,
val fee: Enabled,
) {
) : Serializable {
data class Period(
val days: Int,
)
) : Serializable
data class Enabled(
val enabled: Boolean,
)
) : Serializable
}
enum class RewardType {
@ -92,7 +93,7 @@ data class Token(
val coinGeckoId: String?,
val logoURI: String?,
val isPoints: Boolean?,
) {
) : Serializable {
enum class NetworkType {
AVALANCHE_C,
AVALANCHE_ATOMIC,
@ -169,4 +170,4 @@ data class AddressArgument(
val network: String? = null,
val minimum: Double? = null,
val maximum: Double? = null,
)
) : Serializable

View file

@ -2,18 +2,21 @@ package com.tangem.domain.staking.repositories
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.model.Yield
import com.tangem.domain.tokens.model.CryptoCurrency
interface StakingRepository {
fun isStakingSupported(currencyId: String): Boolean
suspend fun fetchEnabledYields()
suspend fun getEntryInfo(integrationId: String): StakingEntryInfo
suspend fun getYield(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Yield
suspend fun getStakingAvailabilityForActions(
cryptoCurrencyId: CryptoCurrency.ID,
symbol: String,
): StakingAvailability
suspend fun getEntryInfo(integrationId: String): StakingEntryInfo
suspend fun fetchEnabledYields()
fun isStakingSupported(currencyId: String): Boolean
}

View file

@ -12,12 +12,12 @@ android {
dependencies {
/** Core modules */
implementation(project(":core:analytics"))
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(project(":core:res"))
implementation(project(":core:utils"))
implementation(project(":core:ui"))
implementation(project(":libs:crypto"))
implementation(projects.core.res)
implementation(projects.core.utils)
implementation(projects.core.ui)
implementation(projects.libs.crypto)
/** AndroidX */
implementation(deps.androidx.appCompat)
@ -30,7 +30,7 @@ dependencies {
implementation(deps.compose.ui.tooling)
/** Domain */
implementation(project(":features:referral:domain"))
implementation(projects.features.referral.domain)
/** Other libraries */
implementation(deps.compose.shimmer)

View file

@ -1,72 +0,0 @@
package com.tangem.feature.referral.ui
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@Suppress("LongParameterList")
@Composable
internal fun AwardText(
startText: String,
startTextColor: Color,
startTextStyle: TextStyle,
endText: String,
endTextColor: Color,
endTextStyle: TextStyle,
cornersToRound: CornersToRound,
) {
Surface(
shape = cornersToRound.getShape(),
color = TangemTheme.colors.background.primary,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(TangemTheme.dimens.size48)
.padding(
horizontal = TangemTheme.dimens.spacing16,
vertical = TangemTheme.dimens.spacing12,
),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = startText,
color = startTextColor,
maxLines = 1,
style = startTextStyle,
)
Text(
text = endText,
color = endTextColor,
maxLines = 1,
style = endTextStyle,
)
}
}
}
@Preview(widthDp = 360, showBackground = true)
@Composable
private fun Preview_AwardItem() {
TangemThemePreview {
AwardText(
startText = "startText",
startTextColor = TangemTheme.colors.text.tertiary,
startTextStyle = TangemTheme.typography.subtitle2,
endText = "endText",
endTextColor = TangemTheme.colors.text.primary1,
endTextStyle = TangemTheme.typography.subtitle2,
cornersToRound = CornersToRound.TOP_2,
)
}
}

View file

@ -1,27 +0,0 @@
package com.tangem.feature.referral.ui
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
internal enum class CornersToRound {
ALL_4,
TOP_2,
BOTTOM_2,
ZERO,
;
@Suppress("TopLevelComposableFunctions")
@Composable
fun getShape(): RoundedCornerShape {
val radius = TangemTheme.dimens.radius12
return when (this) {
ALL_4 -> RoundedCornerShape(radius)
TOP_2 -> RoundedCornerShape(topStart = radius, topEnd = radius)
BOTTOM_2 -> RoundedCornerShape(bottomStart = radius, bottomEnd = radius)
ZERO -> RoundedCornerShape(0.dp)
}
}
}

View file

@ -26,6 +26,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.core.content.ContextCompat.startActivity
import com.tangem.core.ui.components.PrimaryButtonIconStart
import com.tangem.core.ui.components.rows.CornersToRound
import com.tangem.core.ui.components.rows.RoundableCornersRow
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.referral.domain.models.ExpectedAward
@ -82,7 +84,7 @@ private fun CounterAndAwards(purchasedWalletCount: Int, expectedAwards: Expected
private fun Counter(purchasedWalletCount: Int, expectedAwards: ExpectedAwards?) {
val isExpectedAwardsPresent = expectedAwards != null
AwardText(
RoundableCornersRow(
startText = stringResource(id = R.string.referral_friends_bought_title),
startTextColor = TangemTheme.colors.text.tertiary,
startTextStyle = TangemTheme.typography.subtitle2,
@ -111,7 +113,7 @@ private fun Awards(expectedAwards: ExpectedAwards) {
thickness = TangemTheme.dimens.size0_5,
color = TangemTheme.colors.stroke.primary,
)
AwardText(
RoundableCornersRow(
startText = if (expectedAwards.expectedAwards.isNotEmpty()) {
stringResource(id = R.string.referral_expected_awards)
} else {
@ -137,7 +139,7 @@ private fun Awards(expectedAwards: ExpectedAwards) {
val extraItems = expectedAwards.expectedAwards.drop(elementsCountToShowInLessMode)
initialItems.forEachIndexed { index, expectedAward ->
AwardText(
RoundableCornersRow(
startText = expectedAward.paymentDate,
startTextColor = TangemTheme.colors.text.primary1,
startTextStyle = TangemTheme.typography.subtitle2,
@ -218,7 +220,7 @@ private fun LessMoreButton(isExpanded: MutableState<Boolean>) {
private fun ExtraItems(extraItems: List<ExpectedAward>) {
Column {
extraItems.forEach { expectedAward ->
AwardText(
RoundableCornersRow(
startText = expectedAward.paymentDate,
startTextColor = TangemTheme.colors.text.primary1,
startTextStyle = TangemTheme.typography.subtitle2,

View file

@ -7,7 +7,8 @@ interface StakingRouter {
fun getEntryFragment(): Fragment
companion object {
const val CRYPTO_CURRENCY_KEY = "CRYPTO_CURRENCY_KEY"
const val CRYPTO_CURRENCY_ID_KEY = "CRYPTO_CURRENCY_ID_KEY"
const val USER_WALLET_ID_KEY = "USER_WALLET_ID_KEY"
const val YIELD_KEY = "YIELD_KEY"
}
}

View file

@ -53,6 +53,7 @@ dependencies {
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.staking)
implementation(projects.domain.balanceHiding)
implementation(projects.domain.balanceHiding.models)

View file

@ -13,6 +13,7 @@ import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.features.staking.api.navigation.StakingRouter
import com.tangem.features.staking.impl.navigation.InnerStakingRouter
import com.tangem.features.staking.impl.presentation.state.StakingStateRouter
import com.tangem.features.staking.impl.presentation.ui.StakingScreen
import com.tangem.features.staking.impl.presentation.viewmodel.StakingViewModel
import dagger.hilt.android.AndroidEntryPoint
import java.lang.ref.WeakReference
@ -57,8 +58,8 @@ internal class StakingFragment : ComposeFragment() {
SystemBarsEffect {
setSystemBarsColor(systemBarsColor)
}
val currentState = viewModel.stakingStateRouter.currentState.collectAsStateWithLifecycle()
// StakingScreen(viewModel.uiState, currentState.value)
val currentState = viewModel.uiState.collectAsStateWithLifecycle()
StakingScreen(currentState.value)
}
override fun onDestroy() {

View file

@ -1,8 +1,8 @@
package com.tangem.features.staking.impl.presentation.state
import com.tangem.core.ui.event.consumedEvent
import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub
import com.tangem.features.staking.impl.presentation.state.transformers.StakingScreenStateTransformer
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -33,7 +33,7 @@ internal class StakingStateController @Inject constructor() {
private fun getInitialState(): StakingUiState {
return StakingUiState(
clickIntents = object : StakingClickIntents {},
clickIntents = StakingClickIntentsStub,
cryptoCurrencyName = "",
currentScreen = StakingUiStateType.InitialInfo,
initialInfoState = StakingStates.InitialInfoState.Empty(),

View file

@ -2,6 +2,7 @@ package com.tangem.features.staking.impl.presentation.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
/**
@ -41,6 +42,14 @@ internal sealed class StakingStates {
sealed class InitialInfoState : StakingStates() {
data class Data(
override val isPrimaryButtonEnabled: Boolean,
val available: String,
val onStake: String,
val aprRange: TextReference,
val unbondingPeriod: String,
val minimumRequirement: String,
val rewardClaiming: String,
val warmupPeriod: String,
val rewardSchedule: String,
) : InitialInfoState()
data class Empty(

View file

@ -0,0 +1,8 @@
package com.tangem.features.staking.impl.presentation.state.stub
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
object StakingClickIntentsStub : StakingClickIntents {
override fun onBackClick() {}
}

View file

@ -0,0 +1,77 @@
package com.tangem.features.staking.impl.presentation.state.transformers
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.utils.BigDecimalFormatter
import com.tangem.domain.staking.model.Yield
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.features.staking.impl.presentation.state.StakingUiStateType
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
import com.tangem.utils.Provider
import java.math.BigDecimal
internal class SetInitialDataStateTransformer(
private val clickIntents: StakingClickIntents,
private val yield: Yield,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
) : StakingScreenStateTransformer {
override fun transform(prevState: StakingUiState): StakingUiState {
return prevState.copy(
clickIntents = clickIntents,
currentScreen = StakingUiStateType.InitialInfo,
initialInfoState = createInitialInfoState(),
)
}
private fun createInitialInfoState(): StakingStates.InitialInfoState.Data {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
return StakingStates.InitialInfoState.Data(
isPrimaryButtonEnabled = true,
available = BigDecimalFormatter.formatCryptoAmount(
cryptoCurrencyStatus.value.amount,
cryptoCurrency = cryptoCurrencyStatus.currency.symbol,
decimals = cryptoCurrencyStatus.currency.decimals,
),
onStake = "0 SOL", // TODO staking add after adding /balances request
aprRange = getAprRange(),
unbondingPeriod = yield.metadata.cooldownPeriod.days.toString(),
minimumRequirement = yield.metadata.minimumStake.toString(),
rewardClaiming = yield.metadata.rewardClaiming,
warmupPeriod = yield.metadata.warmupPeriod.days.toString(),
rewardSchedule = yield.metadata.rewardSchedule,
)
}
private fun getAprRange(): TextReference {
val aprValues = yield.validators.mapNotNull { it.apr }
val minApr = aprValues.min()
val maxApr = aprValues.max()
val formattedMinApr = BigDecimalFormatter.formatPercent(
percent = minApr,
useAbsoluteValue = true,
)
val formattedMaxApr = BigDecimalFormatter.formatPercent(
percent = maxApr,
useAbsoluteValue = true,
)
if (maxApr - minApr < EQUALITY_THRESHOLD) {
return stringReference("$formattedMinApr%")
}
return resourceReference(R.string.common_percent_range, wrappedList(formattedMinApr, formattedMaxApr))
}
companion object {
private val EQUALITY_THRESHOLD = BigDecimal(1E-10)
}
}

View file

@ -0,0 +1,200 @@
package com.tangem.features.staking.impl.presentation.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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 androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.rows.CornersToRound
import com.tangem.core.ui.components.rows.RoundableCornersRow
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.StakingStates
@Composable
internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState) {
if (state !is StakingStates.InitialInfoState.Data) return
Column(
modifier = Modifier // Do not put fillMaxSize() in here
.background(TangemTheme.colors.background.tertiary)
.padding(TangemTheme.dimens.spacing16),
) {
MetricsBlock(state)
Spacer(modifier = Modifier.height(8.dp))
StakingDetailsRows(state)
Spacer(modifier = Modifier.height(8.dp))
PrimaryButton(
text = stringResource(id = R.string.common_stake),
modifier = Modifier.fillMaxWidth(),
onClick = {
// TODO staking
},
)
}
}
@Composable
private fun MetricsBlock(state: StakingStates.InitialInfoState.Data) {
Column(
modifier = Modifier
.background(
color = TangemTheme.colors.background.primary,
shape = RoundedCornerShape(TangemTheme.dimens.radius12),
)
.padding(16.dp)
.fillMaxWidth(),
) {
Text(
text = stringResource(id = R.string.staking_details_metrics_block_header),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Column(modifier = Modifier.weight(1F)) {
Text(
text = stringResource(id = R.string.staking_details_apr),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
Text(
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
text = state.aprRange.resolveReference(),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.accent,
)
}
Column(modifier = Modifier.weight(1F)) {
Row {
Text(
modifier = Modifier.padding(end = TangemTheme.dimens.spacing4),
text = stringResource(id = R.string.staking_details_market_rating),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
Icon(
modifier = Modifier
.size(TangemTheme.dimens.size16)
.align(Alignment.CenterVertically),
painter = painterResource(id = R.drawable.ic_alert_24),
contentDescription = null,
tint = TangemTheme.colors.text.tertiary,
)
}
Text(
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
text = "1", // TODO staking
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.accent,
)
}
}
}
}
@Composable
internal fun StakingDetailsRows(state: StakingStates.InitialInfoState.Data) {
InitialInfoContentRow(
startText = stringResource(id = R.string.staking_details_available),
endText = state.available,
cornersToRound = CornersToRound.TOP_2,
)
InitialInfoContentRow(
startText = stringResource(id = R.string.staking_details_on_stake),
endText = state.onStake,
cornersToRound = CornersToRound.ZERO,
)
InitialInfoContentRow(
startText = stringResource(id = R.string.staking_details_apy),
endText = state.aprRange.resolveReference(),
cornersToRound = CornersToRound.ZERO,
)
InitialInfoContentRow(
startText = stringResource(id = R.string.staking_details_unbonding_period),
endText = state.unbondingPeriod,
cornersToRound = CornersToRound.ZERO,
)
InitialInfoContentRow(
startText = stringResource(id = R.string.staking_details_minimum_requirement),
endText = state.minimumRequirement,
cornersToRound = CornersToRound.ZERO,
)
InitialInfoContentRow(
startText = stringResource(id = R.string.staking_details_reward_claiming),
endText = state.rewardClaiming,
cornersToRound = CornersToRound.ZERO,
)
InitialInfoContentRow(
startText = stringResource(id = R.string.staking_details_warmup_period),
endText = state.warmupPeriod,
cornersToRound = CornersToRound.ZERO,
)
InitialInfoContentRow(
startText = stringResource(id = R.string.staking_details_reward_schedule),
endText = state.rewardSchedule,
cornersToRound = CornersToRound.BOTTOM_2,
)
}
@Composable
private fun InitialInfoContentRow(startText: String, endText: String, cornersToRound: CornersToRound) {
RoundableCornersRow(
startText = startText,
startTextColor = TangemTheme.colors.text.primary1,
startTextStyle = TangemTheme.typography.body2,
endText = endText,
endTextColor = TangemTheme.colors.text.tertiary,
endTextStyle = TangemTheme.typography.body2,
cornersToRound = cornersToRound,
iconResId = null, // TODO staking add bottom sheets when text will be available
)
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun StakingInitialInfoContent_Preview(
@PreviewParameter(StakingInitialInfoContentPreviewProvider::class) feeState: StakingStates.InitialInfoState.Data,
) {
TangemThemePreview {
StakingInitialInfoContent(
state = feeState,
)
}
}
private class StakingInitialInfoContentPreviewProvider : PreviewParameterProvider<StakingStates.InitialInfoState.Data> {
override val values: Sequence<StakingStates.InitialInfoState.Data>
get() = sequenceOf(
StakingStates.InitialInfoState.Data(
isPrimaryButtonEnabled = false,
available = "15 SOL",
onStake = "0 SOL",
aprRange = stringReference("2.54-5.12%"),
unbondingPeriod = "3d",
minimumRequirement = "12 SOL",
rewardClaiming = "Auto",
warmupPeriod = "Days",
rewardSchedule = "Block",
),
)
}
// endregion

View file

@ -0,0 +1,125 @@
package com.tangem.features.staking.impl.presentation.ui
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.tween
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.features.staking.impl.presentation.state.StakingUiStateType
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.withIndex
@Composable
internal fun StakingScreen(uiState: StakingUiState) {
BackHandler(onBack = uiState.clickIntents::onBackClick)
Column(
modifier = Modifier
.fillMaxSize()
.imePadding()
.systemBarsPadding()
.background(color = TangemTheme.colors.background.tertiary),
horizontalAlignment = Alignment.CenterHorizontally,
) {
SendAppBar(
uiState = uiState,
)
StakingScreenContent(
uiState = uiState,
modifier = Modifier.weight(1f),
)
}
}
@Composable
private fun SendAppBar(uiState: StakingUiState) {
val titleRes = when (uiState.currentScreen) {
StakingUiStateType.InitialInfo -> stringResource(id = R.string.common_stake)
StakingUiStateType.Amount -> stringResource(id = R.string.send_amount_label)
StakingUiStateType.ValidatorAndFee -> stringResource(id = R.string.common_stake)
StakingUiStateType.Confirm -> ""
}
val backIcon = when (uiState.currentScreen) {
StakingUiStateType.Amount,
StakingUiStateType.ValidatorAndFee,
StakingUiStateType.Confirm,
-> {
R.drawable.ic_close_24
}
StakingUiStateType.InitialInfo -> {
R.drawable.ic_back_24
}
}
AppBarWithBackButtonAndIcon(
text = titleRes,
backIconRes = backIcon,
onBackClick = uiState.clickIntents::onBackClick,
backgroundColor = TangemTheme.colors.background.tertiary,
modifier = Modifier.height(TangemTheme.dimens.size56),
)
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = Modifier) {
val currentScreen = uiState.currentScreen
var currentStateProxy by remember { mutableStateOf(currentScreen) }
var isTransitionAnimationRunning by remember { mutableStateOf(false) }
// Prevent quick screen changes to avoid some of the transition animation distortions
LaunchedEffect(currentScreen) {
snapshotFlow { isTransitionAnimationRunning }
.withIndex()
.map { (index, running) ->
if (running && index != 0) {
delay(timeMillis = 200)
}
running
}
.first { !it }
currentStateProxy = currentScreen
}
// Restrict pressing the back button while screen transition is running to avoid most of the animation distortions
BackHandler(enabled = isTransitionAnimationRunning) {}
// Box is needed to fix animation with resizing of AnimatedContent
Box(modifier = modifier.fillMaxSize()) {
AnimatedContent(
targetState = currentStateProxy,
contentAlignment = Alignment.TopCenter,
label = "Staking Screen Navigation",
transitionSpec = {
val direction = if (initialState.ordinal < targetState.ordinal) {
AnimatedContentTransitionScope.SlideDirection.Start
} else {
AnimatedContentTransitionScope.SlideDirection.End
}
slideIntoContainer(towards = direction, animationSpec = tween())
.togetherWith(slideOutOfContainer(towards = direction, animationSpec = tween()))
},
) { state ->
isTransitionAnimationRunning = transition.targetState != transition.currentState
when (state) {
StakingUiStateType.InitialInfo -> StakingInitialInfoContent(
state = uiState.initialInfoState,
)
else -> Unit
}
}
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.features.staking.impl.presentation.viewmodel
internal interface StakingClickIntents {
// TODO staking
fun onBackClick()
}

View file

@ -5,6 +5,7 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.staking.model.Yield
import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
@ -15,6 +16,8 @@ import com.tangem.features.staking.impl.presentation.state.StakingStateControlle
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.features.staking.impl.presentation.state.StakingStateRouter
import com.tangem.features.staking.impl.presentation.state.transformers.HideBalanceStateTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.SetInitialDataStateTransformer
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.*
@ -37,12 +40,15 @@ internal class StakingViewModel @Inject constructor(
var stakingStateRouter: StakingStateRouter by Delegates.notNull()
private set
private val cryptoCurrency: CryptoCurrency = savedStateHandle[StakingRouter.CRYPTO_CURRENCY_KEY]
?: error("This screen can't open without `CryptoCurrency`")
private val cryptoCurrencyId: CryptoCurrency.ID = savedStateHandle[StakingRouter.CRYPTO_CURRENCY_ID_KEY]
?: error("This screen can't be opened without `CryptoCurrency.ID`")
private val userWalletId: UserWalletId = savedStateHandle.get<String>(StakingRouter.USER_WALLET_ID_KEY)
?.let { stringValue -> UserWalletId(stringValue) }
?: error("This screen can't open without `UserWalletId`")
?: error("This screen can't be opened without `UserWalletId`")
private val yield: Yield = savedStateHandle[StakingRouter.YIELD_KEY]
?: error("This screen can't be opened without `Yield`")
private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
@ -53,16 +59,27 @@ internal class StakingViewModel @Inject constructor(
subscribeOnCurrencyStatusUpdates()
}
fun setRouter(router: InnerStakingRouter, stakingStateRouter: StakingStateRouter) {
override fun onBackClick() {
stakingStateRouter.onBackClick()
}
fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) {
innerRouter = router
this.stakingStateRouter = stakingStateRouter
this.stakingStateRouter = stateRouter
}
private fun subscribeOnCurrencyStatusUpdates() {
viewModelScope.launch {
getCryptoCurrencyStatusSyncUseCase(userWalletId, cryptoCurrency.id).fold(
getCryptoCurrencyStatusSyncUseCase(userWalletId, cryptoCurrencyId).fold(
ifRight = {
cryptoCurrencyStatus = it
stateController.update(
transformer = SetInitialDataStateTransformer(
clickIntents = this@StakingViewModel,
yield = yield,
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
),
)
},
ifLeft = {
// TODO staking error

View file

@ -84,4 +84,6 @@ dependencies {
/** Feature Apis */
implementation(projects.features.tokendetails.api)
implementation(projects.features.send.api)
implementation(projects.features.staking.api)
}

View file

@ -283,6 +283,7 @@ internal object TokenDetailsPreviewData {
periodInDays = 4,
tokenSymbol = "XLM",
iconState = iconState,
onStakeClicked = {},
),
notifications = persistentListOf(),
txHistoryState = TxHistoryState.NotSupported(

View file

@ -16,5 +16,6 @@ internal sealed interface StakingBlockState {
val interestRate: String,
val periodInDays: Int,
val tokenSymbol: String,
val onStakeClicked: () -> Unit,
) : StakingBlockState
}

View file

@ -103,6 +103,7 @@ internal class TokenDetailsStateFactory(
private val stakingStateConverter by lazy {
TokenStakingStateConverter(
currentStateProvider = currentStateProvider,
clickIntents = clickIntents,
)
}

View file

@ -5,10 +5,12 @@ 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.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> {
@ -28,6 +30,7 @@ internal class TokenStakingStateConverter(
periodInDays = it.periodInDays,
tokenSymbol = it.tokenSymbol,
iconState = currentStateProvider().tokenInfoBlockState.iconState,
onStakeClicked = clickIntents::onStakeBannerClick,
)
},
)

View file

@ -121,8 +121,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,
)
}
}
@ -191,6 +191,7 @@ private class StakingBlockStateProvider : CollectionPreviewParameterProvider<Sta
interestRate = "10",
periodInDays = 4,
tokenSymbol = "SOL",
onStakeClicked = {},
),
StakingBlockState.Loading(iconState = iconState),
StakingBlockState.Error(iconState = iconState),

View file

@ -57,4 +57,6 @@ interface TokenDetailsClickIntents {
fun onCopyAddress(): TextReference?
fun onAssociateClick()
fun onStakeBannerClick()
}

View file

@ -3,6 +3,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.os.bundleOf
import androidx.lifecycle.*
import androidx.paging.cachedIn
import arrow.core.getOrElse
@ -10,6 +11,8 @@ import com.tangem.blockchain.common.address.AddressType
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.deeplink.DeepLinksRegistry
import com.tangem.core.deeplink.global.BuyCurrencyDeepLink
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel
import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels
@ -29,6 +32,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
@ -62,6 +66,8 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTrans
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.staking.api.navigation.StakingRouter
import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles
import com.tangem.features.tokendetails.impl.R
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
@ -101,7 +107,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,
@ -212,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) {
@ -428,6 +439,24 @@ 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")
reduxStateHolder.dispatch(
action = NavigationAction.NavigateTo(
screen = AppScreen.Staking,
bundle = bundleOf(
StakingRouter.USER_WALLET_ID_KEY to userWalletId.stringValue,
StakingRouter.CRYPTO_CURRENCY_ID_KEY to cryptoCurrency.id,
StakingRouter.YIELD_KEY to yield,
),
),
)
}
}
override fun onReloadClick() {
analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonReload(cryptoCurrency.symbol))
uiState = stateFactory.getLoadingTxHistoryState()