Updated on 2026-08-14

This commit is contained in:
Tangem 2024-05-31 12:21:36 +02:00
parent 590888db59
commit 682349e353
48 changed files with 860 additions and 111 deletions

View file

@ -67,6 +67,7 @@ dependencies {
implementation(projects.domain.feedback)
implementation(projects.domain.qrScanning)
implementation(projects.domain.qrScanning.models)
implementation(projects.domain.staking)
implementation(projects.common)
implementation(projects.core.analytics)
@ -99,6 +100,7 @@ dependencies {
implementation(projects.data.onboarding)
implementation(projects.data.feedback)
implementation(projects.data.qrScanning)
implementation(projects.data.staking)
/** Features */
implementation(projects.features.onboarding)

View file

@ -0,0 +1,32 @@
package com.tangem.tap.di.domain
import com.tangem.domain.settings.*
import com.tangem.domain.staking.GetStakingAvailabilityUseCase
import com.tangem.domain.staking.GetStakingEntryInfoUseCase
import com.tangem.domain.staking.repositories.StakingRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object StakingDomainModule {
@Provides
@Singleton
fun provideGetStakingEntryInfoUseCase(stakingRepository: StakingRepository): GetStakingEntryInfoUseCase {
return GetStakingEntryInfoUseCase(
stakingRepository = stakingRepository,
)
}
@Provides
@Singleton
fun provideGetStakingAvailabilityUseCase(stakingRepository: StakingRepository): GetStakingAvailabilityUseCase {
return GetStakingAvailabilityUseCase(
stakingRepository = stakingRepository,
)
}
}

View file

@ -4,6 +4,7 @@ import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.common.json.TangemSdkAdapter
import com.tangem.datasource.api.common.adapter.BigDecimalAdapter
import retrofit2.converter.moshi.MoshiConverterFactory
/**

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.common
package com.tangem.datasource.api.common.adapter
import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.common
package com.tangem.datasource.api.common.adapter
import com.squareup.moshi.*
import org.joda.time.DateTime

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.common
package com.tangem.datasource.api.common.adapter
import com.squareup.moshi.*
import org.joda.time.LocalDate

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.stakekit
package com.tangem.datasource.api.common.adapter
import com.squareup.moshi.*
import com.squareup.moshi.adapters.EnumJsonAdapter

View file

@ -1,21 +1,23 @@
package com.tangem.datasource.api.stakekit
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.stakekit.models.request.MultipleYieldBalancesRequestBody
import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody
import com.tangem.datasource.api.stakekit.models.request.RevenueOption
import com.tangem.datasource.api.stakekit.models.request.YieldType
import com.tangem.datasource.api.stakekit.models.response.EnabledYieldsResponse
import com.tangem.datasource.api.stakekit.models.response.model.TokenWithYield
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalances
import com.tangem.datasource.api.stakekit.models.response.model.Yield
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapper
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
@Suppress("LongParameterList")
interface StakeKitApi {
@GET("yields/enabled")
fun getAllYields(
suspend fun getMultipleYields(
@Query("ledgerWalletAPICompatible") ledgerWalletAPICompatible: Boolean,
@Query("type") type: YieldType,
@Query("revenueOption") revenueOption: RevenueOption,
@ -24,9 +26,23 @@ interface StakeKitApi {
@Query("limit") limit: Int,
): ApiResponse<EnabledYieldsResponse>
@GET("yields/{integrationId}")
suspend fun getSingleYield(
@Path("integrationId") integrationId: String,
@Query("ledgerWalletAPICompatible") ledgerWalletAPICompatible: Boolean = false,
): ApiResponse<Yield>
@GET("yields/balances")
fun getMultipleYieldBalances(@Body body: List<MultipleYieldBalancesRequestBody>): ApiResponse<List<YieldBalances>>
suspend fun getMultipleYieldBalances(
@Body body: List<YieldBalanceRequestBody>,
): ApiResponse<List<YieldBalanceWrapper>>
@GET("yields/{integrationId}/balances")
suspend fun getSingleYieldBalance(
@Path("integrationId") integrationId: String,
@Body body: YieldBalanceRequestBody,
): ApiResponse<YieldBalanceWrapper>
@GET("tokens")
fun getTokens(): ApiResponse<List<TokenWithYield>>
suspend fun getTokens(): ApiResponse<List<TokenWithYield>>
}

View file

@ -0,0 +1,31 @@
package com.tangem.datasource.api.stakekit.models.request
import com.squareup.moshi.Json
data class YieldBalanceRequestBody(
@Json(name = "addresses") val addresses: Address,
@Json(name = "args") val args: YieldBalanceRequestArgs,
@Json(name = "integrationId") val integrationId: String? = null,
) {
data class Address(
@Json(name = "address") val address: String,
@Json(name = "additionalAddresses") val additionalAddresses: AdditionalAddresses? = null,
@Json(name = "explorerUrl") val explorerUrl: String,
) {
data class AdditionalAddresses(
@Json(name = "cosmosPubKey") val cosmosPubKey: String? = null,
@Json(name = "binanceBeaconAddress") val binanceBeaconAddress: String? = null,
@Json(name = "stakeAccounts") val stakeAccounts: List<String>? = null,
@Json(name = "lidoStakeAccounts") val lidoStakeAccounts: List<String>? = null,
@Json(name = "tezosPubKey") val tezosPubKey: String? = null,
@Json(name = "cAddressBech") val cAddressBech: String? = null,
@Json(name = "pAddressBech") val pAddressBech: String? = null,
)
}
data class YieldBalanceRequestArgs(
@Json(name = "validatorAddresses") val validatorAddresses: List<String>,
)
}

View file

@ -12,11 +12,11 @@ data class Token(
@Json(name = "address") val address: String?,
@Json(name = "coinGeckoId") val coinGeckoId: String?,
@Json(name = "logoURI") val logoURI: String?,
@Json(name = "isPoints") val isPoints: Boolean,
@Json(name = "isPoints") val isPoints: Boolean?,
) {
enum class NetworkType {
@Json(name = "avalanche")
AVALANCHE,
@Json(name = "avalanche-c")
AVALANCHE_C,
@Json(name = "avalanche-atomic")
AVALANCHE_ATOMIC,

View file

@ -2,6 +2,7 @@ package com.tangem.datasource.api.stakekit.models.response.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class Yield(
@ -16,7 +17,7 @@ data class Yield(
@Json(name = "status")
val status: Status,
@Json(name = "apy")
val apy: Double,
val apy: BigDecimal,
@Json(name = "rewardRate")
val rewardRate: Double,
@Json(name = "rewardType")
@ -114,7 +115,7 @@ data class Yield(
@Json(name = "defaultValidator")
val defaultValidator: String?,
@Json(name = "minimumStake")
val minimumStake: Int,
val minimumStake: Int?,
@Json(name = "supportsMultipleValidators")
val supportsMultipleValidators: Boolean,
@Json(name = "revshare")

View file

@ -0,0 +1,138 @@
package com.tangem.datasource.api.stakekit.models.response.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.joda.time.DateTime
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class YieldBalanceWrapper(
@Json(name = "balances")
val balances: List<Balance>,
@Json(name = "integrationId")
val integrationId: String?,
) {
@JsonClass(generateAdapter = true)
data class Balance(
@Json(name = "groupId")
val groupId: String,
@Json(name = "type")
val type: BalanceType,
@Json(name = "amount")
val amount: BigDecimal,
@Json(name = "date")
val date: DateTime?,
@Json(name = "pricePerShare")
val pricePerShare: BigDecimal,
@Json(name = "pendingActions")
val pendingActions: List<PendingAction>,
@Json(name = "token")
val token: Token,
@Json(name = "validatorAddress")
val validatorAddress: String?,
@Json(name = "validatorAddresses")
val validatorAddresses: List<String>?,
@Json(name = "providerId")
val providerId: String?,
) {
enum class BalanceType {
@Json(name = "available")
AVAILABLE,
@Json(name = "staked")
STAKED,
@Json(name = "unstaking")
UNSTAKING,
@Json(name = "unstaked")
UNSTAKED,
@Json(name = "preparing")
PREPARING,
@Json(name = "rewards")
REWARDS,
@Json(name = "locked")
LOCKED,
@Json(name = "unlocking")
UNLOCKING,
}
@JsonClass(generateAdapter = true)
data class PendingAction(
@Json(name = "type")
val type: StakingActionType,
@Json(name = "passthrough")
val passthrough: String,
@Json(name = "args")
val args: PendingActionArgs?,
) {
@JsonClass(generateAdapter = true)
data class PendingActionArgs(
@Json(name = "amount")
val amount: Amount?,
@Json(name = "duration")
val duration: Duration?,
@Json(name = "validatorAddress")
val validatorAddress: Required?,
@Json(name = "validatorAddresses")
val validatorAddresses: Required?,
@Json(name = "nfts")
val nfts: List<Nft>?,
@Json(name = "tronResource")
val tronResource: TronResource?,
@Json(name = "signatureVerification")
val signatureVerification: Required?,
) {
@JsonClass(generateAdapter = true)
data class Amount(
@Json(name = "required")
val required: Boolean,
@Json(name = "minimum")
val minimum: BigDecimal?,
@Json(name = "maximum")
val maximum: BigDecimal?,
)
@JsonClass(generateAdapter = true)
data class Duration(
@Json(name = "required")
val required: Boolean,
@Json(name = "minimum")
val minimum: Int?,
@Json(name = "maximum")
val maximum: Int?,
)
@JsonClass(generateAdapter = true)
data class Nft(
@Json(name = "baycId")
val baycId: Required?,
@Json(name = "maycId")
val maycId: Required?,
@Json(name = "bakcId")
val bakcId: Required?,
)
@JsonClass(generateAdapter = true)
data class TronResource(
@Json(name = "required")
val required: Boolean,
@Json(name = "options")
val options: List<String>,
)
}
}
@JsonClass(generateAdapter = true)
data class Required(
@Json(name = "required")
val required: Boolean,
)
}
}

View file

@ -4,10 +4,10 @@ import com.squareup.moshi.Moshi
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.datasource.api.common.BigDecimalAdapter
import com.tangem.datasource.api.common.DateTimeAdapter
import com.tangem.datasource.api.common.LocalDateAdapter
import com.tangem.datasource.api.stakekit.UnknownEnumMoshiAdapter
import com.tangem.datasource.api.common.adapter.BigDecimalAdapter
import com.tangem.datasource.api.common.adapter.DateTimeAdapter
import com.tangem.datasource.api.common.adapter.LocalDateAdapter
import com.tangem.datasource.api.common.adapter.UnknownEnumMoshiAdapter
import com.tangem.datasource.api.stakekit.models.response.model.Token
import com.tangem.datasource.config.models.ProviderModel
import dagger.Module

View file

@ -580,6 +580,8 @@
<string name="token_details_hide_alert_message">Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами.</string>
<string name="token_details_hide_alert_title">Скрыть %s</string>
<string name="token_details_hide_token">Скрыть токен</string>
<string name="token_details_staking_block_subtitle">Стейкинг позволяет вам зарабатывать %1$s и получать вознаграждения каждые %2$s дней</string>
<string name="token_details_staking_block_title">Зарабатывайте до %s вознаграждений за стейкинг ежегодно</string>
<string name="token_details_token_type_subtitle">%1$s токен в сети %%image%% %2$s</string>
<string name="token_details_token_type_subtitle_no_standard">Токен в сети %%image%% %1$s</string>
<string name="token_details_unable_hide_alert_message">Токен %1$s (%2$s) является основной валютой в сети %3$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети</string>

View file

@ -26,7 +26,7 @@
<string name="app_settings_theme_mode_light">Light</string>
<string name="app_settings_theme_mode_system">System default</string>
<string name="app_settings_theme_selector_title">Theme</string>
<string name="app_settings_title">App Settings</string>
<string name="app_settings_title">App settings</string>
<string name="balance_hidden_description">To hide or show your balances, simply flip your device screen down, or switch it off in Settings</string>
<string name="balance_hidden_do_not_show_button">Don\'t show again</string>
<string name="balance_hidden_got_it_button">Got it</string>
@ -49,7 +49,7 @@
<string name="card_settings_change_access_code_footer">Access code will be changed on this card only</string>
<string name="card_settings_reset_card_to_factory">Reset to Factory Settings</string>
<string name="card_settings_security_mode">Security Mode</string>
<string name="card_settings_title">Card Settings</string>
<string name="card_settings_title">Card settings</string>
<string name="cardano_coin_will_be_send_with_token_description">In addition to network fee, the Cardano network charges %1$s ADA when transacting with the %2$s token</string>
<string name="cardano_coin_will_be_send_with_token_title">Cardano transaction requirements</string>
<string name="cardano_insufficient_balance_to_send_token_description">To make a %1$s transaction, you must deposit some ADA to cover the network fee and minimum ADA value (5 ADA recommended)</string>
@ -173,7 +173,7 @@
<string name="details_row_description_flip_to_hide">Flip your device screen down to quickly hide and show balances</string>
<string name="details_row_subtitle_signed_hashes_format">%s hashes</string>
<string name="details_row_title_cid">Card ID</string>
<string name="details_row_title_contact_to_support">Contact to Support</string>
<string name="details_row_title_contact_to_support">Contact support</string>
<string name="details_row_title_create_backup">Link More Cards</string>
<string name="details_row_title_currency">App Currency</string>
<string name="details_row_title_flip_to_hide">Flip-to-Hide Balances</string>
@ -181,7 +181,7 @@
<string name="details_row_title_signed_hashes">Signed</string>
<string name="details_title">Details</string>
<string name="disclaimer_error_loading">Check your internet connection or switch to a different network</string>
<string name="disclaimer_title">Terms of Service</string>
<string name="disclaimer_title">Terms of service</string>
<string name="error_wrong_wallet_tapped">You have used a card from another wallet. Tap the card associated with this wallet</string>
<string name="exchange_tokens_available_tokens_header">My tokens</string>
<string name="exchange_tokens_empty_tokens">You haven\'t added any tokens yet. Add tokens via Market to swap</string>
@ -573,6 +573,8 @@
<string name="token_details_hide_alert_message">You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page.</string>
<string name="token_details_hide_alert_title">Hide %s</string>
<string name="token_details_hide_token">Hide token</string>
<string name="token_details_staking_block_subtitle">Staking allows you to earn %1$s and get rewards every %2$s days</string>
<string name="token_details_staking_block_title">Earn up to %s staking rewards yearly</string>
<string name="token_details_token_type_subtitle">%1$s token in %%image%% %2$s network</string>
<string name="token_details_token_type_subtitle_no_standard">Token in %%image%% %1$s network</string>
<string name="token_details_unable_hide_alert_message">The %1$s (%2$s) token is the main currency on the %3$s network and cannot be hidden as long as you have other tokens on this network in the list</string>

View file

@ -4,15 +4,12 @@ import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.currency.DefaultCurrencyIcon
private const val GRAY_SCALE_SATURATION = 0f
private const val GRAY_SCALE_ALPHA = 0.4f
import com.tangem.core.ui.utils.GRAY_SCALE_ALPHA
import com.tangem.core.ui.utils.GrayscaleColorFilter
/**
* Simple icon from network
@ -46,7 +43,4 @@ fun FiatIcon(
},
modifier = modifier,
)
}
private val GrayscaleColorFilter: ColorFilter
get() = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) })
}

View file

@ -10,14 +10,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.res.TangemTheme
private const val GRAY_SCALE_SATURATION = 0f
private const val GRAY_SCALE_ALPHA = 0.4f
private const val NORMAL_ALPHA = 1f
import com.tangem.core.ui.utils.GRAY_SCALE_ALPHA
import com.tangem.core.ui.utils.GrayscaleColorFilter
import com.tangem.core.ui.utils.NORMAL_ALPHA
/**
* Cryptocurrency icon with network badge
@ -112,7 +109,4 @@ private fun BoxScope.ContentIconContainer(
@Composable
private inline fun BaseContainer(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) {
Box(modifier = modifier.size(size = TangemTheme.dimens.size40), content = content)
}
private val GrayscaleColorFilter: ColorFilter
get() = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) })
}

View file

@ -0,0 +1,11 @@
package com.tangem.core.ui.utils
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
const val GRAY_SCALE_SATURATION = 0f
const val GRAY_SCALE_ALPHA = 0.4f
const val NORMAL_ALPHA = 1f
val GrayscaleColorFilter: ColorFilter
get() = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) })

1
data/staking/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,36 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
id("configuration")
}
android {
namespace = "com.tangem.data.staking"
}
dependencies {
implementation(projects.core.datasource)
implementation(projects.core.utils)
implementation(projects.domain.staking)
implementation(projects.features.staking.api)
// region DI
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
// endregion
// region Others dependencies
implementation(deps.jodatime)
implementation(deps.kotlin.coroutines)
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.tangem.blockchain) {
exclude(module = "joda-time")
}
// endregion
}

View file

@ -0,0 +1,63 @@
package com.tangem.data.staking
import com.tangem.blockchain.common.Blockchain
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.stakekit.StakeKitApi
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
internal class DefaultStakingRepository(
private val stakeKitApi: StakeKitApi,
private val stakingFeatureToggles: StakingFeatureToggles,
) : StakingRepository {
override fun getStakingAvailability(blockchainId: String): StakingAvailability {
if (!stakingFeatureToggles.isStakingEnabled) {
return StakingAvailability.Unavailable
}
return integrationIdMap[Blockchain.fromId(blockchainId)]?.let {
StakingAvailability.Available(it)
} ?: StakingAvailability.Unavailable
}
override suspend fun getEntryInfo(integrationId: String): StakingEntryInfo {
val yield = stakeKitApi.getSingleYield(integrationId).getOrThrow()
return StakingEntryInfo(
interestRate = yield.apy,
periodInDays = yield.metadata.cooldownPeriod.days,
tokenSymbol = yield.token.symbol,
)
}
companion object {
private const val SOLANA_INTEGRATION_ID = "solana-sol-native-multivalidator-staking"
private const val COSMOS_INTEGRATION_ID = "cosmos-atom-native-staking"
private const val POLKADOT_INTEGRATION_ID = "polkadot-dot-validator-staking"
private const val ETHEREUM_INTEGRATION_ID = "ethereum-matic-native-staking"
private const val AVALANCHE_INTEGRATION_ID = "avalanche-avax-native-staking"
private const val TRON_INTEGRATION_ID = "tron-trx-native-staking"
private const val CRONOS_INTEGRATION_ID = "cronos-cro-native-staking"
private const val BINANCE_INTEGRATION_ID = "binance-bnb-native-staking"
private const val KAVA_INTEGRATION_ID = "kava-kava-native-staking"
private const val NEAR_INTEGRATION_ID = "near-near-native-staking"
private const val TEZOS_INTEGRATION_ID = "tezos-xtz-native-staking"
private val integrationIdMap = mapOf(
Blockchain.Solana to SOLANA_INTEGRATION_ID,
Blockchain.Cosmos to COSMOS_INTEGRATION_ID,
Blockchain.Polkadot to POLKADOT_INTEGRATION_ID,
Blockchain.Polygon to ETHEREUM_INTEGRATION_ID,
Blockchain.Avalanche to AVALANCHE_INTEGRATION_ID,
Blockchain.Tron to TRON_INTEGRATION_ID,
Blockchain.Cronos to CRONOS_INTEGRATION_ID,
Blockchain.Binance to BINANCE_INTEGRATION_ID,
Blockchain.Kava to KAVA_INTEGRATION_ID,
Blockchain.Near to NEAR_INTEGRATION_ID,
Blockchain.Tezos to TEZOS_INTEGRATION_ID,
)
}
}

View file

@ -0,0 +1,28 @@
package com.tangem.data.staking.di
import com.tangem.data.staking.DefaultStakingRepository
import com.tangem.datasource.api.stakekit.StakeKitApi
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object StakingDataModule {
@Provides
@Singleton
fun provideStakingRepository(
stakeKitApi: StakeKitApi,
stakingFeatureToggles: StakingFeatureToggles,
): StakingRepository {
return DefaultStakingRepository(
stakeKitApi = stakeKitApi,
stakingFeatureToggles = stakingFeatureToggles,
)
}
}

1
domain/staking/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,9 @@
plugins {
alias(deps.plugins.kotlin.jvm)
id("configuration")
}
dependencies {
implementation(deps.kotlin.coroutines)
implementation(deps.arrow.core)
}

View file

@ -0,0 +1,16 @@
package com.tangem.domain.staking
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.repositories.StakingRepository
/**
* Use case for getting info about staking availability for certain blockchain.
*/
class GetStakingAvailabilityUseCase(
private val stakingRepository: StakingRepository,
) {
operator fun invoke(blockchainNetworkId: String): StakingAvailability {
return stakingRepository.getStakingAvailability(blockchainNetworkId)
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.domain.staking
import arrow.core.Either
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.repositories.StakingRepository
/**
* Use case for getting entry info about staking on token screen.
*/
class GetStakingEntryInfoUseCase(private val stakingRepository: StakingRepository) {
suspend operator fun invoke(integrationId: String): Either<Throwable, StakingEntryInfo> {
return Either.catch { stakingRepository.getEntryInfo(integrationId) }
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.domain.staking.model
sealed class StakingAvailability {
data class Available(val integrationId: String) : StakingAvailability()
data object Unavailable : StakingAvailability()
}

View file

@ -0,0 +1,9 @@
package com.tangem.domain.staking.model
import java.math.BigDecimal
data class StakingEntryInfo(
val interestRate: BigDecimal,
val periodInDays: Int,
val tokenSymbol: String,
)

View file

@ -0,0 +1,11 @@
package com.tangem.domain.staking.repositories
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
interface StakingRepository {
fun getStakingAvailability(blockchainId: String): StakingAvailability
suspend fun getEntryInfo(integrationId: String): StakingEntryInfo
}

View file

@ -12,8 +12,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@ -29,6 +27,8 @@ 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.core.ui.utils.GRAY_SCALE_ALPHA
import com.tangem.core.ui.utils.GrayscaleColorFilter
import com.tangem.feature.swap.models.states.PercentDifference
import com.tangem.feature.swap.models.states.ProviderState
@ -38,11 +38,6 @@ import com.tangem.feature.swap.models.states.ProviderState
* https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?type=design&node-id=7856-41909&mode=design&t=vo7dyElitnzSPSW3-4
*/
private const val GRAY_SCALE_SATURATION = 0f
private const val GRAY_SCALE_ALPHA = 0.4f
private val GrayscaleColorFilter: ColorFilter
get() = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) })
@Composable
fun ProviderItemBlock(state: ProviderState, modifier: Modifier = Modifier) {
if (state !is ProviderState.Empty) {

View file

@ -1,5 +1,6 @@
package com.tangem.features.tokendetails.featuretoggles
interface TokenDetailsFeatureToggles {
fun isGenerateXPubEnabled(): Boolean
}

View file

@ -75,6 +75,7 @@ dependencies {
implementation(projects.domain.balanceHiding)
implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.transaction)
implementation(projects.domain.staking)
/** Temp dependency to swap domain */
implementation(projects.features.swap.domain)

View file

@ -6,5 +6,6 @@ import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggle
internal class DefaultTokenDetailsFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : TokenDetailsFeatureToggles {
override fun isGenerateXPubEnabled() = featureTogglesManager.isFeatureEnabled(name = "GENERATE_XPUB_ENABLED")
}

View file

@ -35,7 +35,7 @@ internal object TokenDetailsPreviewData {
val tokenInfoBlockStateWithLongNameInMainCurrency = TokenInfoBlockState(
name = "Stellar (XLM) with long name test",
iconState = TokenInfoBlockState.IconState.CoinIcon(
iconState = IconState.CoinIcon(
url = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png",
fallbackResId = R.drawable.img_stellar_22,
isGrayscale = false,
@ -44,7 +44,7 @@ internal object TokenDetailsPreviewData {
)
val tokenInfoBlockStateWithLongName = TokenInfoBlockState(
name = "Tether (USDT) with long name test",
iconState = TokenInfoBlockState.IconState.TokenIcon(
iconState = IconState.TokenIcon(
url = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png",
fallbackTint = Color.Cyan,
fallbackBackground = Color.Blue,
@ -59,7 +59,7 @@ internal object TokenDetailsPreviewData {
val tokenInfoBlockStateWithLongNameNoStandard = TokenInfoBlockState(
name = "Tether (USDT) with long name test",
iconState = TokenInfoBlockState.IconState.TokenIcon(
iconState = IconState.TokenIcon(
url = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png",
fallbackTint = Color.Cyan,
fallbackBackground = Color.Blue,
@ -74,7 +74,7 @@ internal object TokenDetailsPreviewData {
val tokenInfoBlockState = TokenInfoBlockState(
name = "Tether USDT",
iconState = TokenInfoBlockState.IconState.CustomTokenIcon(
iconState = IconState.CustomTokenIcon(
tint = Color.Green,
background = Color.Magenta,
isGrayscale = true,
@ -86,6 +86,13 @@ internal object TokenDetailsPreviewData {
),
)
val iconState = IconState.TokenIcon(
url = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png",
fallbackTint = Color.Cyan,
fallbackBackground = Color.Blue,
isGrayscale = false,
)
private val actionButtons = persistentListOf(
TokenDetailsActionButton.Buy(dimContent = false, onClick = {}),
TokenDetailsActionButton.Send(dimContent = false, onClick = {}),
@ -103,6 +110,8 @@ internal object TokenDetailsPreviewData {
private val marketPriceLoading = MarketPriceBlockState.Loading(currencySymbol = "USDT")
private val stakingLoading = StakingBlockState.Loading(iconState = iconState)
private val pullToRefreshConfig = TokenDetailsPullToRefreshConfig(
isRefreshing = false,
onRefresh = {},
@ -237,6 +246,7 @@ internal object TokenDetailsPreviewData {
tokenInfoBlockState = tokenInfoBlockState,
tokenBalanceBlockState = balanceLoading,
marketPriceBlockState = marketPriceLoading,
stakingBlockState = stakingLoading,
notifications = persistentListOf(),
txHistoryState = TxHistoryState.Content(
contentItems = MutableStateFlow(
@ -250,6 +260,7 @@ internal object TokenDetailsPreviewData {
bottomSheetConfig = null,
isBalanceHidden = false,
isMarketPriceAvailable = false,
isStakingAvailable = false,
event = consumedEvent(),
)
@ -267,6 +278,12 @@ internal object TokenDetailsPreviewData {
type = PriceChangeType.UP,
),
),
stakingBlockState = StakingBlockState.Content(
interestRate = "7.38",
periodInDays = 4,
tokenSymbol = "XLM",
iconState = iconState,
),
notifications = persistentListOf(),
txHistoryState = TxHistoryState.NotSupported(
onExploreClick = {},
@ -279,6 +296,7 @@ internal object TokenDetailsPreviewData {
bottomSheetConfig = null,
isBalanceHidden = false,
isMarketPriceAvailable = true,
isStakingAvailable = true,
event = consumedEvent(),
)

View file

@ -0,0 +1,30 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
@Immutable
internal sealed class IconState {
abstract val isGrayscale: Boolean
data class CoinIcon(
val url: String?,
@DrawableRes val fallbackResId: Int,
override val isGrayscale: Boolean,
) : IconState()
data class TokenIcon(
val url: String?,
val fallbackTint: Color,
val fallbackBackground: Color,
override val isGrayscale: Boolean,
) : IconState()
data class CustomTokenIcon(
val tint: Color,
val background: Color,
override val isGrayscale: Boolean,
) : IconState()
}

View file

@ -0,0 +1,20 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
import androidx.compose.runtime.Immutable
@Immutable
internal sealed interface StakingBlockState {
val iconState: IconState
data class Error(override val iconState: IconState) : StakingBlockState
data class Loading(override val iconState: IconState) : StakingBlockState
data class Content(
override val iconState: IconState,
val interestRate: String,
val periodInDays: Int,
val tokenSymbol: String,
) : StakingBlockState
}

View file

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

View file

@ -2,7 +2,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
internal data class TokenInfoBlockState(
val name: String,
@ -11,7 +10,7 @@ internal data class TokenInfoBlockState(
) {
@Immutable
sealed class Currency {
object Native : Currency()
data object Native : Currency()
/**
* @param standardName - token standard. Samples: ERC20, BEP20, BEP2, TRC20 and etc.
@ -24,29 +23,4 @@ internal data class TokenInfoBlockState(
@DrawableRes val networkIcon: Int,
) : Currency()
}
@Immutable
sealed class IconState {
abstract val isGrayscale: Boolean
data class CoinIcon(
val url: String?,
@DrawableRes val fallbackResId: Int,
override val isGrayscale: Boolean,
) : IconState()
data class TokenIcon(
val url: String?,
val fallbackTint: Color,
val fallbackBackground: Color,
override val isGrayscale: Boolean,
) : IconState()
data class CustomTokenIcon(
val tint: Color,
val background: Color,
override val isGrayscale: Boolean,
) : IconState()
}
}

View file

@ -4,39 +4,39 @@ import com.tangem.core.ui.extensions.getTintForTokenIcon
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.IconState
import com.tangem.utils.converter.Converter
internal class TokenDetailsIconStateConverter : Converter<CryptoCurrency, TokenInfoBlockState.IconState> {
internal class TokenDetailsIconStateConverter : Converter<CryptoCurrency, IconState> {
override fun convert(value: CryptoCurrency): TokenInfoBlockState.IconState {
override fun convert(value: CryptoCurrency): IconState {
return when (value) {
is CryptoCurrency.Coin -> getIconStateForCoin(value)
is CryptoCurrency.Token -> getIconStateForToken(value)
}
}
private fun getIconStateForCoin(coin: CryptoCurrency.Coin): TokenInfoBlockState.IconState.CoinIcon {
return TokenInfoBlockState.IconState.CoinIcon(
private fun getIconStateForCoin(coin: CryptoCurrency.Coin): IconState.CoinIcon {
return IconState.CoinIcon(
url = coin.iconUrl,
fallbackResId = coin.networkIconResId,
isGrayscale = coin.network.isTestnet,
)
}
private fun getIconStateForToken(token: CryptoCurrency.Token): TokenInfoBlockState.IconState {
private fun getIconStateForToken(token: CryptoCurrency.Token): IconState {
val isGrayscale = token.network.isTestnet
val background = token.tryGetBackgroundForTokenIcon(isGrayscale)
val tint = getTintForTokenIcon(background)
return if (token.isCustom && token.iconUrl == null) {
TokenInfoBlockState.IconState.CustomTokenIcon(
IconState.CustomTokenIcon(
tint = tint,
background = background,
isGrayscale = isGrayscale,
)
} else {
TokenInfoBlockState.IconState.TokenIcon(
IconState.TokenIcon(
url = token.iconUrl,
isGrayscale = isGrayscale,
fallbackTint = tint,

View file

@ -7,6 +7,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
@ -16,6 +17,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.Toke
import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles
import com.tangem.features.tokendetails.impl.R
import com.tangem.lib.crypto.BlockchainUtils.isBitcoin
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -25,11 +27,13 @@ import kotlinx.coroutines.flow.MutableStateFlow
internal class TokenDetailsSkeletonStateConverter(
private val clickIntents: TokenDetailsClickIntents,
private val featureToggles: TokenDetailsFeatureToggles,
private val stakingAvailabilityProvider: Provider<StakingAvailability>,
) : Converter<CryptoCurrency, TokenDetailsState> {
private val iconStateConverter by lazy { TokenDetailsIconStateConverter() }
override fun convert(value: CryptoCurrency): TokenDetailsState {
val iconState = iconStateConverter.convert(value)
return TokenDetailsState(
topAppBarConfig = TokenDetailsTopAppBarConfig(
onBackClick = clickIntents::onBackClick,
@ -37,7 +41,7 @@ internal class TokenDetailsSkeletonStateConverter(
),
tokenInfoBlockState = TokenInfoBlockState(
name = value.name,
iconState = iconStateConverter.convert(value),
iconState = iconState,
currency = when (value) {
is CryptoCurrency.Coin -> TokenInfoBlockState.Currency.Native
is CryptoCurrency.Token -> TokenInfoBlockState.Currency.Token(
@ -49,6 +53,7 @@ internal class TokenDetailsSkeletonStateConverter(
),
tokenBalanceBlockState = TokenDetailsBalanceBlockState.Loading(actionButtons = createButtons()),
marketPriceBlockState = MarketPriceBlockState.Loading(value.symbol),
stakingBlockState = StakingBlockState.Loading(iconState = iconState),
notifications = persistentListOf(),
pendingTxs = persistentListOf(),
swapTxs = persistentListOf(),
@ -62,6 +67,7 @@ internal class TokenDetailsSkeletonStateConverter(
bottomSheetConfig = null,
isBalanceHidden = true,
isMarketPriceAvailable = value.id.rawCurrencyId != null,
isStakingAvailable = stakingAvailabilityProvider.invoke() is StakingAvailability.Available,
event = consumedEvent(),
)
}

View file

@ -16,6 +16,8 @@ import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.*
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
@ -38,10 +40,11 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@Suppress("TooManyFunctions", "LargeClass")
@Suppress("TooManyFunctions", "LargeClass", "LongParameterList")
internal class TokenDetailsStateFactory(
private val currentStateProvider: Provider<TokenDetailsState>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val stakingAvailabilityProvider: Provider<StakingAvailability>,
private val clickIntents: TokenDetailsClickIntents,
private val featureToggles: TokenDetailsFeatureToggles,
symbol: String,
@ -52,6 +55,7 @@ internal class TokenDetailsStateFactory(
TokenDetailsSkeletonStateConverter(
clickIntents = clickIntents,
featureToggles = featureToggles,
stakingAvailabilityProvider = stakingAvailabilityProvider,
)
}
@ -98,6 +102,12 @@ internal class TokenDetailsStateFactory(
)
}
private val stakingStateConverter by lazy {
TokenStakingStateConverter(
currentStateProvider = currentStateProvider,
)
}
fun getInitialState(screenArgument: CryptoCurrency): TokenDetailsState {
return skeletonStateConverter.convert(value = screenArgument)
}
@ -201,6 +211,12 @@ internal class TokenDetailsStateFactory(
)
}
fun getStateWithStaking(stakingEither: Either<Throwable, StakingEntryInfo>): TokenDetailsState {
return currentStateProvider().copy(
stakingBlockState = stakingStateConverter.convert(stakingEither),
)
}
fun getRefreshingState(): TokenDetailsState {
return refreshStateConverter.convert(true)
}

View file

@ -0,0 +1,35 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
import arrow.core.Either
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
internal class TokenStakingStateConverter(
private val currentStateProvider: Provider<TokenDetailsState>,
) : Converter<Either<Throwable, StakingEntryInfo>, StakingBlockState> {
override fun convert(value: Either<Throwable, StakingEntryInfo>): StakingBlockState {
value.fold(
ifLeft = {
return StakingBlockState.Error(
iconState = currentStateProvider().tokenInfoBlockState.iconState,
)
},
ifRight = {
return StakingBlockState.Content(
interestRate = BigDecimalFormatter.formatPercent(
percent = it.interestRate,
useAbsoluteValue = true,
),
periodInDays = it.periodInDays,
tokenSymbol = it.tokenSymbol,
iconState = currentStateProvider().tokenInfoBlockState.iconState,
)
},
)
}
}

View file

@ -42,8 +42,10 @@ import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.*
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlock
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsDialogs
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar
@ -133,6 +135,14 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) {
)
}
if (state.isStakingAvailable) {
item(
key = StakingBlockState::class.java,
contentType = StakingBlockState::class.java,
content = { TokenStakingBlock(modifier = itemModifier, state = state.stakingBlockState) },
)
}
swapTransactionsItems(
state.swapTxs,
itemModifier,

View file

@ -21,26 +21,21 @@ import coil.request.ImageRequest
import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.ImageBackgroundContrastChecker
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.IconState
import com.tangem.features.tokendetails.impl.R
import kotlinx.coroutines.launch
@Composable
internal fun CurrencyIcon(
icon: TokenInfoBlockState.IconState,
alpha: Float,
colorFilter: ColorFilter?,
modifier: Modifier = Modifier,
) {
internal fun CurrencyIcon(icon: IconState, alpha: Float, colorFilter: ColorFilter?, modifier: Modifier = Modifier) {
when (icon) {
is TokenInfoBlockState.IconState.CoinIcon -> CoinIcon(
is IconState.CoinIcon -> CoinIcon(
modifier = modifier,
url = icon.url,
fallbackResId = icon.fallbackResId,
alpha = alpha,
colorFilter = colorFilter,
)
is TokenInfoBlockState.IconState.TokenIcon -> TokenIcon(
is IconState.TokenIcon -> TokenIcon(
modifier = modifier,
url = icon.url,
alpha = alpha,
@ -54,7 +49,7 @@ internal fun CurrencyIcon(
)
},
)
is TokenInfoBlockState.IconState.CustomTokenIcon -> CustomTokenIcon(
is IconState.CustomTokenIcon -> CustomTokenIcon(
modifier = modifier,
tint = icon.tint,
background = icon.background,

View file

@ -11,8 +11,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@ -20,14 +18,13 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.GRAY_SCALE_ALPHA
import com.tangem.core.ui.utils.GrayscaleColorFilter
import com.tangem.core.ui.utils.NORMAL_ALPHA
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState
import com.tangem.features.tokendetails.impl.R
private const val GRAY_SCALE_SATURATION = 0f
private const val GRAY_SCALE_ALPHA = 0.4f
private const val NORMAL_ALPHA = 1f
@Composable
internal fun TokenInfoBlock(state: TokenInfoBlockState, modifier: Modifier = Modifier) {
Row(
@ -132,10 +129,10 @@ private fun extractNetwork(tokenCurrency: TokenInfoBlockState.Currency.Token): E
}
}
private data class ExtractedTokenNetworkText(val normalText: String, val boldText: String)
private val GrayscaleColorFilter: ColorFilter
get() = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) })
private data class ExtractedTokenNetworkText(
val normalText: String,
val boldText: String,
)
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)

View file

@ -0,0 +1,206 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.*
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.GRAY_SCALE_ALPHA
import com.tangem.core.ui.utils.GrayscaleColorFilter
import com.tangem.core.ui.utils.NORMAL_ALPHA
import com.tangem.feature.tokendetails.presentation.tokendetails.state.IconState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockState
import com.tangem.features.tokendetails.impl.R
/**
* Token staking block
*
* @param state component state
* @param modifier modifier
*/
@Composable
internal fun TokenStakingBlock(state: StakingBlockState, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.background(
color = TangemTheme.colors.background.primary,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size72)
.padding(all = TangemTheme.dimens.spacing12),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
horizontalAlignment = Alignment.Start,
) {
Content(state = state)
}
}
@Composable
private fun Content(state: StakingBlockState, modifier: Modifier = Modifier) {
AnimatedContent(
modifier = modifier.heightIn(min = TangemTheme.dimens.size60),
targetState = state,
contentAlignment = Alignment.CenterStart,
label = "Update the content",
) { stakingBlockState ->
when (stakingBlockState) {
is StakingBlockState.Content -> {
StakingContent(
stakingBlockState = stakingBlockState,
iconState = stakingBlockState.iconState,
)
}
is StakingBlockState.Loading -> {
StakingLoading(
iconState = stakingBlockState.iconState,
)
}
is StakingBlockState.Error -> Row {} // TODO staking
}
}
}
@Composable
private fun StakingContent(stakingBlockState: StakingBlockState.Content, iconState: IconState) {
Column {
Row {
val (alpha, colorFilter) = remember(iconState.isGrayscale) {
if (iconState.isGrayscale) {
GRAY_SCALE_ALPHA to GrayscaleColorFilter
} else {
NORMAL_ALPHA to null
}
}
CurrencyIcon(
modifier = Modifier
.size(TangemTheme.dimens.size20)
.clip(TangemTheme.shapes.roundedCorners8)
.align(Alignment.CenterVertically),
icon = iconState,
alpha = alpha,
colorFilter = colorFilter,
)
SpacerW8()
Column {
Text(
text = stringResource(
R.string.token_details_staking_block_title,
stakingBlockState.interestRate,
),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle2,
)
Spacer(modifier = Modifier.size(TangemTheme.dimens.size4))
Text(
text = stringResource(
R.string.token_details_staking_block_subtitle,
stakingBlockState.tokenSymbol,
stakingBlockState.periodInDays,
),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.body2,
)
Spacer(modifier = Modifier.size(TangemTheme.dimens.size8))
}
}
SecondaryButton(
modifier = Modifier.fillMaxWidth(),
text = "Stake",
onClick = { /* [REDACTED_TODO_COMMENT] */ },
)
}
}
@Composable
private fun StakingLoading(iconState: IconState) {
Column {
Row {
val (alpha, colorFilter) = remember(iconState.isGrayscale) {
if (iconState.isGrayscale) {
GRAY_SCALE_ALPHA to GrayscaleColorFilter
} else {
NORMAL_ALPHA to null
}
}
CurrencyIcon(
modifier = Modifier
.size(TangemTheme.dimens.size20)
.clip(TangemTheme.shapes.roundedCorners8)
.align(Alignment.CenterVertically),
icon = iconState,
alpha = alpha,
colorFilter = colorFilter,
)
SpacerW8()
Column {
RectangleShimmer(
modifier = Modifier
.fillMaxWidth()
.height(TangemTheme.dimens.size20),
)
Spacer(modifier = Modifier.size(TangemTheme.dimens.size4))
RectangleShimmer(
modifier = Modifier
.fillMaxWidth()
.height(TangemTheme.dimens.size20),
)
Spacer(modifier = Modifier.size(TangemTheme.dimens.size8))
}
}
SecondaryButton(
modifier = Modifier.fillMaxWidth(),
text = "Loading", // TODO staking
onClick = { /* [REDACTED_TODO_COMMENT] */ },
)
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_TokenStakingBlock(
@PreviewParameter(StakingBlockStateProvider::class)
state: StakingBlockState,
) {
TangemThemePreview {
TokenStakingBlock(state = state)
}
}
private class StakingBlockStateProvider : CollectionPreviewParameterProvider<StakingBlockState>(
collection = listOf(
StakingBlockState.Content(
iconState = iconState,
interestRate = "10",
periodInDays = 4,
tokenSymbol = "SOL",
),
StakingBlockState.Loading(iconState = iconState),
StakingBlockState.Error(iconState = iconState),
),
)
private val iconState = IconState.TokenIcon(
url = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png",
fallbackTint = Color.Cyan,
fallbackBackground = Color.Blue,
isGrayscale = false,
)
// endregion Preview

View file

@ -27,6 +27,9 @@ import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.demo.IsDemoCardUseCase
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.model.StakingAvailability
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.legacy.TradeCryptoAction.TransactionInfo
@ -97,6 +100,8 @@ internal class TokenDetailsViewModel @Inject constructor(
private val shouldShowSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase,
private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase,
private val getExtendedPublicKeyForCurrencyUseCase: GetExtendedPublicKeyForCurrencyUseCase,
private val getStakingEntryInfoUseCase: GetStakingEntryInfoUseCase,
private val getStakingAvailabilityUseCase: GetStakingAvailabilityUseCase,
private val swapRepository: SwapRepository,
private val swapTransactionRepository: SwapTransactionRepository,
private val quotesRepository: QuotesRepository,
@ -107,8 +112,8 @@ internal class TokenDetailsViewModel @Inject constructor(
private val analyticsEventsHandler: AnalyticsEventHandler,
private val hapticManager: HapticManager,
private val clipboardManager: ClipboardManager,
tokenDetailsFeatureToggles: TokenDetailsFeatureToggles,
getUserWalletUseCase: GetUserWalletUseCase,
featureToggles: TokenDetailsFeatureToggles,
deepLinksRegistry: DeepLinksRegistry,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents {
@ -137,10 +142,13 @@ internal class TokenDetailsViewModel @Inject constructor(
private val stateFactory = TokenDetailsStateFactory(
currentStateProvider = Provider { uiState },
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
stakingAvailabilityProvider = Provider {
getStakingAvailabilityUseCase.invoke(cryptoCurrency.network.id.value)
},
clickIntents = this,
symbol = cryptoCurrency.symbol,
decimals = cryptoCurrency.decimals,
featureToggles = featureToggles,
featureToggles = tokenDetailsFeatureToggles,
)
private val exchangeStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
@ -207,6 +215,7 @@ internal class TokenDetailsViewModel @Inject constructor(
subscribeOnCurrencyStatusUpdates()
subscribeOnExchangeTransactionsUpdates()
updateTxHistory(refresh = false, showItemsLoading = true)
updateStakingInfo()
}
private fun handleBalanceHiding(owner: LifecycleOwner) {
@ -358,6 +367,16 @@ internal class TokenDetailsViewModel @Inject constructor(
}
}
private fun updateStakingInfo() {
viewModelScope.launch(dispatchers.io) {
val stakingAvailability = getStakingAvailabilityUseCase(cryptoCurrency.network.id.value)
if (stakingAvailability is StakingAvailability.Available) {
val stakingInfo = getStakingEntryInfoUseCase(stakingAvailability.integrationId)
uiState = stateFactory.getStateWithStaking(stakingInfo)
}
}
}
private fun updateTopBarMenu() {
viewModelScope.launch(dispatchers.main) {
uiState = stateFactory.getStateWithUpdatedMenu(

View file

@ -190,6 +190,7 @@ include(":domain:onboarding")
include(":domain:feedback")
include(":domain:qr-scanning")
include(":domain:qr-scanning:models")
include(":domain:staking")
// endregion Domain modules
// region Data modules
@ -209,4 +210,5 @@ include(":data:promo")
include(":data:onboarding")
include(":data:feedback")
include(":data:qr-scanning")
include(":data:staking")
// endregion Data modules