From 84cf236d5fd4d82bbb9d139b5f77a9b61bc3e811 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 1 Oct 2025 12:27:58 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/di/domain/WalletsDomainModule.kt | 23 +++++++++ .../ui/tokens/TokenItemStateConverter.kt | 7 +++ .../api/tangemTech/YieldSupplyApi.kt | 4 +- .../tangem/datasource/di/ApiConfigsModule.kt | 14 ++++++ .../api/common/config/ApiConfigTest.kt | 8 ++++ .../managers/MockEnvironmentConfigStorage.kt | 6 +++ .../managers/ProdApiConfigsManagerTest.kt | 33 +++++++++++++ .../usecase/YieldSupplyApyFlowUseCase.kt | 26 ++++++++++ .../usecase/YieldSupplyApyUpdateUseCase.kt | 25 ++++++++++ features/wallet/impl/build.gradle.kts | 2 + .../wallet/child/wallet/model/WalletModel.kt | 13 +++++ .../implementors/MultiWalletContentLoader.kt | 3 ++ .../MultiWalletContentLoaderFactory.kt | 3 ++ .../SingleWalletWithTokenContentLoader.kt | 3 ++ ...ngleWalletWithTokenContentLoaderFactory.kt | 3 ++ .../transformers/SetTokenListTransformer.kt | 2 + .../converter/TokenListStateConverter.kt | 47 ++++++++++++++++++- .../subscribers/BasicTokenListSubscriber.kt | 42 ++++++++++++++--- .../MultiWalletTokenListSubscriber.kt | 2 + .../SingleWalletWithTokenListSubscriber.kt | 2 + 20 files changed, 258 insertions(+), 10 deletions(-) create mode 100644 domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyFlowUseCase.kt create mode 100644 domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyUpdateUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index 2df609823f..0f7e759701 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -16,6 +16,9 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.* +import com.tangem.domain.yield.supply.YieldSupplyMarketRepository +import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyApyUpdateUseCase import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSyncUseCase import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase import com.tangem.features.hotwallet.HotWalletFeatureToggles @@ -448,4 +451,24 @@ internal object WalletsDomainModule { userWalletUseCase = getUserWalletUseCase, ) } + + @Provides + @Singleton + fun provideYieldSupplyApyFlowUseCase( + yieldSupplyMarketRepository: YieldSupplyMarketRepository, + ): YieldSupplyApyFlowUseCase { + return YieldSupplyApyFlowUseCase( + yieldSupplyMarketRepository = yieldSupplyMarketRepository, + ) + } + + @Provides + @Singleton + fun provideYieldSupplyApyUpdateUseCase( + yieldSupplyMarketRepository: YieldSupplyMarketRepository, + ): YieldSupplyApyUpdateUseCase { + return YieldSupplyApyUpdateUseCase( + yieldSupplyMarketRepository = yieldSupplyMarketRepository, + ) + } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt index 5f418d436d..0d23288ee3 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt @@ -218,6 +218,13 @@ class TokenItemStateConverter( text = status.getFormattedFiatAmount(appCurrency = appCurrency), isFlickering = status.value.isFlickering(), icons = buildList { + if (status.value.yieldSupplyStatus?.isActive == true && + status.value.yieldSupplyStatus?.isAllowedToSpend == false) { + TokenItemState.FiatAmountState.Content.IconUM( + iconRes = R.drawable.ic_alert_triangle_20, + tint = IconTint.Warning, + ).let(::add) + } if (!status.getStakedBalance().isZero()) { add( TokenItemState.FiatAmountState.Content.IconUM( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/YieldSupplyApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/YieldSupplyApi.kt index cc41d91e89..e343461417 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/YieldSupplyApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/YieldSupplyApi.kt @@ -8,9 +8,9 @@ import retrofit2.http.Path interface YieldSupplyApi { - @GET("v1/yield/markets") + @GET("api/v1/yield/markets") suspend fun getYieldMarkets(): ApiResponse - @GET("v1/yield/token/{tokenAddress}") + @GET("api/v1/yield/token/{tokenAddress}") suspend fun getYieldTokenStatus(@Path("tokenAddress") tokenAddress: String): ApiResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt index 04961c598a..57c3c53a26 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt @@ -53,6 +53,20 @@ internal object ApiConfigsModule { appInfoProvider = appInfoProvider, ) + @Provides + @IntoSet + fun provideYieldSupplyConfig( + environmentConfigStorage: EnvironmentConfigStorage, + appVersionProvider: AppVersionProvider, + authProvider: AuthProvider, + appInfoProvider: AppInfoProvider, + ): ApiConfig = YieldSupply( + environmentConfigStorage = environmentConfigStorage, + appVersionProvider = appVersionProvider, + authProvider = authProvider, + appInfoProvider = appInfoProvider, + ) + @Provides @IntoSet fun provideTangemVisaConfig(appVersionProvider: AppVersionProvider): ApiConfig = TangemPay(appVersionProvider) diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt index e46f600973..53392207a9 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt @@ -37,6 +37,14 @@ class ApiConfigTest { appInfoProvider = mockk(), ) } + ApiConfig.ID.YieldSupply -> { + YieldSupply( + environmentConfigStorage = mockk(), + appVersionProvider = mockk(), + authProvider = mockk(), + appInfoProvider = mockk(), + ) + } ApiConfig.ID.TangemTech -> { TangemTech( environmentConfigStorage = mockk(), diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/MockEnvironmentConfigStorage.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/MockEnvironmentConfigStorage.kt index 016b5cda80..44ba07d2b2 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/MockEnvironmentConfigStorage.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/MockEnvironmentConfigStorage.kt @@ -16,6 +16,9 @@ internal class MockEnvironmentConfigStorage : EnvironmentConfigStorage { express = ExpressModel(apiKey = EXPRESS_API_KEY, signVerifierPublicKey = "vocibus"), devExpress = ExpressModel(apiKey = EXPRESS_DEV_API_KEY, signVerifierPublicKey = "pellentesque"), blockAidApiKey = BLOCK_AID_API_KEY, + tangemApiKey = TANGEM_API_KEY, + tangemApiKeyDev = TANGEM_API_KEY_DEV, + tangemApiKeyStage = TANGEM_API_KEY_STAGE, ) override suspend fun initialize() = environmentConfig @@ -26,5 +29,8 @@ internal class MockEnvironmentConfigStorage : EnvironmentConfigStorage { const val EXPRESS_API_KEY = "express_api_key" const val EXPRESS_DEV_API_KEY = "express_dev_api_key" const val BLOCK_AID_API_KEY = "block_aid_api_key" + const val TANGEM_API_KEY = "tangem_api_key" + const val TANGEM_API_KEY_DEV = "tangem_api_key_dev" + const val TANGEM_API_KEY_STAGE = "tangem_api_key_stage" } } \ No newline at end of file diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index c4e9e81ecf..0ab5871119 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -84,6 +84,14 @@ internal class ProdApiConfigsManagerTest { appInfoProvider = appInfoProvider, ) } + ApiConfig.ID.YieldSupply -> { + YieldSupply( + environmentConfigStorage = environmentConfigStorage, + appVersionProvider = appVersionProvider, + authProvider = appAuthProvider, + appInfoProvider = appInfoProvider, + ) + } ApiConfig.ID.TangemTech -> { TangemTech( environmentConfigStorage = environmentConfigStorage, @@ -102,6 +110,7 @@ internal class ProdApiConfigsManagerTest { private fun provideTestModels() = ApiConfig.ID.entries.map { when (it) { ApiConfig.ID.Express -> createExpressModel() + ApiConfig.ID.YieldSupply -> createYieldSupplyModel() ApiConfig.ID.TangemTech -> createTangemTechModel() ApiConfig.ID.StakeKit -> createStakeKitModel() ApiConfig.ID.TangemPay -> createTangemPayModel() @@ -166,6 +175,30 @@ internal class ProdApiConfigsManagerTest { environment = ApiEnvironment.PROD, baseUrl = "https://api.tangem.org/", headers = mapOf( + "api-key" to ProviderSuspend { MockEnvironmentConfigStorage.TANGEM_API_KEY }, + "card_id" to ProviderSuspend { APP_CARD_ID }, + "card_public_key" to ProviderSuspend { APP_CARD_PUBLIC_KEY }, + "version" to ProviderSuspend { VERSION_NAME }, + "platform" to ProviderSuspend { "android" }, + "system_version" to ProviderSuspend { "Android 16" }, + "language" to ProviderSuspend { Locale.getDefault().language.checkHeaderValueOrEmpty() }, + "timezone" to ProviderSuspend { + TimeZone.getDefault().getDisplayName(false, TimeZone.SHORT).checkHeaderValueOrEmpty() + }, + "device" to ProviderSuspend { "${Build.MANUFACTURER} ${Build.MODEL}".checkHeaderValueOrEmpty() }, + ), + ), + ) + } + + private fun createYieldSupplyModel(): TestModel { + return TestModel( + id = ApiConfig.ID.YieldSupply, + expected = ApiEnvironmentConfig( + environment = ApiEnvironment.PROD, + baseUrl = "https://yield.tangem.org/", + headers = mapOf( + "api-key" to ProviderSuspend { MockEnvironmentConfigStorage.TANGEM_API_KEY }, "card_id" to ProviderSuspend { APP_CARD_ID }, "card_public_key" to ProviderSuspend { APP_CARD_PUBLIC_KEY }, "version" to ProviderSuspend { VERSION_NAME }, diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyFlowUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyFlowUseCase.kt new file mode 100644 index 0000000000..55ad0d7e95 --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyFlowUseCase.kt @@ -0,0 +1,26 @@ +package com.tangem.domain.yield.supply.usecase + +import com.tangem.domain.yield.supply.YieldSupplyMarketRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +/** + * Emits a map of APY values per token. + * + * Return map: + * - key: token contract address + * - value: APY as string + */ +class YieldSupplyApyFlowUseCase( + private val yieldSupplyMarketRepository: YieldSupplyMarketRepository, +) { + + operator fun invoke(): Flow> { + return yieldSupplyMarketRepository.getMarketsFlow() + .map { yieldMarketTokenList -> + yieldMarketTokenList.filter { it.isActive }.associate { + it.tokenAddress to it.apy.toString() + } + } + } +} \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyUpdateUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyUpdateUseCase.kt new file mode 100644 index 0000000000..a3d9200e44 --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyUpdateUseCase.kt @@ -0,0 +1,25 @@ +package com.tangem.domain.yield.supply.usecase + +import arrow.core.Either +import com.tangem.domain.yield.supply.YieldSupplyMarketRepository +import kotlin.collections.filter + +/** + * Updates and returns a map of APY values per token. + * + * Return map: + * - key: token contract address + * - value: APY as string + */ +class YieldSupplyApyUpdateUseCase( + private val yieldSupplyMarketRepository: YieldSupplyMarketRepository, +) { + + suspend operator fun invoke(): Either> = Either.catch { + yieldSupplyMarketRepository.updateMarkets() + .filter { it.isActive } + .associate { + it.tokenAddress to it.apy.toString() + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index e0f9ecea7e..b622e8fe42 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -98,6 +98,8 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.notifications) implementation(projects.domain.transaction) + implementation(projects.domain.yieldSupply) + implementation(projects.domain.yieldSupply.models) /** Feature Apis */ implementation(projects.features.details.api) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 291da9380e..c39e81fe15 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -23,6 +23,7 @@ import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase import com.tangem.domain.settings.* import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase import com.tangem.domain.wallets.usecase.* +import com.tangem.domain.yield.supply.usecase.YieldSupplyApyUpdateUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent @@ -43,6 +44,7 @@ import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.features.wallet.deeplink.WalletDeepLinkActionListener +import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import kotlinx.coroutines.* @@ -92,6 +94,8 @@ internal class WalletModel @Inject constructor( private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase, private val tangemPayIssueOrderUseCase: TangemPayIssueOrderUseCase, private val tangemPayFeatureToggles: TangemPayFeatureToggles, + private val yieldSupplyApyUpdateUseCase: YieldSupplyApyUpdateUseCase, + private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, ) : Model() { @@ -116,6 +120,7 @@ internal class WalletModel @Inject constructor( maybeMigrateNames() maybeSetWalletFirstTimeUsage() + updateYieldSupplyApy() subscribeToUserWalletsUpdates() subscribeOnBalanceHiding() subscribeOnSelectedWalletFlow() @@ -127,6 +132,14 @@ internal class WalletModel @Inject constructor( clickIntents.initialize(innerWalletRouter, modelScope) } + private fun updateYieldSupplyApy() { + if (yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled) { + modelScope.launch(dispatchers.default) { + yieldSupplyApyUpdateUseCase() + } + } + } + private fun maybeMigrateNames() { modelScope.launch { walletNameMigrationUseCase() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt index 2fc6d43584..0aee41a87b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt @@ -10,6 +10,7 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender @@ -42,6 +43,7 @@ internal class MultiWalletContentLoader( private val walletsRepository: WalletsRepository, private val currenciesRepository: CurrenciesRepository, private val accountDependencies: AccountDependencies, + private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, ) : WalletContentLoader(id = userWallet.walletId) { override fun create(): List { @@ -57,6 +59,7 @@ internal class MultiWalletContentLoader( applyTokenListSortingUseCase = applyTokenListSortingUseCase, runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, accountDependencies = accountDependencies, + yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, ).let(::add) WalletNFTListSubscriber( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt index 2bac7a7f47..2def028fe4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt @@ -10,6 +10,7 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender @@ -40,6 +41,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, private val currenciesRepository: CurrenciesRepository, private val accountDependencies: AccountDependencies, + private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, ) { fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): WalletContentLoader { @@ -62,6 +64,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( getNFTCollectionsUseCase = getNFTCollectionsUseCase, currenciesRepository = currenciesRepository, accountDependencies = accountDependencies, + yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt index 9463898730..4bbae24b19 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt @@ -5,6 +5,7 @@ import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender @@ -32,6 +33,7 @@ internal class SingleWalletWithTokenContentLoader( private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, private val accountDependencies: AccountDependencies, + private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, ) : WalletContentLoader(id = userWallet.walletId) { override fun create(): List { @@ -46,6 +48,7 @@ internal class SingleWalletWithTokenContentLoader( getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, accountDependencies = accountDependencies, + yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, ).let(::add) MultiWalletWarningsSubscriber( userWallet = userWallet, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt index f18029c177..743e1bf3c8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt @@ -6,6 +6,7 @@ import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender @@ -33,6 +34,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, private val accountDependencies: AccountDependencies, + private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, ) { fun create(userWallet: UserWallet.Cold, clickIntents: WalletClickIntents): SingleWalletWithTokenContentLoader { @@ -51,6 +53,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( getStoryContentUseCase = getStoryContentUseCase, shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase, accountDependencies = accountDependencies, + yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt index faa718a4c6..9b15ca02a0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt @@ -16,6 +16,7 @@ internal class SetTokenListTransformer( private val userWallet: UserWallet, private val appCurrency: AppCurrency, private val clickIntents: WalletClickIntents, + private val yieldSupplyApyMap: Map = emptyMap(), ) : WalletStateTransformer(userWallet.walletId) { override fun transform(prevState: WalletState): WalletState { @@ -58,6 +59,7 @@ internal class SetTokenListTransformer( selectedWallet = userWallet, appCurrency = appCurrency, clickIntents = clickIntents, + apyMap = yieldSupplyApyMap, ).convert(value = this) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt index 449ecf2c3b..46dcefb0d7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter import com.tangem.common.ui.tokens.TokenItemStateConverter +import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.resourceReference @@ -13,6 +14,7 @@ import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.tokenlist.TokenList.GroupedByNetwork.NetworkGroup @@ -34,6 +36,7 @@ internal class TokenListStateConverter( private val params: TokenConverterParams, private val selectedWallet: UserWallet, private val clickIntents: WalletClickIntents, + private val apyMap: Map = emptyMap(), ) : Converter { private val onTokenClick: (accountId: AccountId?, currencyStatus: CryptoCurrencyStatus) -> Unit = @@ -165,12 +168,54 @@ internal class TokenListStateConverter( tokenConverter: TokenItemStateConverter, token: CryptoCurrencyStatus, ): List { - val tokenItemState = tokenConverter.convert(token) + val tokenItemState = tokenConverter.convert(token).withEarnApyBadge(token) + add(TokensListItemUM.Token(tokenItemState)) return this } + private fun TokenItemState.withEarnApyBadge(cryptoCurrencyStatus: CryptoCurrencyStatus): TokenItemState { + val apy: String? = resolveEarnApy(cryptoCurrencyStatus) + + val shouldApply = apy != null && this.titleState is TokenItemState.TitleState.Content + + return if (!shouldApply) { + this + } else { + val contentTitle = this.titleState as TokenItemState.TitleState.Content + val newTitle = contentTitle.copy( + earnApy = resourceReference( + R.string.yield_module_earn_badge, + wrappedList(apy), + ), + ) + + when (this) { + is TokenItemState.Content -> this.copy(titleState = newTitle) + is TokenItemState.Unreachable -> this.copy(titleState = newTitle) + is TokenItemState.NoAddress -> this.copy(titleState = newTitle) + is TokenItemState.Loading -> this.copy(titleState = newTitle) + is TokenItemState.Draggable -> this.copy(titleState = newTitle) + is TokenItemState.Locked -> this + } + } + } + + private fun resolveEarnApy(cryptoCurrencyStatus: CryptoCurrencyStatus): String? { + if (apyMap.isEmpty()) return null + + val isYieldSupplyActive = (cryptoCurrencyStatus.value as? CryptoCurrencyStatus.Loaded) + ?.yieldSupplyStatus?.isActive == true + if (isYieldSupplyActive) return null + + val contract = (cryptoCurrencyStatus.currency as? CryptoCurrency.Token) + ?.contractAddress + ?.lowercase() ?: return null + + return apyMap[contract] + } + private fun getOrganizeTokensButtonState(tokenList: TokenList): WalletOrganizeTokensButtonConfig? { val currenciesSize = when (tokenList) { TokenList.Empty -> return null diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt index 6f7cb91a9f..b9b464642d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt @@ -13,6 +13,7 @@ import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.account.AccountDependencies import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender @@ -39,6 +40,7 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() { protected abstract val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase protected abstract val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase protected abstract val accountDependencies: AccountDependencies + protected abstract val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase private val sendAnalyticsJobHolder = JobHolder() private val onTokenListReceivedJobHolder = JobHolder() @@ -73,8 +75,14 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() { coroutineScope.launch { startCheck(maybeTokenList) } }, flow2 = appCurrencyFlow(), - transform = { maybeTokenList, appCurrency -> - singleAccountTransform(maybeTokenList, appCurrency, PortfolioId(userWallet.walletId)) + flow3 = yieldSupplyApyFlow(), + transform = { maybeTokenList, appCurrency, yieldSupplyApyMap -> + singleAccountTransform( + maybeTokenList = maybeTokenList, + appCurrency = appCurrency, + portfolioId = PortfolioId(userWallet.walletId), + yieldSupplyApyMap = yieldSupplyApyMap, + ) }, ) } @@ -83,6 +91,7 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() { maybeTokenList: Lce, appCurrency: AppCurrency, portfolioId: PortfolioId, + yieldSupplyApyMap: Map, ) { val tokenList = maybeTokenList.getOrElse( ifLoading = { maybeContent -> @@ -106,7 +115,12 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() { return }, ) - updateContent(TokenConverterParams.Wallet(portfolioId, tokenList), appCurrency) + updateContent( + params = TokenConverterParams.Wallet(portfolioId, tokenList), + appCurrency = appCurrency, + yieldSupplyApyMap = yieldSupplyApyMap, + ) + walletWithFundsChecker.check(tokenList) } @@ -143,7 +157,8 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() { flow2 = appCurrencyFlow(), flow3 = accountDependencies.expandedAccountsHolder.expandedAccounts(userWallet), flow4 = accountDependencies.isAccountsModeEnabledUseCase(), - transform = { accountList, appCurrency, expandedAccounts, isAccountMode -> + flow5 = yieldSupplyApyFlow(), + transform = { accountList, appCurrency, expandedAccounts, isAccountMode, yieldSupplyApyMap -> val accountFlattenTokensList = accountList.flattenTokens() val accountFlattenCurrencies = accountFlattenTokensList .map { it.flattenCurrencies() } @@ -153,7 +168,12 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() { } suspend fun singleAccountTransform(maybeTokenList: Lce) = - this.singleAccountTransform(maybeTokenList, appCurrency, PortfolioId(mainAccount.account.accountId)) + this.singleAccountTransform( + maybeTokenList, + appCurrency, + PortfolioId(mainAccount.account.accountId), + yieldSupplyApyMap, + ) when { !isAccountMode -> when (mainAccount.tokenList.flattenCurrencies().isEmpty()) { @@ -171,7 +191,7 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() { ) false -> { val convertParams = TokenConverterParams.Account(accountList, expandedAccounts) - updateContent(convertParams, appCurrency) + updateContent(convertParams, appCurrency, yieldSupplyApyMap) accountFlattenTokensList .map { tokenList -> coroutineScope.launch { walletWithFundsChecker.check(tokenList) } } .joinAll() @@ -197,13 +217,18 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() { ) } - private fun updateContent(params: TokenConverterParams, appCurrency: AppCurrency) { + private fun updateContent( + params: TokenConverterParams, + appCurrency: AppCurrency, + yieldSupplyApyMap: Map, + ) { stateHolder.update( SetTokenListTransformer( params = params, userWallet = userWallet, appCurrency = appCurrency, clickIntents = clickIntents, + yieldSupplyApyMap = yieldSupplyApyMap, ), ) } @@ -216,4 +241,7 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() { } } .distinctUntilChanged() + + private fun yieldSupplyApyFlow(): Flow> = yieldSupplyApyFlowUseCase() + .distinctUntilChanged() } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt index ab57c197d2..f56709b1d7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt @@ -13,6 +13,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.account.AccountDependencies import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender @@ -34,6 +35,7 @@ internal class MultiWalletTokenListSubscriber( override val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, override val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, override val accountDependencies: AccountDependencies, + override val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, ) : BasicTokenListSubscriber() { override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt index cab9cfea7d..6866fdbf8f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt @@ -9,6 +9,7 @@ import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.account.AccountDependencies import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender @@ -29,6 +30,7 @@ internal class SingleWalletWithTokenListSubscriber( override val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, override val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, override val accountDependencies: AccountDependencies, + override val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, ) : BasicTokenListSubscriber() { override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow {