From 682349e353875dfddd3da1f0aac466cf986a11f7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 31 May 2024 12:21:36 +0200 Subject: [PATCH] Updated on 2026-08-14 --- app/build.gradle.kts | 2 + .../tap/di/domain/StakingDomainModule.kt | 32 +++ .../datasource/api/common/MoshiConverter.kt | 1 + .../common/{ => adapter}/BigDecimalAdapter.kt | 2 +- .../common/{ => adapter}/DateTimeAdapter.kt | 2 +- .../common/{ => adapter}/LocalDateAdapter.kt | 2 +- .../adapter}/UnknownEnumMoshiAdapter.kt | 2 +- .../datasource/api/stakekit/StakeKitApi.kt | 26 ++- .../models/request/YieldBalanceRequestBody.kt | 31 +++ .../stakekit/models/response/model/Token.kt | 6 +- .../stakekit/models/response/model/Yield.kt | 5 +- .../response/model/YieldBalanceWrapper.kt | 138 ++++++++++++ .../com/tangem/datasource/di/MoshiModule.kt | 8 +- core/res/src/main/res/values-ru/strings.xml | 2 + core/res/src/main/res/values/strings.xml | 10 +- .../components/currency/fiaticon/FiatIcon.kt | 12 +- .../currency/tokenicon/TokenIcon.kt | 14 +- .../tangem/core/ui/utils/GrayscaleUtils.kt | 11 + data/staking/.gitignore | 1 + data/staking/build.gradle.kts | 36 +++ .../data/staking/DefaultStakingRepository.kt | 63 ++++++ .../data/staking/di/StakingDataModule.kt | 28 +++ domain/staking/.gitignore | 1 + domain/staking/build.gradle.kts | 9 + .../staking/GetStakingAvailabilityUseCase.kt | 16 ++ .../staking/GetStakingEntryInfoUseCase.kt | 15 ++ .../staking/model/StakingAvailability.kt | 8 + .../domain/staking/model/StakingEntryInfo.kt | 9 + .../staking/repositories/StakingRepository.kt | 11 + .../tangem/feature/swap/ui/ProviderItem.kt | 9 +- .../TokenDetailsFeatureToggles.kt | 1 + features/tokendetails/impl/build.gradle.kts | 1 + .../DefaultTokenDetailsFeatureToggles.kt | 1 + .../tokendetails/TokenDetailsPreviewData.kt | 26 ++- .../tokendetails/state/IconState.kt | 30 +++ .../tokendetails/state/StakingBlockState.kt | 20 ++ .../tokendetails/state/TokenDetailsState.kt | 2 + .../tokendetails/state/TokenInfoBlockState.kt | 28 +-- .../factory/TokenDetailsIconStateConverter.kt | 16 +- .../TokenDetailsSkeletonStateConverter.kt | 8 +- .../state/factory/TokenDetailsStateFactory.kt | 18 +- .../factory/TokenStakingStateConverter.kt | 35 +++ .../tokendetails/ui/TokenDetailsScreen.kt | 10 + .../tokendetails/ui/components/TokenIcon.kt | 15 +- .../ui/components/TokenInfoBlock.kt | 17 +- .../ui/components/TokenStakingBlock.kt | 206 ++++++++++++++++++ .../viewmodels/TokenDetailsViewModel.kt | 23 +- settings.gradle.kts | 2 + 48 files changed, 860 insertions(+), 111 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt rename core/datasource/src/main/java/com/tangem/datasource/api/common/{ => adapter}/BigDecimalAdapter.kt (85%) rename core/datasource/src/main/java/com/tangem/datasource/api/common/{ => adapter}/DateTimeAdapter.kt (91%) rename core/datasource/src/main/java/com/tangem/datasource/api/common/{ => adapter}/LocalDateAdapter.kt (92%) rename core/datasource/src/main/java/com/tangem/datasource/api/{stakekit => common/adapter}/UnknownEnumMoshiAdapter.kt (88%) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapper.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/utils/GrayscaleUtils.kt create mode 100644 data/staking/.gitignore create mode 100644 data/staking/build.gradle.kts create mode 100644 data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt create mode 100644 domain/staking/.gitignore create mode 100644 domain/staking/build.gradle.kts create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/GetStakingEntryInfoUseCase.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/model/StakingAvailability.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/model/StakingEntryInfo.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/IconState.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenStakingBlock.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 512db83b6d..7103b16c66 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt new file mode 100644 index 0000000000..54bfe9d2b7 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -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, + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/MoshiConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/MoshiConverter.kt index 510ac95179..1dd6c425a5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/MoshiConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/MoshiConverter.kt @@ -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 /** diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/BigDecimalAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/BigDecimalAdapter.kt similarity index 85% rename from core/datasource/src/main/java/com/tangem/datasource/api/common/BigDecimalAdapter.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/BigDecimalAdapter.kt index e81aa15e06..c025caee95 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/BigDecimalAdapter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/BigDecimalAdapter.kt @@ -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 diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/DateTimeAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/DateTimeAdapter.kt similarity index 91% rename from core/datasource/src/main/java/com/tangem/datasource/api/common/DateTimeAdapter.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/DateTimeAdapter.kt index 38f4b81532..5e637ed8c8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/DateTimeAdapter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/DateTimeAdapter.kt @@ -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 diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/LocalDateAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/LocalDateAdapter.kt similarity index 92% rename from core/datasource/src/main/java/com/tangem/datasource/api/common/LocalDateAdapter.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/LocalDateAdapter.kt index 95978206f2..6a7cd420c6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/LocalDateAdapter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/LocalDateAdapter.kt @@ -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 diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/UnknownEnumMoshiAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt similarity index 88% rename from core/datasource/src/main/java/com/tangem/datasource/api/stakekit/UnknownEnumMoshiAdapter.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt index 20fe881be9..f61191a104 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/UnknownEnumMoshiAdapter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt @@ -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 diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt index e0adeb7a84..c7d564c69a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt @@ -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 + @GET("yields/{integrationId}") + suspend fun getSingleYield( + @Path("integrationId") integrationId: String, + @Query("ledgerWalletAPICompatible") ledgerWalletAPICompatible: Boolean = false, + ): ApiResponse + @GET("yields/balances") - fun getMultipleYieldBalances(@Body body: List): ApiResponse> + suspend fun getMultipleYieldBalances( + @Body body: List, + ): ApiResponse> + + @GET("yields/{integrationId}/balances") + suspend fun getSingleYieldBalance( + @Path("integrationId") integrationId: String, + @Body body: YieldBalanceRequestBody, + ): ApiResponse @GET("tokens") - fun getTokens(): ApiResponse> + suspend fun getTokens(): ApiResponse> } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt new file mode 100644 index 0000000000..7433386e73 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt @@ -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? = null, + @Json(name = "lidoStakeAccounts") val lidoStakeAccounts: List? = 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, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Token.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Token.kt index 35ed171d9e..88d5d12b59 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Token.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Token.kt @@ -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, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Yield.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Yield.kt index 5f728eae71..c122022402 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Yield.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Yield.kt @@ -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") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapper.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapper.kt new file mode 100644 index 0000000000..01f0e26c16 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapper.kt @@ -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, + @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, + @Json(name = "token") + val token: Token, + @Json(name = "validatorAddress") + val validatorAddress: String?, + @Json(name = "validatorAddresses") + val validatorAddresses: List?, + @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?, + @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, + ) + } + } + + @JsonClass(generateAdapter = true) + data class Required( + @Json(name = "required") + val required: Boolean, + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt index 81956364f7..ddd3386e58 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt @@ -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 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index d69a9a52dd..37ebbc73ba 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -580,6 +580,8 @@ Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами. Скрыть %s Скрыть токен + Стейкинг позволяет вам зарабатывать %1$s и получать вознаграждения каждые %2$s дней + Зарабатывайте до %s вознаграждений за стейкинг ежегодно %1$s токен в сети %%image%% %2$s Токен в сети %%image%% %1$s Токен %1$s (%2$s) является основной валютой в сети %3$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 50d7c7f7d5..f4dac4dbb5 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -26,7 +26,7 @@ Light System default Theme - App Settings + App settings To hide or show your balances, simply flip your device screen down, or switch it off in Settings Don\'t show again Got it @@ -49,7 +49,7 @@ Access code will be changed on this card only Reset to Factory Settings Security Mode - Card Settings + Card settings In addition to network fee, the Cardano network charges %1$s ADA when transacting with the %2$s token Cardano transaction requirements To make a %1$s transaction, you must deposit some ADA to cover the network fee and minimum ADA value (5 ADA recommended) @@ -173,7 +173,7 @@ Flip your device screen down to quickly hide and show balances %s hashes Card ID - Contact to Support + Contact support Link More Cards App Currency Flip-to-Hide Balances @@ -181,7 +181,7 @@ Signed Details Check your internet connection or switch to a different network - Terms of Service + Terms of service You have used a card from another wallet. Tap the card associated with this wallet My tokens You haven\'t added any tokens yet. Add tokens via Market to swap @@ -573,6 +573,8 @@ You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page. Hide %s Hide token + Staking allows you to earn %1$s and get rewards every %2$s days + Earn up to %s staking rewards yearly %1$s token in %%image%% %2$s network Token in %%image%% %1$s network 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 diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/fiaticon/FiatIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/fiaticon/FiatIcon.kt index 76d5c9d094..13462984c2 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/fiaticon/FiatIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/fiaticon/FiatIcon.kt @@ -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) }) \ No newline at end of file +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIcon.kt index 07d299f583..559bc7bde8 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIcon.kt @@ -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) }) \ No newline at end of file +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/GrayscaleUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/GrayscaleUtils.kt new file mode 100644 index 0000000000..f3edb75b60 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/GrayscaleUtils.kt @@ -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) }) \ No newline at end of file diff --git a/data/staking/.gitignore b/data/staking/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/staking/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/staking/build.gradle.kts b/data/staking/build.gradle.kts new file mode 100644 index 0000000000..83a8cccc87 --- /dev/null +++ b/data/staking/build.gradle.kts @@ -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 +} diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt new file mode 100644 index 0000000000..26f738de00 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -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, + ) + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt new file mode 100644 index 0000000000..0028f190b2 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -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, + ) + } +} \ No newline at end of file diff --git a/domain/staking/.gitignore b/domain/staking/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/staking/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/staking/build.gradle.kts b/domain/staking/build.gradle.kts new file mode 100644 index 0000000000..f6a93898bb --- /dev/null +++ b/domain/staking/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +dependencies { + implementation(deps.kotlin.coroutines) + implementation(deps.arrow.core) +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt new file mode 100644 index 0000000000..18717a31f7 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt @@ -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) + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingEntryInfoUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingEntryInfoUseCase.kt new file mode 100644 index 0000000000..02a505aa03 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingEntryInfoUseCase.kt @@ -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 { + return Either.catch { stakingRepository.getEntryInfo(integrationId) } + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingAvailability.kt b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingAvailability.kt new file mode 100644 index 0000000000..bf40ee3b58 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingAvailability.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.staking.model + +sealed class StakingAvailability { + + data class Available(val integrationId: String) : StakingAvailability() + + data object Unavailable : StakingAvailability() +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingEntryInfo.kt b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingEntryInfo.kt new file mode 100644 index 0000000000..691aece740 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingEntryInfo.kt @@ -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, +) \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt new file mode 100644 index 0000000000..763377ac5e --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -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 +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index 2707fa77b2..d94153492f 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -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) { diff --git a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/featuretoggles/TokenDetailsFeatureToggles.kt b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/featuretoggles/TokenDetailsFeatureToggles.kt index d64095a963..1fc4007823 100644 --- a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/featuretoggles/TokenDetailsFeatureToggles.kt +++ b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/featuretoggles/TokenDetailsFeatureToggles.kt @@ -1,5 +1,6 @@ package com.tangem.features.tokendetails.featuretoggles interface TokenDetailsFeatureToggles { + fun isGenerateXPubEnabled(): Boolean } \ No newline at end of file diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 5c435284d7..bff55fb9ad 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -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) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/featuretoggles/DefaultTokenDetailsFeatureToggles.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/featuretoggles/DefaultTokenDetailsFeatureToggles.kt index 1df3fcf7be..96d2dbbb72 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/featuretoggles/DefaultTokenDetailsFeatureToggles.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/featuretoggles/DefaultTokenDetailsFeatureToggles.kt @@ -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") } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index b16fe4ac2c..470cc0ac26 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -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(), ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/IconState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/IconState.kt new file mode 100644 index 0000000000..a3e44c1324 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/IconState.kt @@ -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() +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt new file mode 100644 index 0000000000..f1668235b6 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt @@ -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 +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index f9f5dfcabd..26f36f81ad 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -17,6 +17,7 @@ internal data class TokenDetailsState( val tokenInfoBlockState: TokenInfoBlockState, val tokenBalanceBlockState: TokenDetailsBalanceBlockState, val marketPriceBlockState: MarketPriceBlockState, + val stakingBlockState: StakingBlockState, val notifications: ImmutableList, val pendingTxs: PersistentList, val swapTxs: PersistentList, @@ -26,5 +27,6 @@ internal data class TokenDetailsState( val bottomSheetConfig: TangemBottomSheetConfig?, val isBalanceHidden: Boolean, val isMarketPriceAvailable: Boolean, + val isStakingAvailable: Boolean, val event: StateEvent, ) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt index f0ec9e8d43..9e54bb1627 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt @@ -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() - } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsIconStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsIconStateConverter.kt index c428da7cec..cfbfcb7015 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsIconStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsIconStateConverter.kt @@ -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 { +internal class TokenDetailsIconStateConverter : Converter { - 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, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 073b8e88ba..2c29e74ce3 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -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, ) : Converter { 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(), ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 1cd62078a1..af67c4e57d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -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, private val appCurrencyProvider: Provider, + private val stakingAvailabilityProvider: Provider, 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): TokenDetailsState { + return currentStateProvider().copy( + stakingBlockState = stakingStateConverter.convert(stakingEither), + ) + } + fun getRefreshingState(): TokenDetailsState { return refreshStateConverter.convert(true) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt new file mode 100644 index 0000000000..371e2e766d --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt @@ -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, +) : Converter, StakingBlockState> { + + override fun convert(value: Either): 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, + ) + }, + ) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index ebce26c35c..d9eb9ec18d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -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, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenIcon.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenIcon.kt index 8bf7ed9b78..c0b325c9e4 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenIcon.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenIcon.kt @@ -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, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt index 37f036d60f..773dc2868f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt @@ -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) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenStakingBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenStakingBlock.kt new file mode 100644 index 0000000000..6ce576304f --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenStakingBlock.kt @@ -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( + 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 \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 814cc1bcdd..610f053855 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -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( diff --git a/settings.gradle.kts b/settings.gradle.kts index 1b3dab02bb..6412e9236b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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 \ No newline at end of file