Updated on 2026-08-14
This commit is contained in:
parent
6446a08369
commit
820379bb78
36 changed files with 765 additions and 181 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 use
Push-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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,5 +12,8 @@ android {
|
|||
dependencies {
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.kotlin.serialization)
|
||||
|
||||
|
||||
implementation(projects.domain.tokens.models)
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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() {}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.features.staking.impl.presentation.viewmodel
|
||||
|
||||
internal interface StakingClickIntents {
|
||||
// TODO staking
|
||||
|
||||
fun onBackClick()
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -84,4 +84,6 @@ dependencies {
|
|||
/** Feature Apis */
|
||||
implementation(projects.features.tokendetails.api)
|
||||
implementation(projects.features.send.api)
|
||||
implementation(projects.features.staking.api)
|
||||
|
||||
}
|
||||
|
|
@ -283,6 +283,7 @@ internal object TokenDetailsPreviewData {
|
|||
periodInDays = 4,
|
||||
tokenSymbol = "XLM",
|
||||
iconState = iconState,
|
||||
onStakeClicked = {},
|
||||
),
|
||||
notifications = persistentListOf(),
|
||||
txHistoryState = TxHistoryState.NotSupported(
|
||||
|
|
|
|||
|
|
@ -16,5 +16,6 @@ internal sealed interface StakingBlockState {
|
|||
val interestRate: String,
|
||||
val periodInDays: Int,
|
||||
val tokenSymbol: String,
|
||||
val onStakeClicked: () -> Unit,
|
||||
) : StakingBlockState
|
||||
}
|
||||
|
|
@ -103,6 +103,7 @@ internal class TokenDetailsStateFactory(
|
|||
private val stakingStateConverter by lazy {
|
||||
TokenStakingStateConverter(
|
||||
currentStateProvider = currentStateProvider,
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -57,4 +57,6 @@ interface TokenDetailsClickIntents {
|
|||
fun onCopyAddress(): TextReference?
|
||||
|
||||
fun onAssociateClick()
|
||||
|
||||
fun onStakeBannerClick()
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue