diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index a8cf062a6b..4272136431 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit a8cf062a6bb58332458c0f5b43026e062378e5c7 +Subproject commit 4272136431c3629230803e70c4d2cf412365418d 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 index f98c7d2bd2..92e80ff6db 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -6,6 +6,7 @@ import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.repositories.StakingTransactionHashRepository import com.tangem.domain.staking.single.SingleYieldBalanceFetcher +import com.tangem.domain.staking.usecase.StakingApyFlowUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import dagger.Module import dagger.Provides @@ -216,4 +217,10 @@ internal object StakingDomainModule { fun provideStakingIdFactory(walletManagersFacade: WalletManagersFacade): StakingIdFactory { return StakingIdFactory(walletManagersFacade = walletManagersFacade) } + + @Provides + @Singleton + fun provideStakingApyFlowUseCase(stakingRepository: StakingRepository): StakingApyFlowUseCase { + return StakingApyFlowUseCase(stakingRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt index de38050dcf..ea0ad78e44 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt @@ -148,4 +148,32 @@ internal object YieldSupplyDomainModule { currenciesRepository = currenciesRepository, ) } + + @Provides + @Singleton + fun provideYieldSupplyGetCurrentFeeUseCase( + feeRepository: FeeRepository, + quotesRepository: QuotesRepository, + currenciesRepository: CurrenciesRepository, + ): YieldSupplyGetCurrentFeeUseCase { + return YieldSupplyGetCurrentFeeUseCase( + feeRepository = feeRepository, + quotesRepository = quotesRepository, + currenciesRepository = currenciesRepository, + ) + } + + @Provides + @Singleton + fun provideYieldSupplyGetMaxFeeUseCase( + yieldSupplyRepository: YieldSupplyRepository, + quotesRepository: QuotesRepository, + currenciesRepository: CurrenciesRepository, + ): YieldSupplyGetMaxFeeUseCase { + return YieldSupplyGetMaxFeeUseCase( + yieldSupplyRepository = yieldSupplyRepository, + quotesRepository = quotesRepository, + currenciesRepository = currenciesRepository, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt index 22b5f4c524..edb7c0365c 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt @@ -112,6 +112,7 @@ internal class DefaultRampManager( userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, ): ScenarioUnavailabilityReason { + val yieldSupplyStatus = cryptoCurrencyStatus.value.yieldSupplyStatus return when { cryptoCurrencyStatus.value.amount.isNullOrZero() -> { ScenarioUnavailabilityReason.EmptyBalance(ScenarioUnavailabilityReason.WithdrawalScenario.SEND) @@ -125,6 +126,9 @@ internal class DefaultRampManager( networkName = cryptoCurrencyStatus.currency.network.name, ) } + yieldSupplyStatus?.isAllowedToSpend == false && yieldSupplyStatus.isActive -> { + ScenarioUnavailabilityReason.YieldSupplyApprovalRequired + } else -> ScenarioUnavailabilityReason.None } } diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 65b9d9e0e9..d3f4ee15cd 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -36,7 +36,6 @@ import com.tangem.features.send.v2.api.SendComponent import com.tangem.features.send.v2.api.SendEntryPointComponent import com.tangem.features.staking.api.StakingComponent import com.tangem.features.swap.SwapComponent -import com.tangem.features.swap.v2.api.SendWithSwapComponent import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent import com.tangem.features.tangempay.components.TangemPayOnboardingComponent import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.ContinueOnboarding @@ -111,7 +110,6 @@ internal class ChildFactory @Inject constructor( private val updateAccessCodeComponentFactory: UpdateAccessCodeComponent.Factory, private val viewPhraseComponentFactory: ViewPhraseComponent.Factory, private val forgetWalletComponentFactory: ForgetWalletComponent.Factory, - private val sendWithSwapComponentFactory: SendWithSwapComponent.Factory, private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory, private val tangemPayDetailsContainerComponentFactory: TangemPayDetailsContainerComponent.Factory, private val tangemPayOnboardingComponentFactory: TangemPayOnboardingComponent.Factory, @@ -585,16 +583,6 @@ internal class ChildFactory @Inject constructor( componentFactory = sendEntryPointComponentFactory, ) } - is AppRoute.SendWithSwap -> { - createComponentChild( - context = context, - params = SendWithSwapComponent.Params( - userWalletId = route.userWalletId, - currency = route.currency, - ), - componentFactory = sendWithSwapComponentFactory, - ) - } is AppRoute.CreateAccount -> { createComponentChild( context = context, @@ -661,7 +649,11 @@ internal class ChildFactory @Inject constructor( is AppRoute.YieldSupplyPromo -> { createComponentChild( context = context, - params = YieldSupplyPromoComponent.Params(route.userWalletId, route.cryptoCurrency), + params = YieldSupplyPromoComponent.Params( + userWalletId = route.userWalletId, + currency = route.cryptoCurrency, + apy = route.apy, + ), componentFactory = yieldSupplyPromoComponentFactory, ) } diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 7e09365956..55e85465d3 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -372,12 +372,6 @@ sealed class AppRoute(val path: String) : Route { path = "/send_entry_point/${userWalletId.stringValue}/${currency.id.value}?", ) - @Serializable - data class SendWithSwap( - val userWalletId: UserWalletId, - val currency: CryptoCurrency, - ) : AppRoute(path = "/send_with_swap/${userWalletId.stringValue}/${currency.symbol}") - @Serializable data class CreateAccount( val userWalletId: UserWalletId, @@ -428,5 +422,6 @@ sealed class AppRoute(val path: String) : Route { data class YieldSupplyPromo( val userWalletId: UserWalletId, val cryptoCurrency: CryptoCurrency, + val apy: String, ) : AppRoute(path = "/yield_supply_promo/${userWalletId.stringValue}/${cryptoCurrency.symbol}") } \ No newline at end of file diff --git a/common/test/src/main/java/com/tangem/common/test/domain/walletmanager/MockUpdateWalletManagerResultFactory.kt b/common/test/src/main/java/com/tangem/common/test/domain/walletmanager/MockUpdateWalletManagerResultFactory.kt index 7abe7f7217..645598c7cd 100644 --- a/common/test/src/main/java/com/tangem/common/test/domain/walletmanager/MockUpdateWalletManagerResultFactory.kt +++ b/common/test/src/main/java/com/tangem/common/test/domain/walletmanager/MockUpdateWalletManagerResultFactory.kt @@ -67,10 +67,12 @@ class MockUpdateWalletManagerResultFactory { value = BigDecimal.ONE, currencyRawId = CryptoCurrency.RawID("token"), contractAddress = "0xTokenAddress", - yieldSupplyStatus = YieldSupplyStatus( + yieldSupplyStatus = + YieldSupplyStatus( isActive = true, isInitialized = true, isAllowedToSpend = false, + effectiveProtocolBalance = BigDecimal.ONE, ), ), ), diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt index 7a8229120e..8c2f75bb85 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt @@ -381,7 +381,11 @@ sealed class NotificationUM(val config: NotificationConfig) { title = TextReference.Res(R.string.send_notification_invalid_amount_title), subtitle = TextReference.Res( id = R.string.send_notification_invalid_amount_rent_fee, - formatArgs = wrappedList(rentInfo.exemptionAmount), + formatArgs = wrappedList( + rentInfo.exemptionAmount.format { + crypto(rentInfo.cryptoCurrency) + }, + ), ), ) diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenActionsUtils.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenActionsUtils.kt index 374cbde833..49a9714fa8 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenActionsUtils.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenActionsUtils.kt @@ -68,6 +68,9 @@ fun ScenarioUnavailabilityReason.getUnavailabilityReasonText(): TextReference { -> { resourceReference(id = R.string.token_button_unavailability_reason_loading) } + ScenarioUnavailabilityReason.YieldSupplyApprovalRequired -> resourceReference( + R.string.token_button_unavailability_reason_yield_supply_approval, + ) ScenarioUnavailabilityReason.None -> { throw IllegalArgumentException("The unavailability reason must be other than None") } 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 de0d5e70fe..0c5042c6d8 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 @@ -7,6 +7,7 @@ import com.tangem.core.ui.components.icons.IconTint import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList @@ -16,14 +17,14 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.StatusSource -import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.yieldSupplyKey +import com.tangem.domain.models.currency.yieldSupplyNotAllAmountSupplied import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.converter.Converter -import com.tangem.utils.extensions.isZero import com.tangem.utils.extensions.orZero import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal @@ -39,12 +40,13 @@ import java.math.BigDecimal */ class TokenItemStateConverter( private val appCurrency: AppCurrency, - private val apyMap: Map = emptyMap(), + private val yieldModuleApyMap: Map = emptyMap(), + private val stakingApyMap: Map = emptyMap(), private val iconStateProvider: (CryptoCurrencyStatus) -> CurrencyIconState = { CryptoCurrencyToIconStateConverter().convert(it) }, private val titleStateProvider: (CryptoCurrencyStatus) -> TokenItemState.TitleState = { - createTitleState(it, apyMap) + createTitleState(it, yieldModuleApyMap, stakingApyMap) }, private val subtitleStateProvider: (CryptoCurrencyStatus) -> TokenItemState.SubtitleState? = { createSubtitleState(it, appCurrency) @@ -153,7 +155,8 @@ class TokenItemStateConverter( private fun createTitleState( currencyStatus: CryptoCurrencyStatus, - apyMap: Map, + yieldModuleApyMap: Map, + stakingApyMap: Map, ): TokenItemState.TitleState { return when (val value = currencyStatus.value) { is CryptoCurrencyStatus.Loading, @@ -168,31 +171,51 @@ class TokenItemStateConverter( is CryptoCurrencyStatus.NoQuote, is CryptoCurrencyStatus.NoAccount, -> { - val earnApyText = resolveEarnApy(currencyStatus, apyMap)?.let { apy -> - resourceReference( - R.string.yield_module_earn_badge, - wrappedList(apy), - ) - } + val (earnApyText, isActive) = resolveEarnApy( + cryptoCurrencyStatus = currencyStatus, + yieldModuleApyMap = yieldModuleApyMap, + stakingApyMap = stakingApyMap, + ) TokenItemState.TitleState.Content( text = stringReference(currencyStatus.currency.name), hasPending = value.hasCurrentNetworkTransactions, earnApy = earnApyText, + earnApyIsActive = isActive, ) } } } - private fun resolveEarnApy(cryptoCurrencyStatus: CryptoCurrencyStatus, apyMap: Map): String? { - if (apyMap.isEmpty()) return null + private fun resolveEarnApy( + cryptoCurrencyStatus: CryptoCurrencyStatus, + yieldModuleApyMap: Map, + stakingApyMap: Map, + ): Pair { + val token = cryptoCurrencyStatus.currency as? CryptoCurrency.Token + if (token != null && yieldModuleApyMap.isNotEmpty()) { + val yieldSupplyApy = yieldModuleApyMap[token.yieldSupplyKey()] + if (yieldSupplyApy != null) { + val isActive = cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive ?: false + return resourceReference( + R.string.yield_module_earn_badge, + wrappedList(yieldSupplyApy), + ) to isActive + } + } - val isYieldSupplyActive = (cryptoCurrencyStatus.value as? CryptoCurrencyStatus.Loaded) - ?.yieldSupplyStatus?.isActive == true - if (isYieldSupplyActive) return null + if (stakingApyMap.isNotEmpty()) { + val stakingKey = cryptoCurrencyStatus.currency.stakingKey() + val stakingApy = stakingApyMap[stakingKey]?.format { percent(withPercentSign = false) } + if (stakingApy != null) { + val hasStakedBalance = cryptoCurrencyStatus.value.yieldBalance is YieldBalance.Data + return resourceReference( + R.string.yield_module_earn_badge, + wrappedList(stakingApy), + ) to hasStakedBalance + } + } - val token = cryptoCurrencyStatus.currency as? CryptoCurrency.Token ?: return null - - return apyMap[token.yieldSupplyKey()] + return null to false } private fun createSubtitleState( @@ -248,20 +271,14 @@ class TokenItemStateConverter( isFlickering = status.value.isFlickering(), icons = buildList { if (status.value.yieldSupplyStatus?.isActive == true && - status.value.yieldSupplyStatus?.isAllowedToSpend == false) { + status.value.yieldSupplyStatus?.isAllowedToSpend == false || + status.yieldSupplyNotAllAmountSupplied() + ) { 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( - iconRes = R.drawable.ic_staking_24, - tint = IconTint.Accent, - ), - ) - } if (status.value.sources.total == StatusSource.ONLY_CACHE) { add( TokenItemState.FiatAmountState.Content.IconUM( @@ -307,5 +324,9 @@ class TokenItemStateConverter( } fun CryptoCurrencyStatus.Value.isFlickering(): Boolean = sources.total == StatusSource.CACHE + + private fun CryptoCurrency.stakingKey(): String { + return "${network.backendId}_$symbol" + } } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt index 0665ec199b..6b7d1ab493 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt @@ -41,32 +41,44 @@ internal class YieldSupply( private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( environment = ApiEnvironment.DEV, baseUrl = "[REDACTED_ENV_URL]", - headers = createHeaders(), + headers = createHeaders(ApiEnvironment.DEV), ) private fun createStageEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( environment = ApiEnvironment.STAGE, baseUrl = "[REDACTED_ENV_URL]", - headers = createHeaders(), + headers = createHeaders(ApiEnvironment.STAGE), ) private fun createMockedEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( environment = ApiEnvironment.MOCK, baseUrl = "[REDACTED_ENV_URL]", - headers = createHeaders(), + headers = createHeaders(ApiEnvironment.MOCK), ) private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( environment = ApiEnvironment.PROD, baseUrl = "https://yield.tangem.org/", - headers = createHeaders(), + headers = createHeaders(ApiEnvironment.PROD), ) - private fun createHeaders() = buildMap { + private fun createHeaders(apiEnvironment: ApiEnvironment) = buildMap { put(key = "api-key", value = ProviderSuspend { - environmentConfigStorage.getConfigSync().yieldModuleApiKey.orEmpty() + getApiKey(apiEnvironment) }) putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider, appInfoProvider).values) putAll(from = RequestHeader.AuthenticationHeader(authProvider).values) } + + private fun getApiKey(apiEnvironment: ApiEnvironment): String { + return when (apiEnvironment) { + ApiEnvironment.MOCK, + ApiEnvironment.DEV, + ApiEnvironment.DEV_2, + ApiEnvironment.DEV_3, + ApiEnvironment.STAGE, + -> environmentConfigStorage.getConfigSync().yieldModuleApiKeyDev + ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().yieldModuleApiKey + } ?: error("No tangem tech api config provided") + } } \ No newline at end of file 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 d6519755c5..b6304ee6a3 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 @@ -19,6 +19,7 @@ interface StakeKitApi { @Query("preferredValidatorsOnly") preferredValidatorsOnly: Boolean? = null, @Query("ledgerWalletAPICompatible") ledgerWalletAPICompatible: Boolean? = null, @Query("type") type: YieldType? = null, + @Query("yieldId") yieldId: String? = null, @Query("revenueOption") revenueOption: RevenueOption? = null, @Query("page") page: Int? = null, @Query("network") network: String? = null, diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt index 384af56181..252782f20a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt @@ -21,4 +21,5 @@ data class EnvironmentConfig( val tangemApiKeyDev: String? = null, val tangemApiKeyStage: String? = null, val yieldModuleApiKey: String? = null, + val yieldModuleApiKeyDev: String? = null, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt index cf52beb064..5c43a7c952 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt @@ -30,6 +30,7 @@ internal object EnvironmentConfigConverter : Converter) { - dataStore.updateData { _ -> - items + dataStore.updateData { data -> + val updatedItems = data.toMutableList() + items.forEach { newItem -> + val existingItemIndex = data.indexOfFirst { it.id == newItem.id } + if (existingItemIndex != -1) { + // Update existing item + updatedItems[existingItemIndex] = newItem + } else { + // Add new item + updatedItems.add(newItem) + } + } + updatedItems } } 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 840ce4bcd1..18c51f8e49 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 @@ -20,6 +20,7 @@ internal class MockEnvironmentConfigStorage : EnvironmentConfigStorage { tangemApiKeyDev = TANGEM_API_KEY_DEV, tangemApiKeyStage = TANGEM_API_KEY_STAGE, yieldModuleApiKey = YIELD_MODULE_KEY, + yieldModuleApiKeyDev = YIELD_MODULE_KEY_DEV, ) override suspend fun initialize() = environmentConfig @@ -34,5 +35,6 @@ internal class MockEnvironmentConfigStorage : EnvironmentConfigStorage { const val TANGEM_API_KEY_DEV = "tangem_api_key_dev" const val TANGEM_API_KEY_STAGE = "tangem_api_key_stage" const val YIELD_MODULE_KEY = "yield_module_api_key" + const val YIELD_MODULE_KEY_DEV = "yield_module_api_key_dev" } } \ No newline at end of file diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 86569cbb95..c5d3a6d70e 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1684,8 +1684,8 @@ Historische Renditen Sofortige Auszahlung Wie funktioniert das? - Verdiene %s%% jährlich - Aave • Variabler Zinssatz + Mit Aave verbinden + Aave %1$s • Variabler Zinssatz Aave Durchschnitt %s Renditen des letzten Jahres @@ -1698,7 +1698,7 @@ Effektiver Jahreszins für Versorgung Lass Dein Geld arbeiten – verdiene Zinsen auf Dein Guthaben. Verdienst auf Dein Guthaben - Verdienen %1$s%% pro Jahr + Lass dein Guthaben arbeiten Der Stakingservice ist derzeit nicht verfügbar. Bitte versuche es später erneut. Einnahmen nicht verfügbar Chart konnte nicht geladen werden... diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index fdbd3598d3..16107ea2c1 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -22,6 +22,7 @@ すでにアクティブアカウントの上限(20件)に達しています。復元するには、1つをアーカイブしてください。 アカウントを復元できません アーカイブ済み + アカウントをアーカイブできませんでした。しばらくしてからもう一度お試しください。 アカウントを作成できませんでした。しばらくしてからもう一度お試しください。 アカウントを作成しました アカウントをアーカイブする @@ -249,10 +250,12 @@ 遅い 速度と料金 終了 + 忘れる 無料 送信元 アドレスを同期する はじめる + トークンを取得 プロバイダーへ移動 トークンへ移動 わかりました @@ -526,14 +529,20 @@ アクセスコードでアプリを保護して、設定を完了してください。 そうした場合は、最初からやり直す必要があります。 本当にアクティベーション処理を終了してもよろしいですか? + 暗号資産をオフラインで厳重保管。カードサイズで、金庫以上の安心を。 実行すると、最初からやり直す必要があります。 Googleドライブのバックアップから既存のウォレットを復元する Googleドライブのバックアップ + さらに強固なセキュリティのために、新しいウォレットを作成して資産を移動しましょう。 + 新しいウォレットを作成 Tangemの高性能ハードウェアウォレットで、セキュリティをさらに強化しましょう。 ハードウェアウォレット + 現在のウォレットをTangemウォレットに移します。 + 現在のウォレットをアップグレードする バックアップへ移動 アクセスコードを作成する前にウォレットをバックアップしてください。 まずバックアップを完了する + その他の方法 秘密鍵をオフラインで安全に保存する物理デバイス。 リカバリーフレーズ 鍵はアプリに保存されます @@ -603,6 +612,7 @@ 選択したトークンは現在、暗号資産ウォレット内でのアクションには利用できません。しかし、心配しないでください。賛成票を投じることで関心を表明できます。 賛成票を投じる ウォレットは複数のネットワークをサポートしていません。 + コインについて このアセットを購入・交換・受け取るには、ポートフォリオに追加してください。 このアセットは現在ウォレットで利用できません このアセットはこのウォレットでは使用できません。 @@ -638,6 +648,7 @@ トレンド ステーキングは暗号資産で報酬を受け取る最も簡単な方法です。 %s 最大%s APYを獲得 + トークンを追加しました %sについて %d取引所 @@ -895,7 +906,7 @@ 最大%d日 %s分 - 下記より利用可能 + 利用可能: 以下が手に入ります。 サービスは外部プロバイダーによって提供されます。 \nTangemは責任を負いません。 この画面を閉じて、トークンの詳細画面で取引状況を確認できます。 @@ -1275,7 +1286,20 @@ 受け取る トークンを選択 利用不可 + 入金 + 異議申し立て + 取引を表示 + サービス手数料 + 手数料 + 完了 + 拒否 + 保留中 + 銀行がこの取引リクエストを拒否しました。 + この手数料は、送金処理にかかるコストをカバーするためのものです。 + 出金 + PINを変更する データの読み込みに失敗しました。しばらくしてからもう一度お試しください。 + カードの一時停止 非表示 技術的な問題が検出されました。しばらくしてからもう一度お試しいただくか、サポートにお問い合わせください。 現在、受け取りは利用できません @@ -1310,6 +1334,7 @@ ネットワーク%s内の保留中の取引が完了すると、送金が可能になります。 %sの売却は、現在のプロバイダーではサポートされていませんが、より多くのオプションを追加できるよう取り組んでいます。 %sのステーキングは、現在のプロバイダーではサポートされていませんが、より多くのオプションを追加できるよう取り組んでいます。 + ここにテキストを入力 XPUBを生成する 非表示 このトークンをメイン画面から非表示にします。トークンの管理ページからいつでも再度追加できます。 @@ -1729,10 +1754,10 @@ 今後の%sの入金はすべて、取引手数料が差し引かれて自動的にAaveに供給されます。 ネットワーク手数料が上限手数料を超えた場合、手数料が下がるまで取引は成立しません。この制限は後で変更できます。 最大手数料 - 取引手数料は、預入額の4%未満である必要があります。Tangemは、この条件を満たす十分な残高が貯まった時点で、Aaveへの資金移動を行います。 + 取引手数料は入金額の4%未満である必要があります。残高がこの条件を満たすのに十分な金額になった場合にのみ、TangemはAaveに資金を送ります。 最低入金額 手数料ポリシー - Tangemはまた、得られた利回りに対して3% のサービス手数料を差し引きます。 + Tangemはまた、得られた利回りに対して3%のサービス手数料を差し引きます。 ネットワーク手数料が現在高すぎます。設定した上限を下回るまで待機しています。 過去のリターン ここに説明を入力してください。1〜3行が理想的です。[プレースホルダー] @@ -1747,8 +1772,8 @@ Aaveは、総額819億ドル以上の資産を管理する分散型プロトコルです。 分散型・自己管理型 サービスを利用することにより、プロバイダー\n %1$sおよび%2$sに同意したことになります - 年間%s%%の収益 - Aave • 変動金利 + Aave を接続 + Aave %1$s%% • 変動金利 Aave 平均%s 昨年のリターン @@ -1771,7 +1796,7 @@ 利息は自動的に発生します Aaveの利回り 入金の処理中 - 年間%1$s%%の収益 + 残高を活用 自動 取引のネットワーク手数料をカバーするために、 %1$s %2$sを入金してください %s手数料を支払えません diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 5ed2ce02b7..168ba8b435 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -192,8 +192,10 @@ Медленно Скорость и комиссия Завершить + Забыть Из Синхронизировать адреса + Начать зарабатывать К провайдеру Перейти в токен Понятно @@ -527,6 +529,7 @@ Выбранный токен не доступен в кошельке на данный момент. Но не переживайте, вы можете выразить свой интерес проголосовав за его добавление. Голосовать Кошелёк не поддерживает более одной сети + О монете Чтобы купить, обменять или получить данный токен, вам нужно добавить его к себе в портфель Этот актив в настоящее время не поддерживается в кошельке Этот токен не доступен для данного кошелька @@ -561,6 +564,7 @@ В тренде Стейкинг — простой способ получать доход с вашей криптовалюты. %s Получайте до %s APY + Токен добавлен О %s %d биржа @@ -838,6 +842,7 @@ до %d дней %s мин + Доступно от Вы получите Вы можете закрыть этот экран и проверить статус транзакции на экране информации о токене. До @@ -1214,6 +1219,7 @@ Вы получите Выберите токен не доступен + Комиссия Это мой кошелек Балансы скрыты Балансы показаны @@ -1590,7 +1596,7 @@ Минимальный депозит Политика комиссий Tangem взимает комиссию за обслуживание в размере 3% от полученного дохода. - Комиссия в сети сейчас слишком высокая. Ждём, пока она упадёт ниже вашего лимита. + Ваши средства будут автоматически переведены в Aave, как только комиссия сети снизится или баланс достигнет минимально необходимой суммы. Историческая доходность Необходимо разрешение для токена Проверьте ваше интернет соединение @@ -1603,10 +1609,10 @@ Aave — это децентрализованный протокол, управляющий активами на сумму более 81,9 миллиарда долларов США. Децентрализованный и некастодиальный Используя сервис, вы соглашаетесь с условиями провайдера %1$s и %2$s - Зарабатывайте %s%% в год - Aave • Ставка с плавающим процентом + Подключить Aave + Aave %1$s%%• Ставка с плавающим процентом Aave - Среднее %s + Сред. %s Доходность за прошлый год Текущая процентная ставка всегда переменная и автоматически рассчитывается смарт-контрактом AAVE в блокчейне на основе текущего спроса и предложения. При поддержке @@ -1618,16 +1624,16 @@ Следующие пополнения вашего счёта автоматически поступят в Aave. Активен На паузе - Закончить зарабатывать + Завершить заработок Выключив эту функцию, вы выведёте средства из Aave, получите их обратно в %s в кошельке и перестанете зарабатывать награды. Комиссия сети будет вычтена из суммы вашего вывода. Годовая доходность (APY) APY Пусть ваши деньги работают — зарабатывайте проценты на свой баланс. Проценты начисляются автоматически. - Доходность Aave + Доходность Отправка ваших средств - Зарабатывайте %1$s%% в год + Пусть ваш баланс работает на вас! Автоматически Внесите немного %1$s %2$s, чтобы покрыть комиссию сети за транзакции. Невозможно покрыть комиссию в %s diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index b74761146f..fa0fd9a2ea 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -992,6 +992,10 @@ I realize that I can\'t use this card to recover my access code on the other cards of the current wallet Factory Reset will completely delete the wallet from the selected card or ring. You will not be able to restore the current wallet or use the card or ring to recover the access code. Factory Reset will completely delete the wallet from the selected card or ring and remove it from the app. You will not be able to restore the current wallet. + All Tangem devices have been reset. + Something went wrong with activation process. Please reset cards one by one. + Card verification failed + Please reset the next device to continue Ring owners get 3 commission-free swaps on Changelly until 15.11! Swap With 0% Fees Now! Log into the app and check your balance without scanning the card or ring @@ -1817,6 +1821,8 @@ APY %1$s%% Available Current APY + When topping up for lending, a network fee will be deducted from the amount — never more than %1$s + The network fee is currently too high to execute lending. Funds will be supplied once it drops to %1$s or below. My funds Your %1$s is now deposited in Aave and earning interest. You hold a%2$s token, which represents your balance and grows over time. When you top up, funds go to Aave to earn interest, minus a transaction fee. Earn @@ -1832,7 +1838,7 @@ Minimal top-up Fee policy Tangem also takes a 3% service fee on the yield earned. - Network fee is too high right now. Waiting until it falls below your limit. + Your funds will be automatically transferred to Aave once network fees are lower or your balance meets the minimum required amount. Historical returns Write description here. In one, two or three lines will be awesome. [PLACEHOLDER] Some token approve needed @@ -1861,16 +1867,16 @@ Your next top-ups will be automatically supplied to Aave. Active Paused - Stop earning + Disable yield mode Turning off will withdraw your funds from Aave, return them to %s in your wallet, and stop earning rewards. The network fee will be deducted from the amount you withdraw. Supply APY APY - Make your money work — earn interest on your balance. + Let your funds work in the background while you stay in control. Interest accrues automatically - Aave yield + Yield mode Processing your deposit - Earn %1$s%% per year + Make your balance work for you Automatic Deposit some %1$s %2$s to cover the network fee for transactions Unable to cover %s fee diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/BoundCounter.kt b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/BoundCounter.kt index 6a97038b3d..7bda686f02 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/BoundCounter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/BoundCounter.kt @@ -31,7 +31,9 @@ internal class BoundCounter( } fun addNextChar() { - string += text[charPosition(string.count())] + val nextIndex = charPosition(string.count()) + if (nextIndex < 0 || nextIndex >= text.length) return + string += text[nextIndex] width += nextCharWidth() _nextCharWidth = null } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenTitle.kt index 3fadd11e81..f762f3b848 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenTitle.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenTitle.kt @@ -3,11 +3,7 @@ package com.tangem.core.ui.components.token.internal import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -63,6 +59,7 @@ private fun ContentTitle(state: TokenTitleState.Content, modifier: Modifier = Mo YieldSupplyApyLabel( apy = state.earnApy, + isActive = state.earnApyIsActive, modifier = Modifier.align(alignment = Alignment.CenterVertically), ) } @@ -81,18 +78,26 @@ private fun CurrencyNameText(name: String, isAvailable: Boolean, modifier: Modif } @Composable -private fun YieldSupplyApyLabel(apy: TextReference?, modifier: Modifier = Modifier) { +private fun YieldSupplyApyLabel(apy: TextReference?, isActive: Boolean, modifier: Modifier = Modifier) { AnimatedVisibility(visible = apy != null, modifier = modifier) { Box( - modifier = modifier.background( - color = TangemTheme.colors.text.accent.copy(alpha = 0.1f), + modifier = Modifier.background( + color = if (isActive) { + TangemTheme.colors.text.accent.copy(alpha = 0.1f) + } else { + TangemTheme.colors.control.unchecked + }, shape = TangemTheme.shapes.roundedCornersSmall2, ), ) { Text( text = apy?.resolveReference().orEmpty(), style = TangemTheme.typography.caption1, - color = TangemTheme.colors.text.accent, + color = if (isActive) { + TangemTheme.colors.text.accent + } else { + TangemTheme.colors.text.secondary + }, modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp), ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt index f86a303999..88fc97f796 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt @@ -169,6 +169,7 @@ sealed class TokenItemState { val hasPending: Boolean = false, val isAvailable: Boolean = true, val earnApy: TextReference? = null, + val earnApyIsActive: Boolean = false, ) : TitleState() data object Loading : TitleState() diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalPercentFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalPercentFormat.kt index 84e5870e68..8d22eff0a6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalPercentFormat.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalPercentFormat.kt @@ -7,6 +7,7 @@ import java.util.Locale class BigDecimalPercentFormat( val isWithoutSign: Boolean = true, + val withPercentSign: Boolean = true, val locale: Locale = Locale.getDefault(), ) : BigDecimalFormat { override fun invoke(value: BigDecimal): String = default()(value) @@ -16,24 +17,30 @@ class BigDecimalPercentFormat( fun BigDecimalFormatScope.percent( withoutSign: Boolean = true, + withPercentSign: Boolean = true, locale: Locale = Locale.getDefault(), ): BigDecimalPercentFormat { return BigDecimalPercentFormat( isWithoutSign = withoutSign, locale = locale, + withPercentSign = withPercentSign, ) } // == Formatters == private fun BigDecimalPercentFormat.default(): BigDecimalFormat = BigDecimalFormat { value -> - val formatter = NumberFormat.getPercentInstance(locale).apply { + val formatter = if (withPercentSign) { + NumberFormat.getPercentInstance(locale) + } else { + NumberFormat.getNumberInstance(locale) + }.apply { maximumFractionDigits = 2 minimumFractionDigits = 2 roundingMode = RoundingMode.HALF_UP } - val valueToFormat = if (isWithoutSign) value.abs() else value + val finalValue = if (withPercentSign) valueToFormat else valueToFormat.movePointRight(2) - formatter.format(valueToFormat) + formatter.format(finalValue) } \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/TangemBlogUrlBuilder.kt b/core/utils/src/main/java/com/tangem/utils/TangemBlogUrlBuilder.kt index 4b6406002d..3bb43d00e2 100644 --- a/core/utils/src/main/java/com/tangem/utils/TangemBlogUrlBuilder.kt +++ b/core/utils/src/main/java/com/tangem/utils/TangemBlogUrlBuilder.kt @@ -22,4 +22,6 @@ object TangemBlogUrlBuilder { const val RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP = "https://tangem.com/en/blog/post/give-revoke-permission/" const val YIELD_SUPPLY_HOW_IT_WORKS_URL = "https://tangem.com/en/blog/post/savings-account" + const val YIELD_SUPPLY_TOS_URL = "https://aave.com/terms-of-service" + const val YIELD_SUPPLY_PRIVACY_URL = "https://aave.com/privacy-policy" } \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt index f8e13d53fc..067f886293 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt @@ -41,6 +41,7 @@ internal object TokenMarketListConverter : Converter stakingYieldsStore.store( - stakingTokensWithYields.data.data.filter { - it.isAvailable == true - }, - ) - else -> { - stakingYieldsStore.store(emptyList()) - throw (stakingTokensWithYields as ApiResponse.Error).cause + val yieldsResponses = getAvailableIntegrationsIds().map { + async { it.getYieldRequest() } + }.awaitAll() + + val yields = yieldsResponses.flatMap { response -> + when (response) { + is ApiResponse.Success -> response.data.data.filter { yield -> yield.isAvailable == true } + else -> { + Timber.e("Error fetching enabled yields: ${(response as? ApiResponse.Error)?.cause}") + emptyList() + } } } + stakingYieldsStore.store(yields) } } @@ -151,6 +157,26 @@ internal class DefaultStakingRepository( } } + private suspend fun StakingIntegrationID.getYieldRequest(): ApiResponse { + return when (this) { + is StakingIntegrationID.Coin -> stakeKitApi.getEnabledYields( + preferredValidatorsOnly = false, + network = networkId, + ) + is StakingIntegrationID.EthereumToken -> stakeKitApi.getEnabledYields( + preferredValidatorsOnly = false, + yieldId = value, + network = networkId, + ) + } + } + + private fun getAvailableIntegrationsIds(): List { + return StakingIntegrationID.entries.filterNot { + it.blockchain == Blockchain.Cardano && !stakingFeatureToggles.isCardanoStakingEnabled + } + } + private fun NetworkTypeDTO.extractJsonName(): String { return networkTypeAdapter.toJson(this).replace("\"", "") } @@ -364,7 +390,8 @@ internal class DefaultStakingRepository( ) val transaction = transactionConverter.convert(transactionResponse.getOrThrow()) - val unsignedTransaction = transaction.unsignedTransaction ?: error("No unsigned transaction available") + val unsignedTransaction = + transaction.unsignedTransaction ?: error("No unsigned transaction available") val transactionData = TransactionData.Compiled( value = getTransactionDataType(networkId, unsignedTransaction), fee = fee, @@ -473,7 +500,7 @@ internal class DefaultStakingRepository( ) } - private fun getEnabledYields(): Flow> { + override fun getEnabledYields(): Flow> { return stakingYieldsStore.get().map { YieldConverter.convertListIgnoreErrors( input = it, diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt index f374404cbe..48ac28966a 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt @@ -1,10 +1,8 @@ package com.tangem.data.tokens.repository import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider -import com.tangem.blockchain.common.FeeResourceAmountProvider -import com.tangem.blockchain.common.MinimumSendAmountProvider -import com.tangem.blockchain.common.ReserveAmountProvider -import com.tangem.blockchain.common.UtxoAmountLimitProvider +import com.tangem.blockchain.common.* +import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.data.tokens.converters.UtxoConverter import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -21,6 +19,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.isZero import com.tangem.utils.extensions.orZero import kotlinx.coroutines.withContext +import timber.log.Timber import java.math.BigDecimal internal class DefaultCurrencyChecksRepository( @@ -141,7 +140,11 @@ internal class DefaultCurrencyChecksRepository( return when { balanceValue.amount.isZero() && stakingTotalBalance.isZero() -> null balanceValue.amount < rentData.exemptionAmount && stakingTotalBalance.isZero() -> { - CryptoCurrencyWarning.Rent(rentData.rent, rentData.exemptionAmount) + CryptoCurrencyWarning.Rent( + rent = rentData.rent, + exemptionAmount = rentData.exemptionAmount, + cryptoCurrency = currencyStatus.currency, + ) } else -> null } @@ -156,9 +159,36 @@ internal class DefaultCurrencyChecksRepository( return when { balanceAfterTransaction.isZero() -> null balanceAfterTransaction < rentData.exemptionAmount -> { - CryptoCurrencyWarning.Rent(rentData.rent, rentData.exemptionAmount) + CryptoCurrencyWarning.Rent( + rent = rentData.rent, + exemptionAmount = rentData.exemptionAmount, + cryptoCurrency = currencyStatus.currency, + ) } else -> null } } + + override suspend fun getProtocolBalance( + userWalletId: UserWalletId, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ): BigDecimal? { + val token = cryptoCurrencyStatus.currency as? CryptoCurrency.Token ?: return null + val isActive = cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive ?: false + if (!isActive) return null + return runCatching { + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = token.network.toBlockchain(), + derivationPath = token.network.derivationPath.value, + ) ?: error("Wallet manager not found") + walletManager.getEffectiveProtocolBalance( + token = Token( + symbol = token.symbol, + contractAddress = token.contractAddress, + decimals = token.decimals, + ), + ) + }.onFailure(Timber::e).getOrThrow() + } } \ No newline at end of file diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/UpdateWalletManagerResultFactory.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/UpdateWalletManagerResultFactory.kt index 55054ba3c8..a8d65edb01 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/UpdateWalletManagerResultFactory.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/UpdateWalletManagerResultFactory.kt @@ -128,6 +128,7 @@ internal class UpdateWalletManagerResultFactory { isActive = type.isActive, isInitialized = type.isInitialized, isAllowedToSpend = type.isAllowedToSpend, + effectiveProtocolBalance = type.effectiveProtocolBalance, ), ) } diff --git a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyTransactionRepository.kt b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyTransactionRepository.kt index 12c2a98044..c761bd1160 100644 --- a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyTransactionRepository.kt +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyTransactionRepository.kt @@ -91,24 +91,26 @@ internal class DefaultYieldSupplyTransactionRepository( ) } - override suspend fun getProtocolBalance(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): BigDecimal? = - withContext(dispatchers.io) { - require(cryptoCurrency is CryptoCurrency.Token) - runCatching { - val walletManager = walletManagersFacade.getOrCreateWalletManager( - userWalletId = userWalletId, - blockchain = cryptoCurrency.network.toBlockchain(), - derivationPath = cryptoCurrency.network.derivationPath.value, - ) ?: error("Wallet manager not found") - walletManager.getProtocolBalance( - token = Token( - symbol = cryptoCurrency.symbol, - contractAddress = cryptoCurrency.contractAddress, - decimals = cryptoCurrency.decimals, - ), - ) - }.onFailure(Timber::e).getOrThrow() - } + override suspend fun getEffectiveProtocolBalance( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): BigDecimal? = withContext(dispatchers.io) { + require(cryptoCurrency is CryptoCurrency.Token) + runCatching { + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = cryptoCurrency.network.toBlockchain(), + derivationPath = cryptoCurrency.network.derivationPath.value, + ) ?: error("Wallet manager not found") + walletManager.getEffectiveProtocolBalance( + token = Token( + symbol = cryptoCurrency.symbol, + contractAddress = cryptoCurrency.contractAddress, + decimals = cryptoCurrency.decimals, + ), + ) + }.onFailure(Timber::e).getOrThrow() + } @Suppress("LongParameterList") private suspend fun buildEnterTransactions( @@ -222,6 +224,17 @@ internal class DefaultYieldSupplyTransactionRepository( ): YieldSupplyStatus? = withContext(dispatchers.io) { runCatching { val sdkSupplyStatus = walletManager.getYieldSupplyStatus(cryptoCurrency.contractAddress) + val protocolBalance = if (sdkSupplyStatus?.isActive == true) { + walletManager.getEffectiveProtocolBalance( + token = Token( + symbol = cryptoCurrency.symbol, + contractAddress = cryptoCurrency.contractAddress, + decimals = cryptoCurrency.decimals, + ), + ) + } else { + null + } val isAllowedToSpend = walletManager.isAllowedToSpend( Token( symbol = cryptoCurrency.symbol, @@ -234,6 +247,7 @@ internal class DefaultYieldSupplyTransactionRepository( isActive = sdkSupplyStatus?.isActive == true, isInitialized = sdkSupplyStatus?.isInitialized == true, isAllowedToSpend = isAllowedToSpend, + effectiveProtocolBalance = protocolBalance, ) }.onFailure(Timber::e).getOrNull() } diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt index d2a80e71cf..36b179897a 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt @@ -13,6 +13,7 @@ data class TokenMarket( val tokenQuotesShort: TokenQuotesShort, val tokenCharts: Charts, val stakingRate: BigDecimal?, + val updateTimestamp: Long?, private val imageHost: String, ) { diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensions.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensions.kt index b3fb460bdb..532c2f1621 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensions.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensions.kt @@ -2,4 +2,20 @@ package com.tangem.domain.models.currency fun CryptoCurrency.Token.yieldSupplyKey(): String { return "${network.backendId}_$contractAddress" +} + +fun CryptoCurrencyStatus.yieldSupplyNotAllAmountSupplied(): Boolean { + if (this.currency !is CryptoCurrency.Token) return false + + val supplyStatus = this.value.yieldSupplyStatus + if (supplyStatus?.isActive != true) return false + + val protocolBalance = supplyStatus.effectiveProtocolBalance + val amount = this.value.amount + + return if (protocolBalance != null && amount != null) { + amount > protocolBalance + } else { + false + } } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/yield/supply/YieldSupplyStatus.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/yield/supply/YieldSupplyStatus.kt index 3a8165a366..138dc5ad5b 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/yield/supply/YieldSupplyStatus.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/yield/supply/YieldSupplyStatus.kt @@ -1,5 +1,6 @@ package com.tangem.domain.models.yield.supply +import com.tangem.domain.models.serialization.SerializedBigDecimal import kotlinx.serialization.Serializable /** @@ -11,10 +12,12 @@ import kotlinx.serialization.Serializable * @property isActive Indicates if the yield token is currently active. * @property isInitialized Indicates if the yield token has been initialized. * @property isAllowedToSpend Indicates if spending from the yield module is permitted. - */ + * @property effectiveProtocolBalance Indicates the balance (excluding service fee) + * */ @Serializable data class YieldSupplyStatus( val isActive: Boolean, val isInitialized: Boolean, val isAllowedToSpend: Boolean, + val effectiveProtocolBalance: SerializedBigDecimal?, ) \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingIntegrationID.kt b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingIntegrationID.kt index f1514e69a5..ff38b75b71 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingIntegrationID.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingIntegrationID.kt @@ -23,31 +23,43 @@ sealed interface StakingIntegrationID { /** Approval requirements for the staking integration. Defaults to no approval needed */ val approval: StakingApproval get() = StakingApproval.Empty + /** + * Represents the network ID associated with the staking integration from provider + * https://docs.yield.xyz/reference/yieldscontroller_getyields + */ + val networkId: String + /** Represents blockchains whose native coins can be staked */ enum class Coin : StakingIntegrationID { Ton { override val value: String = "ton-ton-chorus-one-pools-staking" override val blockchain: Blockchain = Blockchain.TON + override val networkId: String = "ton" }, Solana { override val value: String = "solana-sol-native-multivalidator-staking" override val blockchain: Blockchain = Blockchain.Solana + override val networkId: String = "solana" }, Cosmos { override val value: String = "cosmos-atom-native-staking" override val blockchain: Blockchain = Blockchain.Cosmos + override val networkId: String = "cosmos" }, Tron { override val value: String = "tron-trx-native-staking" override val blockchain: Blockchain = Blockchain.Tron + override val networkId: String = "tron" }, BSC { override val value: String = "bsc-bnb-native-staking" override val blockchain: Blockchain = Blockchain.BSC + override val networkId: String = "binance" }, Cardano { override val value: String = "cardano-ada-native-staking" override val blockchain: Blockchain = Blockchain.Cardano + override val networkId: String = "cardano" }, } @@ -61,6 +73,7 @@ sealed interface StakingIntegrationID { override val value: String = "ethereum-matic-native-staking" override val approval: StakingApproval.Needed = StakingApproval.Needed(spenderAddress = "0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908") + override val networkId: String = "ethereum" }, ; 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 index efbbae2c38..7e360eb780 100644 --- 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 @@ -22,6 +22,8 @@ interface StakingRepository { suspend fun fetchEnabledYields() + fun getEnabledYields(): Flow> + suspend fun getEntryInfo(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): StakingEntryInfo suspend fun getYield(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Yield diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/usecase/StakingApyFlowUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/usecase/StakingApyFlowUseCase.kt new file mode 100644 index 0000000000..5138158704 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/usecase/StakingApyFlowUseCase.kt @@ -0,0 +1,37 @@ +package com.tangem.domain.staking.usecase + +import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.repositories.StakingRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import java.math.BigDecimal + +/** + * Emits a map of APY values per currency for staking. + * + * Return map: + * - key: currency staking key (network.backendId + "_" + symbol) + * - value: APY as string + */ +class StakingApyFlowUseCase(private val stakingRepository: StakingRepository) { + + operator fun invoke(): Flow> { + return stakingRepository.getEnabledYields() + .map { yields -> + yields.associate { yield -> + val key = "${yield.token.network.name.lowercase()}_${yield.token.symbol}" + val apy = calculateApy(yield) + key to apy + } + } + } + + private fun calculateApy(yield: Yield): BigDecimal { + val rates = yield.validators.mapNotNull { it.rewardInfo?.rate } + return if (rates.isNotEmpty()) { + rates.maxOf { it } + } else { + yield.apy + } + } +} \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt index bf816df605..64053517d7 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt @@ -56,6 +56,8 @@ sealed class ScenarioUnavailabilityReason { data object TrustlineRequired : ScenarioUnavailabilityReason() + data object YieldSupplyApprovalRequired : ScenarioUnavailabilityReason() + enum class WithdrawalScenario { SELL, SEND // TODO staking create&process STAKING } diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt index b33b3562ed..3365ce6d87 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt @@ -192,6 +192,7 @@ sealed class TokenScreenAnalyticsEvent( is ScenarioUnavailabilityReason.NotExchangeable, is ScenarioUnavailabilityReason.NotSupportedBySellService, is ScenarioUnavailabilityReason.StakingUnavailable, + ScenarioUnavailabilityReason.YieldSupplyApprovalRequired, -> UNAVAILABLE ScenarioUnavailabilityReason.UnassociatedAsset -> ASSET_REQUIREMENT ScenarioUnavailabilityReason.TrustlineRequired -> TRUSTLINE_REQUIREMENT diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt index 083b0aba95..cb4b78542e 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt @@ -39,8 +39,13 @@ sealed class CryptoCurrencyWarning { * @param rent Amount that will be charged in overtime if the blockchain does not have an amount greater than * the [exemptionAmount] * @param exemptionAmount Amount that should be on the blockchain balance not to pay rent + * @param cryptoCurrency Currency in which the rent is charged */ - data class Rent(val rent: BigDecimal, val exemptionAmount: BigDecimal) : CryptoCurrencyWarning() + data class Rent( + val rent: BigDecimal, + val exemptionAmount: BigDecimal, + val cryptoCurrency: CryptoCurrency, + ) : CryptoCurrencyWarning() data class SwapPromo( val promoId: PromoId, @@ -68,4 +73,10 @@ sealed class CryptoCurrencyWarning { val requiredAmount: BigDecimal, val currencyDecimals: Int, ) : CryptoCurrencyWarning() + + data class YieldSupplyNotDepositedAmount( + val currency: CryptoCurrency, + val currencySymbol: String, + val amount: BigDecimal, + ) : CryptoCurrencyWarning() } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index d4611e9d40..d491aff2a6 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -51,7 +51,8 @@ class GetCurrencyWarningsUseCase( flowOf(currencyChecksRepository.getRentInfoWarning(userWalletId, currencyStatus)), flowOf(currencyChecksRepository.getExistentialDeposit(userWalletId, currency.network)), flowOf(currencyChecksRepository.getFeeResourceAmount(userWalletId, currency.network)), - ) { coinRelatedWarnings, maybeRentWarning, maybeEdWarning, maybeFeeResource -> + flowOf(currencyChecksRepository.getProtocolBalance(userWalletId, currencyStatus)), + ) { coinRelatedWarnings, maybeRentWarning, maybeEdWarning, maybeFeeResource, yieldSupplyProtocolBalance -> setOfNotNull( maybeRentWarning, maybeEdWarning?.let { getExistentialDepositWarning(currency, it) }, @@ -62,6 +63,7 @@ class GetCurrencyWarningsUseCase( getBeaconChainShutdownWarning(rawId = currency.network.id.rawId), getAssetRequirementsWarning(userWalletId = userWalletId, currency = currency), getMigrationFromMaticToPolWarning(currency), + getYieldSupplyWarning(cryptoCurrencyStatus = currencyStatus, yieldSupplyProtocolBalance), ) }.flowOn(dispatchers.io) } @@ -261,6 +263,28 @@ class GetCurrencyWarningsUseCase( } } + private fun getYieldSupplyWarning( + cryptoCurrencyStatus: CryptoCurrencyStatus, + protocolBalance: BigDecimal?, + ): CryptoCurrencyWarning? { + val value = cryptoCurrencyStatus.value + val isActive = value.yieldSupplyStatus?.isActive == true + val amount = value.amount + + if (!isActive || protocolBalance == null || amount == null) return null + + val notDepositedAmount = amount.minus(protocolBalance) + return if (notDepositedAmount > BigDecimal.ZERO) { + CryptoCurrencyWarning.YieldSupplyNotDepositedAmount( + currency = cryptoCurrencyStatus.currency, + amount = notDepositedAmount, + currencySymbol = cryptoCurrencyStatus.currency.symbol, + ) + } else { + null + } + } + private fun BigDecimal?.isZero(): Boolean { return this?.signum() == 0 } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/NeedShowYieldSupplyDepositedWarningUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/NeedShowYieldSupplyDepositedWarningUseCase.kt index 417e84e987..fa90d45903 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/NeedShowYieldSupplyDepositedWarningUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/NeedShowYieldSupplyDepositedWarningUseCase.kt @@ -5,15 +5,18 @@ import com.tangem.domain.tokens.repository.YieldSupplyWarningsViewedRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext +@Suppress("UnusedPrivateProperty") class NeedShowYieldSupplyDepositedWarningUseCase( private val yieldSupplyWarningsViewedRepository: YieldSupplyWarningsViewedRepository, private val dispatchers: CoroutineDispatcherProvider, ) { suspend operator fun invoke(cryptoCurrencyStatus: CryptoCurrencyStatus?): Boolean = withContext(dispatchers.io) { - val hasActiveLending = cryptoCurrencyStatus?.value?.yieldSupplyStatus?.isActive == true - if (!hasActiveLending) return@withContext false - val showedWarnings = yieldSupplyWarningsViewedRepository.getViewedWarnings() - return@withContext !showedWarnings.contains(cryptoCurrencyStatus?.currency?.name) + // TEMPORARY REQUIREMENTS + return@withContext false + // val hasActiveLending = cryptoCurrencyStatus?.value?.yieldSupplyStatus?.isActive == true + // if (!hasActiveLending) return@withContext false + // val showedWarnings = yieldSupplyWarningsViewedRepository.getViewedWarnings() + // return@withContext !showedWarnings.contains(cryptoCurrencyStatus?.currency?.name) } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt index 180041ff13..bef54bd155 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt @@ -1,7 +1,6 @@ package com.tangem.domain.tokens.actions import com.tangem.domain.exchange.RampStateManager -import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -69,7 +68,7 @@ internal class CommonActionsFactory( async { getSwapUnavailabilityReason( userWalletId = userWallet.walletId, - currency = cryptoCurrencyStatus.currency, + currencyStatus = cryptoCurrencyStatus, requirementsDeferred = requirementsDeferred, ) } @@ -175,17 +174,21 @@ internal class CommonActionsFactory( private suspend fun getSwapUnavailabilityReason( userWalletId: UserWalletId, - currency: CryptoCurrency, + currencyStatus: CryptoCurrencyStatus, requirementsDeferred: Deferred?, ): ScenarioUnavailabilityReason { val swapUnavailabilityReason = rampStateManager - .availableForSwap(userWalletId = userWalletId, cryptoCurrency = currency) + .availableForSwap(userWalletId = userWalletId, cryptoCurrency = currencyStatus.currency) val shouldCheckAssetRequirements = swapUnavailabilityReason == ScenarioUnavailabilityReason.None && requirementsDeferred != null - return if (shouldCheckAssetRequirements) { - getReceiveScenario(requirementsDeferred.await()) - } else { - swapUnavailabilityReason + + val yieldSupplyStatus = currencyStatus.value.yieldSupplyStatus + val isUnavailableByYieldSupply = yieldSupplyStatus?.isAllowedToSpend == false && yieldSupplyStatus.isActive + + return when { + isUnavailableByYieldSupply -> ScenarioUnavailabilityReason.YieldSupplyApprovalRequired + shouldCheckAssetRequirements -> getReceiveScenario(requirementsDeferred.await()) + else -> swapUnavailabilityReason } } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrencyChecksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrencyChecksRepository.kt index 8351958b10..d1b4b99d81 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrencyChecksRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrencyChecksRepository.kt @@ -61,4 +61,11 @@ interface CurrencyChecksRepository { currencyStatus: CryptoCurrencyStatus, balanceAfterTransaction: BigDecimal, ): CryptoCurrencyWarning.Rent? + + /** + * Returns the YieldSupplied protocol balance in Aave for the given `cryptoCurrency`. + * This represents the amount supplied to the protocol for the specified `userWalletId` + * (e.g., aTokens balance). Returns null if not applicable or unknown. + */ + suspend fun getProtocolBalance(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus): BigDecimal? } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/NeedShowYieldSupplyDepositedWarningUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/NeedShowYieldSupplyDepositedWarningUseCaseTest.kt deleted file mode 100644 index 1873ddeed3..0000000000 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/NeedShowYieldSupplyDepositedWarningUseCaseTest.kt +++ /dev/null @@ -1,107 +0,0 @@ -package com.tangem.domain.tokens - -import com.google.common.truth.Truth.assertThat -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.network.NetworkAddress -import com.tangem.domain.models.serialization.SerializedBigDecimal -import com.tangem.domain.models.yield.supply.YieldSupplyStatus -import com.tangem.domain.tokens.repository.YieldSupplyWarningsViewedRepository -import com.tangem.domain.tokens.mock.MockTokens -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.impl.annotations.RelaxedMockK -import io.mockk.junit5.MockKExtension -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith - -@OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockKExtension::class) -class NeedShowYieldSupplyDepositedWarningUseCaseTest { - - @RelaxedMockK - private lateinit var repository: YieldSupplyWarningsViewedRepository - - private lateinit var dispatchers: TestingCoroutineDispatcherProvider - - @BeforeEach - fun setup() { - dispatchers = TestingCoroutineDispatcherProvider() - } - - @Test - fun `GIVEN null status WHEN invoke THEN returns false`() = runTest { - val useCase = NeedShowYieldSupplyDepositedWarningUseCase(repository, dispatchers) - - val result = useCase.invoke(null) - - assertThat(result).isFalse() - coVerify(exactly = 0) { repository.getViewedWarnings() } - } - - @Test - fun `GIVEN inactive lending WHEN invoke THEN returns false`() = runTest { - val useCase = NeedShowYieldSupplyDepositedWarningUseCase(repository, dispatchers) - val status = createStatus(isActive = false) - - val result = useCase.invoke(status) - - assertThat(result).isFalse() - coVerify(exactly = 0) { repository.getViewedWarnings() } - } - - @Test - fun `GIVEN active lending and not viewed WHEN invoke THEN returns true`() = runTest { - val useCase = NeedShowYieldSupplyDepositedWarningUseCase(repository, dispatchers) - val status = createStatus(isActive = true) - coEvery { repository.getViewedWarnings() } returns emptySet() - - val result = useCase.invoke(status) - - assertThat(result).isTrue() - coVerify(exactly = 1) { repository.getViewedWarnings() } - } - - @Test - fun `GIVEN active lending and already viewed WHEN invoke THEN returns false`() = runTest { - val useCase = NeedShowYieldSupplyDepositedWarningUseCase(repository, dispatchers) - val status = createStatus(isActive = true) - coEvery { repository.getViewedWarnings() } returns setOf(status.currency.name) - - val result = useCase.invoke(status) - - assertThat(result).isFalse() - coVerify(exactly = 1) { repository.getViewedWarnings() } - } - - private fun createStatus(isActive: Boolean): CryptoCurrencyStatus { - val currency = MockTokens.token1 - val yieldSupplyStatus = YieldSupplyStatus( - isActive = isActive, - isInitialized = true, - isAllowedToSpend = true, - ) - val value = CryptoCurrencyStatus.NoQuote( - amount = SerializedBigDecimal.ZERO, - yieldBalance = null, - yieldSupplyStatus = yieldSupplyStatus, - hasCurrentNetworkTransactions = false, - pendingTransactions = emptySet(), - networkAddress = NetworkAddress.Single( - defaultAddress = NetworkAddress.Address( - value = "address", - type = NetworkAddress.Address.Type.Primary, - ), - ), - sources = CryptoCurrencyStatus.Sources(), - ) - - return CryptoCurrencyStatus( - currency = currency, - value = value, - ) - } -} \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/FeeExtensions.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/FeeExtensions.kt new file mode 100644 index 0000000000..4032d39149 --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/FeeExtensions.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.yield.supply + +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.domain.models.currency.CryptoCurrency +import java.math.BigInteger + +fun Fee.fixFee(cryptoCurrency: CryptoCurrency, gasLimit: BigInteger): Fee = when (this) { + is Fee.Ethereum.Legacy -> copy( + gasLimit = gasLimit, + amount = amount.copy( + value = gasPrice.multiply(gasLimit) + .toBigDecimal().movePointLeft(cryptoCurrency.decimals), + ), + ) + is Fee.Ethereum.EIP1559 -> copy( + gasLimit = gasLimit, + amount = amount.copy( + value = maxFeePerGas.multiply(gasLimit) + .toBigDecimal().movePointLeft(cryptoCurrency.decimals), + ), + ) + else -> this +} \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyConst.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyConst.kt new file mode 100644 index 0000000000..f4ae8729f3 --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyConst.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.yield.supply + +object YieldSupplyConst { + val YIELD_SUPPLY_EVM_CONSTANT_GAS_LIMIT = 350_000.toBigInteger() +} \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyTransactionRepository.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyTransactionRepository.kt index 68982a6e66..dccf5c9e1d 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyTransactionRepository.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyTransactionRepository.kt @@ -23,5 +23,5 @@ interface YieldSupplyTransactionRepository { suspend fun getYieldContractAddress(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): String? - suspend fun getProtocolBalance(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): BigDecimal? + suspend fun getEffectiveProtocolBalance(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): BigDecimal? } \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEstimateEnterFeeUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEstimateEnterFeeUseCase.kt index 273114f51c..58f1a8476f 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEstimateEnterFeeUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEstimateEnterFeeUseCase.kt @@ -3,17 +3,16 @@ package com.tangem.domain.yield.supply.usecase import arrow.core.Either import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.common.TransactionData -import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplyEnterCallData import com.tangem.domain.blockaid.BlockAidGasEstimate import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.yield.supply.fixFee import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.error.FeeErrorResolver import com.tangem.domain.transaction.error.GetFeeError import com.tangem.utils.extensions.isSingleItem import timber.log.Timber -import java.math.BigInteger class YieldSupplyEstimateEnterFeeUseCase( private val feeRepository: FeeRepository, @@ -107,24 +106,6 @@ class YieldSupplyEstimateEnterFeeUseCase( return withCalculatedFees + withEstimatedFees } - private fun Fee.fixFee(cryptoCurrency: CryptoCurrency, gasLimit: BigInteger) = when (this) { - is Fee.Ethereum.Legacy -> copy( - gasLimit = gasLimit, - amount = amount.copy( - value = gasPrice.multiply(gasLimit) - .toBigDecimal().movePointLeft(cryptoCurrency.decimals), - ), - ) - is Fee.Ethereum.EIP1559 -> copy( - gasLimit = gasLimit, - amount = amount.copy( - value = maxFeePerGas.multiply(gasLimit) - .toBigDecimal().movePointLeft(cryptoCurrency.decimals), - ), - ) - else -> this - } - private companion object { // Using constant gas limit to avoid fee calculation errors when contract address is not deployed yet val ETHEREUM_CONSTANT_GAS_LIMIT = 500_000.toBigInteger() diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCase.kt new file mode 100644 index 0000000000..c9ee2d6a93 --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCase.kt @@ -0,0 +1,65 @@ +package com.tangem.domain.yield.supply.usecase + +import arrow.core.Either +import arrow.core.Either.Companion.catch +import com.tangem.domain.yield.supply.fixFee +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.quotes.QuotesRepository +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.transaction.FeeRepository +import com.tangem.domain.yield.supply.YieldSupplyConst.YIELD_SUPPLY_EVM_CONSTANT_GAS_LIMIT +import java.math.BigDecimal +import java.math.RoundingMode + +/** + * Calculates current fee for Yield Supply enter transaction expressed in token units. + */ +class YieldSupplyGetCurrentFeeUseCase( + private val feeRepository: FeeRepository, + private val quotesRepository: QuotesRepository, + private val currenciesRepository: CurrenciesRepository, +) { + + suspend operator fun invoke( + userWallet: UserWallet, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ): Either = catch { + val feeWithoutGas = feeRepository.getEthereumFeeWithoutGas(userWallet, cryptoCurrencyStatus.currency) + + val fiatRate = cryptoCurrencyStatus.value.fiatRate ?: error("Fiat rate is missing") + require(fiatRate > BigDecimal.ZERO) { "Fiat rate for token must be > 0" } + + val nativeCryptoCurrency = currenciesRepository.getNetworkCoin( + userWalletId = userWallet.walletId, + networkId = cryptoCurrencyStatus.currency.network.id, + derivationPath = cryptoCurrencyStatus.currency.network.derivationPath, + ) + + val quotes = + quotesRepository.getMultiQuoteSyncOrNull(setOfNotNull(nativeCryptoCurrency.id.rawCurrencyId)) + ?: error("Quotes for native coin are unavailable") + + val quotesStatus = quotes.firstOrNull() ?: error("Empty quotes list for native coin") + + val nativeFiatRate = (quotesStatus.value as? QuoteStatus.Data)?.fiatRate + ?: error("Native fiat rate is missing") + require(nativeFiatRate > BigDecimal.ZERO) { "Native fiat rate must be > 0" } + + val nativeGas = feeWithoutGas.fixFee( + nativeCryptoCurrency, + YIELD_SUPPLY_EVM_CONSTANT_GAS_LIMIT, + ) + + val rateRatio = nativeFiatRate.divide( + fiatRate, + cryptoCurrencyStatus.currency.decimals, + RoundingMode.HALF_UP, + ) + + val tokenValue = rateRatio.multiply(nativeGas.amount.value) + + tokenValue.stripTrailingZeros() + } +} \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetMaxFeeUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetMaxFeeUseCase.kt new file mode 100644 index 0000000000..e463368cd3 --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetMaxFeeUseCase.kt @@ -0,0 +1,67 @@ +package com.tangem.domain.yield.supply.usecase + +import arrow.core.Either +import arrow.core.Either.Companion.catch +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.quotes.QuotesRepository +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.yield.supply.YieldSupplyRepository +import java.math.BigDecimal +import java.math.RoundingMode + +/** + * Calculates max allowed network fee for Yield Supply enter transaction expressed in token units. + * + * Uses YieldMarketToken.maxFeeNative (native coin units) and converts it to token units with the + * same conversion logic as [YieldSupplyGetCurrentFeeUseCase]: based on fiat rate ratio + * (nativeFiatRate / tokenFiatRate). + */ +class YieldSupplyGetMaxFeeUseCase( + private val yieldSupplyRepository: YieldSupplyRepository, + private val quotesRepository: QuotesRepository, + private val currenciesRepository: CurrenciesRepository, +) { + + suspend operator fun invoke( + userWallet: UserWallet, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ): Either = catch { + val token = cryptoCurrencyStatus.currency as? CryptoCurrency.Token + ?: error("CryptoCurrency must be token for max fee calculation") + + val fiatRate = cryptoCurrencyStatus.value.fiatRate ?: error("Fiat rate is missing") + require(fiatRate > BigDecimal.ZERO) { "Fiat rate for token must be > 0" } + + val nativeCryptoCurrency = currenciesRepository.getNetworkCoin( + userWalletId = userWallet.walletId, + networkId = cryptoCurrencyStatus.currency.network.id, + derivationPath = cryptoCurrencyStatus.currency.network.derivationPath, + ) + + val quotes = + quotesRepository.getMultiQuoteSyncOrNull(setOfNotNull(nativeCryptoCurrency.id.rawCurrencyId)) + ?: error("Quotes for native coin are unavailable") + + val quotesStatus = quotes.firstOrNull() ?: error("Empty quotes list for native coin") + + val nativeFiatRate = (quotesStatus.value as? QuoteStatus.Data)?.fiatRate + ?: error("Native fiat rate is missing") + require(nativeFiatRate > BigDecimal.ZERO) { "Native fiat rate must be > 0" } + + val marketToken = yieldSupplyRepository.getTokenStatus(token) + val maxFeeNative = marketToken.maxFeeNative.toBigDecimal() + + val rateRatio = nativeFiatRate.divide( + fiatRate, + cryptoCurrencyStatus.currency.decimals, + RoundingMode.HALF_UP, + ) + + val tokenValue = rateRatio.multiply(maxFeeNative) + + tokenValue.stripTrailingZeros() + } +} \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetProtocolBalanceUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetProtocolBalanceUseCase.kt index a36158a7b6..3bc5de3b71 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetProtocolBalanceUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetProtocolBalanceUseCase.kt @@ -19,7 +19,7 @@ class YieldSupplyGetProtocolBalanceUseCase( ): Either = Either.catch { requireNotNull(cryptoCurrency as CryptoCurrency.Token) - yieldSupplyTransactionRepository.getProtocolBalance( + yieldSupplyTransactionRepository.getEffectiveProtocolBalance( userWalletId = userWalletId, cryptoCurrency = cryptoCurrency, ) diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetTokenStatusUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetTokenStatusUseCase.kt index 36737e8d43..e86b0308e0 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetTokenStatusUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetTokenStatusUseCase.kt @@ -13,6 +13,6 @@ class YieldSupplyGetTokenStatusUseCase( suspend operator fun invoke(token: CryptoCurrency.Token): Either = Either.catch { val tokens = yieldSupplyRepository.getCachedMarkets().orEmpty() val cachedStatus = tokens.firstOrNull { it.yieldSupplyKey == token.yieldSupplyKey() } - cachedStatus ?: error("YieldMarketToken not found") + cachedStatus ?: yieldSupplyRepository.getTokenStatus(token) } } \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyMinAmountUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyMinAmountUseCase.kt index b544b7fe62..9e6f8b109d 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyMinAmountUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyMinAmountUseCase.kt @@ -2,16 +2,15 @@ package com.tangem.domain.yield.supply.usecase import arrow.core.Either import arrow.core.Either.Companion.catch -import com.tangem.blockchain.common.transaction.Fee -import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.yield.supply.fixFee import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.transaction.FeeRepository +import com.tangem.domain.yield.supply.YieldSupplyConst.YIELD_SUPPLY_EVM_CONSTANT_GAS_LIMIT import java.math.BigDecimal -import java.math.BigInteger import java.math.RoundingMode class YieldSupplyMinAmountUseCase( @@ -45,7 +44,10 @@ class YieldSupplyMinAmountUseCase( ?: error("Native fiat rate is missing") require(nativeFiatRate > BigDecimal.ZERO) { "Native fiat rate must be > 0" } - val nativeGas = feeWithoutGas.fixFee(nativeCryptoCurrency, ETHEREUM_CONSTANT_GAS_LIMIT) + val nativeGas = feeWithoutGas.fixFee( + nativeCryptoCurrency, + YIELD_SUPPLY_EVM_CONSTANT_GAS_LIMIT, + ) val rateRatio = nativeFiatRate.divide( fiatRate, @@ -62,27 +64,8 @@ class YieldSupplyMinAmountUseCase( .stripTrailingZeros() } - private fun Fee.fixFee(cryptoCurrency: CryptoCurrency, gasLimit: BigInteger) = when (this) { - is Fee.Ethereum.Legacy -> copy( - gasLimit = gasLimit, - amount = amount.copy( - value = gasPrice.multiply(gasLimit) - .toBigDecimal().movePointLeft(cryptoCurrency.decimals), - ), - ) - is Fee.Ethereum.EIP1559 -> copy( - gasLimit = gasLimit, - amount = amount.copy( - value = maxFeePerGas.multiply(gasLimit) - .toBigDecimal().movePointLeft(cryptoCurrency.decimals), - ), - ) - else -> this - } - private companion object { val FEE_BUFFER_MULTIPLIER: BigDecimal = BigDecimal("1.25") val MAX_FEE_PERCENT: BigDecimal = BigDecimal("0.04") - val ETHEREUM_CONSTANT_GAS_LIMIT = 350_000.toBigInteger() } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt index 0256ce0fd1..ec31aac834 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt @@ -41,6 +41,7 @@ internal class MarketsTokenItemConverter( stakingRate = value.stakingRate?.format { percent() }?.let { resourceReference(R.string.markets_apy_placeholder, wrappedList(it)) }, + updateTimestamp = value.updateTimestamp, ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt index bc0f3aea6d..b74e9a3d29 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt @@ -23,11 +23,11 @@ import com.tangem.core.ui.test.MarketsTestTags import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.markets.impl.R import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM +import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM.Companion.TOKEN_LAZY_LIST_ID_SEPARATOR import kotlinx.coroutines.launch private const val LOAD_NEXT_PAGE_ON_END_INDEX = 50 private const val LOAD_NEXT_PAGE_ON_END_INDEX_SEARCH = 25 -private const val TOKEN_LAZY_LIST_ID_SEPARATOR = "***" @Composable @Suppress("LongMethod") @@ -92,7 +92,7 @@ internal fun MarketsListLazyColumn( is ListUM.Content -> { items( items = state.items, - key = { it.id.value + TOKEN_LAZY_LIST_ID_SEPARATOR + it.marketCap.toString() }, + key = { it.getComposeKey() }, ) { item -> MarketsListItem( model = item, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt index 3371a2f40d..16d1279d95 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt @@ -27,6 +27,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet ), isUnder100kMarketCap = false, stakingRate = stringReference("APY 12.34%"), + updateTimestamp = 0, ), MarketsListItemUM( id = CryptoCurrency.RawID("1"), @@ -41,6 +42,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet chartData = null, isUnder100kMarketCap = false, stakingRate = stringReference("APY 12.34%"), + updateTimestamp = 0, ), MarketsListItemUM( id = CryptoCurrency.RawID("1"), @@ -57,6 +59,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet ), isUnder100kMarketCap = false, stakingRate = stringReference("APY 12.34%"), + updateTimestamp = 0, ), MarketsListItemUM( id = CryptoCurrency.RawID("1"), @@ -73,6 +76,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet ), isUnder100kMarketCap = false, stakingRate = stringReference("APY 12.34%"), + updateTimestamp = 0, ), MarketsListItemUM( id = CryptoCurrency.RawID("1"), @@ -89,6 +93,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet ), isUnder100kMarketCap = false, stakingRate = stringReference("APY 12.34%"), + updateTimestamp = 0, ), MarketsListItemUM( id = CryptoCurrency.RawID("1"), @@ -105,6 +110,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet ), isUnder100kMarketCap = false, stakingRate = stringReference("APY 12.34%"), + updateTimestamp = 0, ), ), ) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListItemUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListItemUM.kt index 5f29995bff..a83ea2e7d6 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListItemUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListItemUM.kt @@ -21,6 +21,7 @@ data class MarketsListItemUM( val chartData: MarketChartRawData?, val isUnder100kMarketCap: Boolean, val stakingRate: TextReference?, + val updateTimestamp: Long?, ) { val chartType: MarketChartLook.Type = when (trendType) { PriceChangeType.UP -> MarketChartLook.Type.Growing @@ -33,4 +34,12 @@ data class MarketsListItemUM( val text: String, val changeType: PriceChangeType? = null, ) + + fun getComposeKey(): String { + return id.value + TOKEN_LAZY_LIST_ID_SEPARATOR + marketCap.toString() + updateTimestamp + } + + companion object { + const val TOKEN_LAZY_LIST_ID_SEPARATOR = "@" + } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt index a61e5837a8..c8f65cda5a 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt @@ -402,7 +402,10 @@ internal class OnrampTokenListModel @Inject constructor( cryptoCurrency = status.currency, ).isAvailable() && !status.currency.isCustom - isAvailable && status.value !is CryptoCurrencyStatus.NoQuote + val supplyStatus = status.value.yieldSupplyStatus + val isUnavailableByYieldSupply = supplyStatus?.isAllowedToSpend == false && supplyStatus.isActive + + isAvailable && status.value !is CryptoCurrencyStatus.NoQuote && !isUnavailableByYieldSupply } } } diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt index edc28adbb1..8e955454ce 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt @@ -54,12 +54,12 @@ internal class PushNotificationsModel @Inject constructor( modelScope.launch { neverRequestPermissionUseCase(PUSH_PERMISSION) neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) - params.modelCallbacks.onDenySystemPermission() if (params.isBottomSheet) { notificationsRepository.setUserAllowToSubscribeOnPushNotifications(false) } else { params.nextRoute?.let { appRouter.push(it) } } + params.modelCallbacks.onDenySystemPermission() } } diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entry/SendEntryRoute.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entry/SendEntryRoute.kt new file mode 100644 index 0000000000..2fab40cdc0 --- /dev/null +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entry/SendEntryRoute.kt @@ -0,0 +1,18 @@ +package com.tangem.features.send.v2.api.entry + +import com.tangem.core.decompose.navigation.Route + +/** + * Route for switching send and send via swap flows. + */ +sealed class SendEntryRoute : Route { + + /** Route to send screen */ + data object Send : SendEntryRoute() + /** Route to send via swap screen */ + data object SendWithSwap : SendEntryRoute() + /** Route to choose token screen for send via swap */ + data class ChooseToken( + val showSendViaSwapNotification: Boolean, + ) : SendEntryRoute() +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/DefaultSendEntryPointComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/DefaultSendEntryPointComponent.kt index fb1684acf2..b9f865983a 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/DefaultSendEntryPointComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/DefaultSendEntryPointComponent.kt @@ -15,6 +15,8 @@ import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.childStack import com.arkivanov.decompose.router.stack.pop +import com.arkivanov.decompose.value.ObserveLifecycleMode +import com.arkivanov.decompose.value.subscribe import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.context.childByContext @@ -25,11 +27,14 @@ import com.tangem.features.managetokens.component.ChooseManagedTokensComponent import com.tangem.features.send.v2.api.SendComponent import com.tangem.features.send.v2.api.SendEntryPointComponent import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.entry.SendEntryRoute import com.tangem.features.send.v2.entrypoint.model.SendEntryPointModel import com.tangem.features.swap.v2.api.SendWithSwapComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch internal class DefaultSendEntryPointComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @@ -54,6 +59,7 @@ internal class DefaultSendEntryPointComponent @AssistedInject constructor( userWalletId = params.userWalletId, currency = params.cryptoCurrency, callback = model, + currentRoute = model.currentRoute.asStateFlow(), ), ) @@ -83,6 +89,17 @@ internal class DefaultSendEntryPointComponent @AssistedInject constructor( }, ) + init { + childStack.subscribe( + lifecycle = lifecycle, + mode = ObserveLifecycleMode.CREATE_DESTROY, + ) { stack -> + componentScope.launch { + model.currentRoute.emit(stack.active.configuration) + } + } + } + @Composable override fun Content(modifier: Modifier) { val childStackValue by childStack.subscribeAsState() diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/SendEntryRoute.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/SendEntryRoute.kt deleted file mode 100644 index 470cfb6215..0000000000 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/SendEntryRoute.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.features.send.v2.entrypoint - -import com.tangem.core.decompose.navigation.Route - -internal sealed class SendEntryRoute : Route { - data object Send : SendEntryRoute() - data object SendWithSwap : SendEntryRoute() - data class ChooseToken( - val showSendViaSwapNotification: Boolean, - ) : SendEntryRoute() -} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt index 24f122f9bd..02ba094907 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt @@ -1,19 +1,22 @@ package com.tangem.features.send.v2.entrypoint.model import com.tangem.common.ui.notifications.NotificationId +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router import com.tangem.domain.notifications.ShouldShowNotificationUseCase import com.tangem.features.managetokens.component.ChooseManagedTokensComponent import com.tangem.features.send.v2.api.SendComponent -import com.tangem.features.send.v2.entrypoint.SendEntryRoute +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.entry.SendEntryRoute import com.tangem.features.send.v2.subcomponents.amount.SendAmountUpdateTrigger import com.tangem.features.swap.v2.api.SendWithSwapComponent import com.tangem.features.swap.v2.api.subcomponents.SwapAmountUpdateTrigger import com.tangem.utils.coroutines.CoroutineDispatcherProvider import jakarta.inject.Inject import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch @Suppress("LongParameterList") @@ -24,6 +27,7 @@ internal class SendEntryPointModel @Inject constructor( private val sendAmountUpdateTrigger: SendAmountUpdateTrigger, private val swapAmountUpdateTrigger: SwapAmountUpdateTrigger, private val shouldShowNotificationUseCase: ShouldShowNotificationUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model(), SendComponent.ModelCallback, SendWithSwapComponent.ModelCallback, @@ -32,6 +36,8 @@ internal class SendEntryPointModel @Inject constructor( private var lastSavedAmount = "" private var isEnterInFiat = false + val currentRoute = MutableStateFlow(SendEntryRoute.Send) + override fun onConvertToAnotherToken(lastAmount: String, isEnterInFiatSelected: Boolean) { lastSavedAmount = lastAmount isEnterInFiat = isEnterInFiatSelected @@ -53,6 +59,12 @@ internal class SendEntryPointModel @Inject constructor( modelScope.launch { sendAmountUpdateTrigger.triggerUpdateAmount(lastAmount, isEnterInFiat) router.replaceAll(SendEntryRoute.Send) + analyticsEventHandler.send( + CommonSendAnalyticEvents.AmountScreenOpened( + categoryName = CommonSendAnalyticEvents.SEND_CATEGORY, + source = CommonSendAnalyticEvents.CommonSendSource.Send, + ), + ) } } @@ -64,6 +76,12 @@ internal class SendEntryPointModel @Inject constructor( router.pop() delay(10L) router.replaceAll(SendEntryRoute.SendWithSwap) + analyticsEventHandler.send( + CommonSendAnalyticEvents.AmountScreenOpened( + categoryName = CommonSendAnalyticEvents.SEND_CATEGORY, + source = CommonSendAnalyticEvents.CommonSendSource.SendWithSwap, + ), + ) } } diff --git a/features/swap-v2/api/build.gradle.kts b/features/swap-v2/api/build.gradle.kts index fab13fd59c..6973365628 100644 --- a/features/swap-v2/api/build.gradle.kts +++ b/features/swap-v2/api/build.gradle.kts @@ -13,6 +13,8 @@ dependencies { implementation(projects.core.decompose) implementation(projects.core.ui) + api(projects.features.sendV2.api) + /** Common */ implementation(projects.common.ui) diff --git a/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/SendWithSwapComponent.kt b/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/SendWithSwapComponent.kt index 365495e6e5..ce7a97b04f 100644 --- a/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/SendWithSwapComponent.kt +++ b/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/SendWithSwapComponent.kt @@ -4,6 +4,8 @@ import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.send.v2.api.entry.SendEntryRoute +import kotlinx.coroutines.flow.StateFlow interface SendWithSwapComponent : ComposableContentComponent { @@ -11,6 +13,7 @@ interface SendWithSwapComponent : ComposableContentComponent { val userWalletId: UserWalletId, val currency: CryptoCurrency, val callback: ModelCallback? = null, + val currentRoute: StateFlow, ) interface Factory : ComponentFactory diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt index c26536b21c..2006967f4f 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt @@ -23,6 +23,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.swap.models.R import com.tangem.domain.swap.models.SwapDirection import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.entry.SendEntryRoute import com.tangem.features.send.v2.api.subcomponents.destination.DestinationRoute import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponent import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams @@ -85,12 +86,17 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor( componentScope.launch { when (val activeComponent = stack.active.instance) { is SwapAmountComponent -> { - analyticsEventHandler.send( - CommonSendAnalyticEvents.AmountScreenOpened( - categoryName = model.analyticCategoryName, - source = model.analyticsSendSource, - ), - ) + if ( + params.currentRoute.value is SendEntryRoute.SendWithSwap && + model.currentRoute.value != stack.active.configuration + ) { + analyticsEventHandler.send( + CommonSendAnalyticEvents.AmountScreenOpened( + categoryName = model.analyticCategoryName, + source = model.analyticsSendSource, + ), + ) + } activeComponent.updateState(model.uiState.value.amountUM) } is SendDestinationComponent -> { diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ExpressDataError.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ExpressDataError.kt index 568214f788..1f13d0910e 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ExpressDataError.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ExpressDataError.kt @@ -64,4 +64,9 @@ sealed class ExpressDataError { override val code: Int = -2 override val message: String = "tooLargeSolanaTransaction" } + + data object DexActiveSupplyError : ExpressDataError() { + override val code: Int = -3 + override val message: String = "dexActiveSupplyError" + } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 7e47fff730..bbebf6f219 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -334,6 +334,14 @@ internal class SwapInteractorImpl @AssistedInject constructor( isBalanceWithoutFeeEnough: Boolean, expressOperationType: ExpressOperationType, ): Pair { + if (fromToken.value.yieldSupplyStatus?.isActive == true) { + return provider to produceDexSwapDataError( + error = ExpressDataError.DexActiveSupplyError, + fromToken = fromToken, + amount = amount, + ) + } + val maybeQuotes = repository.findBestQuote( userWallet = userWallet, fromContractAddress = fromToken.currency.getContractAddress(), diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index cf53aeb958..a8bb23deaf 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -1267,16 +1267,7 @@ internal class SwapModel @Inject constructor( toToken.currency.id.value } - return groupToFind.available.find { idToFind == it.currencyStatus.currency.id.value }?.providers - ?.filter { provider -> - // !!!WARNING!!! Filter out dex provider if yield supply is active - val yieldSupplyStatus = fromToken.value.yieldSupplyStatus - if (yieldSupplyStatus != null && yieldSupplyStatus.isActive) { - provider.type == ExchangeProviderType.CEX - } else { - true - } - }.orEmpty() + return groupToFind.available.find { idToFind == it.currencyStatus.currency.id.value }?.providers.orEmpty() } private fun Map.getLastLoadedSuccessStates(): SuccessLoadedSwapData { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt index c02b4801b9..ad3fcd0c4e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt @@ -7,6 +7,7 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification +import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics internal class TokenDetailsNotificationsAnalyticsSender( private val cryptoCurrency: CryptoCurrency, @@ -44,6 +45,10 @@ internal class TokenDetailsNotificationsAnalyticsSender( is TokenDetailsNotification.KaspaIncompleteTransactionWarning -> TokenDetailsAnalyticsEvent.Notice.Reveal( currency = cryptoCurrency, ) + is TokenDetailsNotification.YieldSupplyNotTransferedToAave -> YieldSupplyAnalytics.NoticeAmountNotDeposited( + token = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + ) is TokenDetailsNotification.NetworksUnreachable, is TokenDetailsNotification.ExistentialDeposit, is TokenDetailsNotification.NetworksNoAccount, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt index 35937498ee..b1ae6761a1 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -7,6 +7,8 @@ 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.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning @@ -136,7 +138,12 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) { title = TextReference.Res(R.string.warning_rent_fee_title), subtitle = TextReference.Res( id = R.string.warning_solana_rent_fee_message, - formatArgs = wrappedList(rentInfo.rent, rentInfo.exemptionAmount), + formatArgs = wrappedList( + rentInfo.rent, + rentInfo.exemptionAmount.format { + crypto(rentInfo.cryptoCurrency) + }, + ), ), onCloseClick = onCloseClick, ) @@ -257,4 +264,14 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) { iconResId = R.drawable.ic_error_sync_24, ), ) + + data class YieldSupplyNotTransferedToAave(val tokenName: String, val amount: String) : Warning( + title = resourceReference( + id = R.string.yield_module_amount_not_transfered_to_aave_title, + wrappedList(amount, tokenName), + ), + subtitle = resourceReference( + id = R.string.yield_module_high_fee_error, + ), + ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt index a192ef46d0..c5081a966c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt @@ -24,6 +24,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import timber.log.Timber import java.math.BigDecimal +import kotlin.String internal class TokenDetailsNotificationConverter( private val userWalletId: UserWalletId, @@ -155,6 +156,10 @@ internal class TokenDetailsNotificationConverter( ) is CryptoCurrencyWarning.MigrationMaticToPol -> MigrationMaticToPol is CryptoCurrencyWarning.UsedOutdatedDataWarning -> UsedOutdatedData + is CryptoCurrencyWarning.YieldSupplyNotDepositedAmount -> YieldSupplyNotTransferedToAave( + tokenName = warning.currencySymbol, + amount = warning.amount.format { crypto(symbol = "", decimals = warning.currency.decimals) }, + ) } } diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index 7568906434..419a988ef4 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -399,7 +399,7 @@ internal class WalletSettingsModel @Inject constructor( ), firstActionBuilder = { EventMessageAction( - title = resourceReference(R.string.common_delete), + title = resourceReference(R.string.common_forget), isWarning = true, onClick = ::forgetWallet, ) 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 292e3b3ad9..8099f6dd13 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 @@ -245,14 +245,14 @@ internal class WalletModel @Inject constructor( "isBiometricsEnabled $isBiometricsEnabled," + "isHuaweiDevice $isHuaweiDevice", ) - if (!isBiometricsEnabled) return@launch - if (!shouldShow) { - return@launch - } if (!shouldAskNotificationPermissionsViaBs) { notificationsRepository.setShouldAskNotificationPermissionsViaBs(true) return@launch } + if (!isBiometricsEnabled) return@launch + if (!shouldShow) { + return@launch + } delay(timeMillis = 1_800) 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 0aee41a87b..5eb6975247 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 @@ -8,6 +8,7 @@ import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.staking.usecase.StakingApyFlowUseCase import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase @@ -44,6 +45,7 @@ internal class MultiWalletContentLoader( private val currenciesRepository: CurrenciesRepository, private val accountDependencies: AccountDependencies, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, + private val stakingApyFlowUseCase: StakingApyFlowUseCase, ) : WalletContentLoader(id = userWallet.walletId) { override fun create(): List { @@ -60,6 +62,7 @@ internal class MultiWalletContentLoader( runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, accountDependencies = accountDependencies, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, + stakingApyFlowUseCase = stakingApyFlowUseCase, ).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 2def028fe4..63d8447f57 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 @@ -8,6 +8,7 @@ import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.staking.usecase.StakingApyFlowUseCase import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase @@ -42,6 +43,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( private val currenciesRepository: CurrenciesRepository, private val accountDependencies: AccountDependencies, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, + private val stakingApyFlowUseCase: StakingApyFlowUseCase, ) { fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): WalletContentLoader { @@ -65,6 +67,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( currenciesRepository = currenciesRepository, accountDependencies = accountDependencies, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, + stakingApyFlowUseCase = stakingApyFlowUseCase, ) } } \ 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 4bbae24b19..7b01ba0107 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 @@ -4,6 +4,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.staking.usecase.StakingApyFlowUseCase 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 @@ -34,6 +35,7 @@ internal class SingleWalletWithTokenContentLoader( private val getStoryContentUseCase: GetStoryContentUseCase, private val accountDependencies: AccountDependencies, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, + private val stakingApyFlowUseCase: StakingApyFlowUseCase, ) : WalletContentLoader(id = userWallet.walletId) { override fun create(): List { @@ -49,6 +51,7 @@ internal class SingleWalletWithTokenContentLoader( runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, accountDependencies = accountDependencies, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, + stakingApyFlowUseCase = stakingApyFlowUseCase, ).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 743e1bf3c8..4c178e1136 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 @@ -5,6 +5,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.staking.usecase.StakingApyFlowUseCase 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 @@ -35,6 +36,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( private val getStoryContentUseCase: GetStoryContentUseCase, private val accountDependencies: AccountDependencies, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, + private val stakingApyFlowUseCase: StakingApyFlowUseCase, ) { fun create(userWallet: UserWallet.Cold, clickIntents: WalletClickIntents): SingleWalletWithTokenContentLoader { @@ -54,6 +56,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase, accountDependencies = accountDependencies, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, + stakingApyFlowUseCase = stakingApyFlowUseCase, ) } } \ 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 9b15ca02a0..5255206967 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 @@ -10,6 +10,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.converte import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TokenListStateConverter import com.tangem.feature.wallet.presentation.wallet.state.utils.enableButtons import timber.log.Timber +import java.math.BigDecimal internal class SetTokenListTransformer( private val params: TokenConverterParams, @@ -17,6 +18,7 @@ internal class SetTokenListTransformer( private val appCurrency: AppCurrency, private val clickIntents: WalletClickIntents, private val yieldSupplyApyMap: Map = emptyMap(), + private val stakingApyMap: Map = emptyMap(), ) : WalletStateTransformer(userWallet.walletId) { override fun transform(prevState: WalletState): WalletState { @@ -59,7 +61,8 @@ internal class SetTokenListTransformer( selectedWallet = userWallet, appCurrency = appCurrency, clickIntents = clickIntents, - apyMap = yieldSupplyApyMap, + yieldModuleApyMap = yieldSupplyApyMap, + stakingApyMap = stakingApyMap, ).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 eee1c4776a..9134b2295d 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 @@ -27,6 +27,7 @@ import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.mutate import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList +import java.math.BigDecimal import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.OrganizeTokensButtonConfig as WalletOrganizeTokensButtonConfig internal class TokenListStateConverter( @@ -34,7 +35,8 @@ internal class TokenListStateConverter( private val params: TokenConverterParams, private val selectedWallet: UserWallet, private val clickIntents: WalletClickIntents, - private val apyMap: Map, + private val yieldModuleApyMap: Map, + private val stakingApyMap: Map, ) : Converter { private val onTokenClick: (accountId: AccountId?, currencyStatus: CryptoCurrencyStatus) -> Unit = @@ -49,7 +51,8 @@ internal class TokenListStateConverter( private fun tokenStatusConverter(accountId: AccountId? = null) = TokenItemStateConverter( appCurrency = appCurrency, - apyMap = apyMap, + yieldModuleApyMap = yieldModuleApyMap, + stakingApyMap = stakingApyMap, onItemClick = { _, status -> onTokenClick(accountId, status) }, onItemLongClick = { _, status -> onTokenLongClick(accountId, status) }, ) 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 80068f621c..328d3a5cbe 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 @@ -16,6 +16,7 @@ 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.domain.staking.usecase.StakingApyFlowUseCase 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 @@ -25,12 +26,14 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetToken import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTokenListTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.combine6 import com.tangem.utils.coroutines.saveIn import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import timber.log.Timber +import java.math.BigDecimal @Suppress("LongParameterList") internal abstract class BasicTokenListSubscriber : WalletSubscriber() { @@ -43,6 +46,7 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() { protected abstract val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase protected abstract val accountDependencies: AccountDependencies protected abstract val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase + protected abstract val stakingApyFlowUseCase: StakingApyFlowUseCase private val sendAnalyticsJobHolder = JobHolder() private val onTokenListReceivedJobHolder = JobHolder() @@ -81,12 +85,14 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() { }, flow2 = appCurrencyFlow(), flow3 = yieldSupplyApyFlow(), - transform = { maybeTokenList, appCurrency, yieldSupplyApyMap -> + flow4 = stakingApyFlow(), + transform = { maybeTokenList, appCurrency, yieldSupplyApyMap, stakingApyMap -> singleAccountTransform( maybeTokenList = maybeTokenList, appCurrency = appCurrency, portfolioId = PortfolioId(userWallet.walletId), yieldSupplyApyMap = yieldSupplyApyMap, + stakingApyMap = stakingApyMap, ) }, ) @@ -97,6 +103,7 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() { appCurrency: AppCurrency, portfolioId: PortfolioId, yieldSupplyApyMap: Map, + stakingApyMap: Map, ) { val tokenList = maybeTokenList.getOrElse( ifLoading = { maybeContent -> @@ -124,6 +131,7 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() { params = TokenConverterParams.Wallet(portfolioId, tokenList), appCurrency = appCurrency, yieldSupplyApyMap = yieldSupplyApyMap, + stakingApyMap = stakingApyMap, ) walletWithFundsChecker.check(tokenList) @@ -143,8 +151,8 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() { } } - private fun createAccountListFlow(coroutineScope: CoroutineScope): Flow<*> = combine( - flow = accountListFlow(coroutineScope) + private fun createAccountListFlow(coroutineScope: CoroutineScope): Flow<*> = combine6( + flow1 = accountListFlow(coroutineScope) .onEach { accountStatusList -> coroutineScope.launch { sendTokenListAnalytics( @@ -165,7 +173,8 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() { flow3 = accountDependencies.expandedAccountsHolder.expandedAccounts(userWallet), flow4 = accountDependencies.isAccountsModeEnabledUseCase(), flow5 = yieldSupplyApyFlow(), - transform = { accountList, appCurrency, expandedAccounts, isAccountMode, yieldSupplyApyMap -> + flow6 = stakingApyFlow(), + transform = { accountList, appCurrency, expandedAccounts, isAccountMode, yieldSupplyApyMap, stakingApyMap -> val accountFlattenTokensList = accountList.flattenTokens() val accountFlattenCurrencies = accountFlattenTokensList .map { it.flattenCurrencies() } @@ -180,6 +189,7 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() { appCurrency, PortfolioId(mainAccount.account.accountId), yieldSupplyApyMap, + stakingApyMap, ) when { @@ -198,7 +208,7 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() { ) false -> { val convertParams = TokenConverterParams.Account(accountList, expandedAccounts) - updateContent(convertParams, appCurrency, yieldSupplyApyMap) + updateContent(convertParams, appCurrency, yieldSupplyApyMap, stakingApyMap) accountFlattenTokensList .map { tokenList -> coroutineScope.launch { walletWithFundsChecker.check(tokenList) } } .joinAll() @@ -232,6 +242,7 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() { params: TokenConverterParams, appCurrency: AppCurrency, yieldSupplyApyMap: Map, + stakingApyMap: Map, ) { stateHolder.update( SetTokenListTransformer( @@ -240,6 +251,7 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() { appCurrency = appCurrency, clickIntents = clickIntents, yieldSupplyApyMap = yieldSupplyApyMap, + stakingApyMap = stakingApyMap, ), ) } @@ -255,4 +267,7 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() { private fun yieldSupplyApyFlow(): Flow> = yieldSupplyApyFlowUseCase() .distinctUntilChanged() + + private fun stakingApyFlow(): Flow> = stakingApyFlowUseCase() + .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 f56709b1d7..f64dafc766 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 @@ -10,6 +10,7 @@ import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.staking.usecase.StakingApyFlowUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.tokens.error.TokenListError @@ -36,6 +37,7 @@ internal class MultiWalletTokenListSubscriber( override val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, override val accountDependencies: AccountDependencies, override val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, + override val stakingApyFlowUseCase: StakingApyFlowUseCase, ) : 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 6866fdbf8f..15892dd13b 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 @@ -7,6 +7,7 @@ import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.staking.usecase.StakingApyFlowUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase @@ -31,6 +32,7 @@ internal class SingleWalletWithTokenListSubscriber( override val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, override val accountDependencies: AccountDependencies, override val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, + override val stakingApyFlowUseCase: StakingApyFlowUseCase, ) : BasicTokenListSubscriber() { override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt index 5dab9f0577..7cc617b264 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt @@ -22,7 +22,11 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.walletconnect.WcAnalyticEvents -import com.tangem.domain.walletconnect.model.* +import com.tangem.domain.walletconnect.model.WcPairError +import com.tangem.domain.walletconnect.model.WcPairError.Unknown +import com.tangem.domain.walletconnect.model.WcPairRequest +import com.tangem.domain.walletconnect.model.WcSessionApprove +import com.tangem.domain.walletconnect.model.WcSessionProposal import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData import com.tangem.domain.walletconnect.usecase.pair.WcPairState import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase @@ -114,35 +118,40 @@ internal class WcPairModel @Inject constructor( val availableWallets = pairState.dAppSession.proposalNetwork.keys .filter { !it.isLocked && it.isMultiCurrency } sessionProposal = pairState.dAppSession - proposalNetwork = sessionProposal.proposalNetwork.getValue(selectedUserWalletFlow.value) - additionallyEnabledNetworks = proposalNetwork.available - appInfoUiState.transformerUpdate( - WcAppInfoTransformer( - dAppSession = sessionProposal, - dAppVerifiedStateConverter = dAppVerifiedStateConverter, - onDismiss = ::rejectPairing, - onConnect = ::onConnect, - onWalletClick = { - stackNavigation.pushNew( - WcAppInfoRoutes.SelectWallet(selectedUserWalletFlow.value.walletId), - ) - }.takeIf { availableWallets.size >= WC_WALLETS_SELECTOR_MIN_COUNT }, - onNetworksClick = { - stackNavigation.pushNew( - WcAppInfoRoutes.SelectNetworks( - missingRequiredNetworks = proposalNetwork.missingRequired, - requiredNetworks = proposalNetwork.required, - availableNetworks = proposalNetwork.available, - enabledAvailableNetworks = additionallyEnabledNetworks, - notAddedNetworks = proposalNetwork.notAdded, - ), - ) - }, - userWallet = selectedUserWalletFlow.value, - proposalNetwork = proposalNetwork, - additionallyEnabledNetworks = additionallyEnabledNetworks, - ), - ) + val foundNetwork = sessionProposal.proposalNetwork[selectedUserWalletFlow.value] + if (foundNetwork == null) { + processError(Unknown("Selected wallet not found")) + } else { + proposalNetwork = foundNetwork + additionallyEnabledNetworks = proposalNetwork.available + appInfoUiState.transformerUpdate( + WcAppInfoTransformer( + dAppSession = sessionProposal, + dAppVerifiedStateConverter = dAppVerifiedStateConverter, + onDismiss = ::rejectPairing, + onConnect = ::onConnect, + onWalletClick = { + stackNavigation.pushNew( + WcAppInfoRoutes.SelectWallet(selectedUserWalletFlow.value.walletId), + ) + }.takeIf { availableWallets.size >= WC_WALLETS_SELECTOR_MIN_COUNT }, + onNetworksClick = { + stackNavigation.pushNew( + WcAppInfoRoutes.SelectNetworks( + missingRequiredNetworks = proposalNetwork.missingRequired, + requiredNetworks = proposalNetwork.required, + availableNetworks = proposalNetwork.available, + enabledAvailableNetworks = additionallyEnabledNetworks, + notAddedNetworks = proposalNetwork.notAdded, + ), + ) + }, + userWallet = selectedUserWalletFlow.value, + proposalNetwork = proposalNetwork, + additionallyEnabledNetworks = additionallyEnabledNetworks, + ), + ) + } } } } @@ -234,7 +243,7 @@ internal class WcPairModel @Inject constructor( override fun onWalletSelected(userWalletId: UserWalletId) { val selectedUserWallet = sessionProposal.proposalNetwork.keys.first { it.walletId == userWalletId } - proposalNetwork = sessionProposal.proposalNetwork.getValue(selectedUserWallet) + proposalNetwork = sessionProposal.proposalNetwork[selectedUserWallet] ?: return selectedUserWalletFlow.update { selectedUserWallet } additionallyEnabledNetworks = proposalNetwork.available appInfoUiState.transformerUpdate( diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcNavigationUtils.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcNavigationUtils.kt index dd7da71fa0..7a34577605 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcNavigationUtils.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcNavigationUtils.kt @@ -10,8 +10,8 @@ import com.tangem.features.send.v2.api.params.FeeSelectorParams.FeeSelectorDetai import com.tangem.features.walletconnect.connections.components.AlertsComponentV2 import com.tangem.features.walletconnect.connections.utils.WcAlertsFactory.createCommonTransactionAppInfoAlertUM import com.tangem.features.walletconnect.transaction.components.send.WcCustomAllowanceComponent -import com.tangem.features.walletconnect.transaction.components.send.WcSendingProcessComponent import com.tangem.features.walletconnect.transaction.components.send.WcSendMultipleTransactionsComponent +import com.tangem.features.walletconnect.transaction.components.send.WcSendingProcessComponent import com.tangem.features.walletconnect.transaction.entity.common.WcCommonTransactionModel import com.tangem.features.walletconnect.transaction.model.WcSendTransactionModel import com.tangem.features.walletconnect.transaction.routes.WcTransactionRoutes @@ -65,7 +65,7 @@ internal fun getWcCommonScreen( WcSendMultipleTransactionsComponent( appComponentContext = appComponentContext, model = model, - onConfirm = config.onConfirm, + onConfirm = { model.onMultiTransactionConfirm() }, ) } WcTransactionRoutes.TransactionProcess -> { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt index 8b201362c2..9e386e1956 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt @@ -148,7 +148,7 @@ internal class WcSendTransactionModel @Inject constructor( sign = { if (isMultipleSignRequired(useCase)) { analytics.send(SolanaLargeTransaction(useCase.rawSdkRequest.dAppMetaData.name)) - openMultipleTransaction(useCase) + openMultipleTransaction() } else { useCase.sign() } @@ -173,15 +173,13 @@ internal class WcSendTransactionModel @Inject constructor( } } - private fun openMultipleTransaction(useCase: WcSignUseCase<*>) { - stackNavigation.pushNew( - WcTransactionRoutes.MultipleTransactions( - onConfirm = { - useCase.sign() - stackNavigation.pushNew(WcTransactionRoutes.TransactionProcess) - }, - ), - ) + private fun openMultipleTransaction() { + stackNavigation.pushNew(WcTransactionRoutes.MultipleTransactions) + } + + fun onMultiTransactionConfirm() { + useCase.sign() + stackNavigation.pushNew(WcTransactionRoutes.TransactionProcess) } /** diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/routes/WcTransactionRoutes.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/routes/WcTransactionRoutes.kt index cc1d9513d7..40b2a98e49 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/routes/WcTransactionRoutes.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/routes/WcTransactionRoutes.kt @@ -44,9 +44,7 @@ internal sealed class WcTransactionRoutes : TangemBottomSheetConfigContent, Rout } @Serializable - data class MultipleTransactions( - val onConfirm: () -> Unit, - ) : WcTransactionRoutes() + data object MultipleTransactions : WcTransactionRoutes() @Serializable data object TransactionProcess : WcTransactionRoutes() diff --git a/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/YieldSupplyPromoComponent.kt b/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/YieldSupplyPromoComponent.kt index 0516efb037..0b98f700df 100644 --- a/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/YieldSupplyPromoComponent.kt +++ b/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/YieldSupplyPromoComponent.kt @@ -10,6 +10,7 @@ interface YieldSupplyPromoComponent : ComposableContentComponent { data class Params( val userWalletId: UserWalletId, val currency: CryptoCurrency, + val apy: String, ) interface Factory : ComponentFactory diff --git a/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/analytics/YieldSupplyAnalytics.kt b/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/analytics/YieldSupplyAnalytics.kt index faddabab9e..596a7bb892 100644 --- a/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/analytics/YieldSupplyAnalytics.kt +++ b/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/analytics/YieldSupplyAnalytics.kt @@ -33,6 +33,17 @@ sealed class YieldSupplyAnalytics( ), ) + data class StopEarningScreen( + val token: String, + val blockchain: String, + ) : YieldSupplyAnalytics( + event = "Stop Earning Screen", + params = mapOf( + TOKEN_PARAM to token, + BLOCKCHAIN to blockchain, + ), + ) + data class ButtonStartEarning( val token: String, val blockchain: String, @@ -55,6 +66,17 @@ sealed class YieldSupplyAnalytics( ), ) + data class ButtonGiveApprove( + val token: String, + val blockchain: String, + ) : YieldSupplyAnalytics( + event = " Button - Give Approve", + params = mapOf( + TOKEN_PARAM to token, + BLOCKCHAIN to blockchain, + ), + ) + data object ButtonFeePolicy : YieldSupplyAnalytics( event = "Button - Fee Policy", ) @@ -150,22 +172,11 @@ sealed class YieldSupplyAnalytics( event = "APY Chart", ) - data class NoticeCommissionTooHigh( + data class NoticeAmountNotDeposited( val token: String, val blockchain: String, ) : YieldSupplyAnalytics( - event = "Notice - Commission Is Too High", - params = mapOf( - TOKEN_PARAM to token, - BLOCKCHAIN to blockchain, - ), - ) - - data class NoticeNotEnoughMinAmount( - val token: String, - val blockchain: String, - ) : YieldSupplyAnalytics( - event = "Notice - Not Enough Min Amount", + event = "Notice - Amount Not Deposited", params = mapOf( TOKEN_PARAM to token, BLOCKCHAIN to blockchain, diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/YieldSupplyTrigger.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/YieldSupplyTrigger.kt new file mode 100644 index 0000000000..691095237a --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/YieldSupplyTrigger.kt @@ -0,0 +1,39 @@ +package com.tangem.features.yield.supply.impl.common + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Trigger for entering/exiting protocol from other components + */ +interface YieldSupplyProtocolTrigger { + suspend fun onEnterProtocol() + suspend fun onExitProtocol() +} + +/** + * Listener to observe entering/exiting protocol events + */ +interface YieldSupplyProtocolListener { + val enterProtocolTriggerFlow: Flow + val exitProtocolTriggerFlow: Flow +} + +@Singleton +internal class DefaultYieldSupplyProtocolTrigger @Inject constructor() : + YieldSupplyProtocolTrigger, + YieldSupplyProtocolListener { + + override val enterProtocolTriggerFlow = MutableSharedFlow() + override val exitProtocolTriggerFlow = MutableSharedFlow() + + override suspend fun onEnterProtocol() { + enterProtocolTriggerFlow.emit(Unit) + } + + override suspend fun onExitProtocol() { + exitProtocolTriggerFlow.emit(Unit) + } +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/formatter/YieldSupplyMinAmountFormatter.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/formatter/YieldSupplyAmountFormatter.kt similarity index 53% rename from features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/formatter/YieldSupplyMinAmountFormatter.kt rename to features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/formatter/YieldSupplyAmountFormatter.kt index 08c48d9cb0..296cd74afb 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/formatter/YieldSupplyMinAmountFormatter.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/formatter/YieldSupplyAmountFormatter.kt @@ -2,24 +2,38 @@ package com.tangem.features.yield.supply.impl.common.formatter import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.approximateAmount import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.utils.StringsSigns +import com.tangem.utils.StringsSigns.DOT import java.math.BigDecimal -internal class YieldSupplyMinAmountFormatter( +internal class YieldSupplyAmountFormatter( private val feeCryptoCurrency: CryptoCurrency, private val appCurrency: AppCurrency, ) { - operator fun invoke(feeValue: BigDecimal, fiatRate: BigDecimal?): TextReference { + operator fun invoke(feeValue: BigDecimal, fiatRate: BigDecimal?, showCrypto: Boolean = true): TextReference { val cryptoFee = feeValue.format { crypto(feeCryptoCurrency) } val fiatFeeValue = fiatRate?.let(feeValue::multiply) - val fiatFee = fiatFeeValue.format { fiat(appCurrency.code, appCurrency.symbol) } + val fiatFee = if (showCrypto) { + fiatFeeValue.format { + fiat(appCurrency.code, appCurrency.symbol) + .approximateAmount() + } + } else { + fiatFeeValue.format { + fiat(appCurrency.code, appCurrency.symbol) + } + } - return stringReference(cryptoFee + " ${StringsSigns.DOT} " + fiatFee) + return if (showCrypto) { + stringReference("$cryptoFee $DOT $fiatFee") + } else { + stringReference(fiatFee) + } } } \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/ui/YieldSupplyFeeRow.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/ui/YieldSupplyFeeRow.kt index 2f2901b79d..dc00a547a8 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/ui/YieldSupplyFeeRow.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/ui/YieldSupplyFeeRow.kt @@ -12,6 +12,7 @@ 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.text.style.TextAlign import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.TextShimmer @@ -46,6 +47,7 @@ internal fun YieldSupplyFeeRow(title: TextReference, value: TextReference?) { text = targetValue.resolveReference(), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.End, ) } else { TextShimmer( diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/di/YieldSupplyFeatureModule.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/di/YieldSupplyFeatureModule.kt index 45b8806bd6..9226bcfa8e 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/di/YieldSupplyFeatureModule.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/di/YieldSupplyFeatureModule.kt @@ -3,6 +3,10 @@ package com.tangem.features.yield.supply.impl.di import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.features.yield.supply.impl.DefaultYieldSupplyFeatureToggles import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles +import com.tangem.features.yield.supply.impl.common.DefaultYieldSupplyProtocolTrigger +import com.tangem.features.yield.supply.impl.common.YieldSupplyProtocolListener +import com.tangem.features.yield.supply.impl.common.YieldSupplyProtocolTrigger +import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -18,4 +22,17 @@ internal object YieldSupplyFeatureModule { fun provideYieldFeatureToggles(featureTogglesManager: FeatureTogglesManager): YieldSupplyFeatureToggles { return DefaultYieldSupplyFeatureToggles(featureTogglesManager) } +} + +@InstallIn(SingletonComponent::class) +@Module +internal interface YieldSupplyProtocolModuleBinds { + + @Singleton + @Binds + fun bindYieldSupplyProtocolTrigger(impl: DefaultYieldSupplyProtocolTrigger): YieldSupplyProtocolTrigger + + @Singleton + @Binds + fun bindYieldSupplyProtocolListener(impl: DefaultYieldSupplyProtocolTrigger): YieldSupplyProtocolListener } \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/entity/YieldSupplyUM.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/entity/YieldSupplyUM.kt index 30008334c4..112b1dbb24 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/entity/YieldSupplyUM.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/entity/YieldSupplyUM.kt @@ -9,6 +9,7 @@ internal sealed class YieldSupplyUM { data object Initial : YieldSupplyUM() data class Available( + val apy: String, val title: TextReference, val onClick: () -> Unit, ) : YieldSupplyUM() @@ -18,6 +19,7 @@ internal sealed class YieldSupplyUM { data object Unavailable : YieldSupplyUM() data class Content( + val apy: String, val title: TextReference, val subtitle: TextReference, val rewardsApy: TextReference, diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt index 0895525f5e..5d1d974638 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt @@ -8,6 +8,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference @@ -17,7 +18,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.TxInfo import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.yield.supply.YieldSupplyStatus -import com.tangem.domain.tokens.FetchCurrencyStatusUseCase +import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyActivateUseCase @@ -27,6 +28,7 @@ import com.tangem.domain.yield.supply.usecase.YieldSupplyIsAvailableUseCase import com.tangem.features.yield.supply.impl.R import com.tangem.features.yield.supply.api.YieldSupplyComponent import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics +import com.tangem.features.yield.supply.impl.common.YieldSupplyProtocolListener import com.tangem.features.yield.supply.impl.main.entity.YieldSupplyUM import com.tangem.features.yield.supply.impl.main.model.transformers.YieldSupplyTokenStatusSuccessTransformer import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -49,13 +51,14 @@ internal class YieldSupplyModel @Inject constructor( private val appRouter: AppRouter, private val getUserWalletUseCase: GetUserWalletUseCase, private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, - private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, + private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, @DelayedWork private val coroutineScope: CoroutineScope, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val yieldSupplyGetTokenStatusUseCase: YieldSupplyGetTokenStatusUseCase, private val yieldSupplyIsAvailableUseCase: YieldSupplyIsAvailableUseCase, private val yieldSupplyActivateUseCase: YieldSupplyActivateUseCase, private val yieldSupplyDeactivateUseCase: YieldSupplyDeactivateUseCase, + private val yieldSupplyProtocolListener: YieldSupplyProtocolListener, ) : Model(), YieldSupplyClickIntents { private val params = paramsContainer.require() @@ -83,6 +86,38 @@ internal class YieldSupplyModel @Inject constructor( init { checkIfYieldSupplyIsAvailable() + observeProtocolEvents() + } + + private fun observeProtocolEvents() { + yieldSupplyProtocolListener.exitProtocolTriggerFlow.onEach { + uiState.update { + YieldSupplyUM.Processing.Exit + } + coroutineScope.launch(dispatchers.io) { + delay(PROCESSING_UPDATE_DELAY) + singleNetworkStatusFetcher( + params = SingleNetworkStatusFetcher.Params( + userWalletId = userWallet.walletId, + network = cryptoCurrency.network, + ), + ) + } + }.launchIn(modelScope) + yieldSupplyProtocolListener.enterProtocolTriggerFlow.onEach { + uiState.update { + YieldSupplyUM.Processing.Enter + } + coroutineScope.launch(dispatchers.io) { + delay(PROCESSING_UPDATE_DELAY) + singleNetworkStatusFetcher( + params = SingleNetworkStatusFetcher.Params( + userWalletId = userWallet.walletId, + network = cryptoCurrency.network, + ), + ) + } + }.launchIn(modelScope) } private fun checkIfYieldSupplyIsAvailable() { @@ -106,6 +141,7 @@ internal class YieldSupplyModel @Inject constructor( currencyId = cryptoCurrency.id, isSingleWalletWithTokens = false, ).onEach { maybeCryptoCurrency -> + Timber.tag("getSingleCryptoCurrencyStatusUseCase").d("update $maybeCryptoCurrency") maybeCryptoCurrency.fold( ifRight = { cryptoCurrencyStatus -> cryptoCurrencyStatusFlow.update { cryptoCurrencyStatus } @@ -144,10 +180,17 @@ internal class YieldSupplyModel @Inject constructor( } override fun onStartEarningClick() { + val yieldSupplyUM = uiState.value + val apy = when (yieldSupplyUM) { + is YieldSupplyUM.Available -> yieldSupplyUM.apy + is YieldSupplyUM.Content -> yieldSupplyUM.apy + else -> "" + } appRouter.push( YieldSupplyPromo( userWalletId = params.userWalletId, cryptoCurrency = params.cryptoCurrency, + apy = apy, ), ) } @@ -180,7 +223,12 @@ internal class YieldSupplyModel @Inject constructor( hasActiveTransaction && yieldTransaction != null -> { coroutineScope.launch(dispatchers.io) { delay(PROCESSING_UPDATE_DELAY) - fetchCurrencyStatusUseCase(userWalletId = userWallet.walletId, cryptoCurrency.id) + singleNetworkStatusFetcher( + params = SingleNetworkStatusFetcher.Params( + userWalletId = userWallet.walletId, + network = cryptoCurrency.network, + ), + ) } uiState.update { when (yieldTransaction) { @@ -199,32 +247,10 @@ internal class YieldSupplyModel @Inject constructor( ), ) } - modelScope.launch(dispatchers.default) { - yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken) - .onRight { tokenStatus -> - uiState.update { - YieldSupplyUM.Content( - title = resourceReference( - R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title, - ), - subtitle = resourceReference( - R.string.yield_module_token_details_earn_notification_earning_on_your_balance_subtitle, - ), - rewardsApy = combinedReference( - resourceReference( - R.string.yield_module_token_details_earn_notification_apy, - ), - stringReference(" ${tokenStatus.apy}%"), - ), - onClick = ::onActiveClick, - isAllowedToSpend = yieldSupplyStatus.isAllowedToSpend, - ) - } - }.onLeft { - Timber.e(it) - uiState.update { YieldSupplyUM.Loading } - } - } + loadActiveState( + cryptoCurrencyToken = cryptoCurrencyToken, + yieldSupplyStatus = yieldSupplyStatus, + ) } else -> { @@ -233,6 +259,49 @@ internal class YieldSupplyModel @Inject constructor( } } + private fun loadActiveState(cryptoCurrencyToken: CryptoCurrency.Token, yieldSupplyStatus: YieldSupplyStatus) { + modelScope.launch(dispatchers.default) { + yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken) + .onRight { tokenStatus -> + uiState.update { + YieldSupplyUM.Content( + title = resourceReference( + R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title, + ), + subtitle = resourceReference( + R.string.yield_module_token_details_earn_notification_earning_on_your_balance_subtitle, + ), + rewardsApy = combinedReference( + resourceReference( + R.string.yield_module_token_details_earn_notification_apy, + ), + stringReference(" ${tokenStatus.apy}%"), + ), + onClick = ::onActiveClick, + isAllowedToSpend = yieldSupplyStatus.isAllowedToSpend, + apy = tokenStatus.apy.toString(), + ) + } + }.onLeft { + Timber.e(it) + uiState.update { + YieldSupplyUM.Content( + title = resourceReference( + R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title, + ), + subtitle = resourceReference( + R.string.yield_module_token_details_earn_notification_earning_on_your_balance_subtitle, + ), + rewardsApy = TextReference.EMPTY, + onClick = ::onActiveClick, + isAllowedToSpend = yieldSupplyStatus.isAllowedToSpend, + apy = "", + ) + } + } + } + } + private fun sendInfoAboutProtocolStatus(cryptoCurrencyStatus: CryptoCurrencyStatus) { if (lastYieldSupplyStatus == cryptoCurrencyStatus.value.yieldSupplyStatus) return val token = cryptoCurrency as? CryptoCurrency.Token ?: return diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/transformers/YieldSupplyTokenStatusSuccessTransformer.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/transformers/YieldSupplyTokenStatusSuccessTransformer.kt index 4fed5db894..338012ba0e 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/transformers/YieldSupplyTokenStatusSuccessTransformer.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/transformers/YieldSupplyTokenStatusSuccessTransformer.kt @@ -21,6 +21,7 @@ internal class YieldSupplyTokenStatusSuccessTransformer( formatArgs = wrappedList(tokenStatus.apy), ), onClick = onStartEarningClick, + apy = tokenStatus.apy.toString(), ) } } \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/ui/YieldSupplyBlockContent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/ui/YieldSupplyBlockContent.kt index b596d93729..49ea1ef511 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/ui/YieldSupplyBlockContent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/ui/YieldSupplyBlockContent.kt @@ -308,6 +308,7 @@ private class PreviewProvider : PreviewParameterProvider { R.string.yield_module_token_details_earn_notification_title, wrappedList("5.1"), ), + apy = "5.1", onClick = {}, ), YieldSupplyUM.Content( @@ -315,6 +316,7 @@ private class PreviewProvider : PreviewParameterProvider { subtitle = stringReference("Interest accrues automatically"), rewardsApy = stringReference("5.1 % APY"), onClick = {}, + apy = "5.1", isAllowedToSpend = false, ), YieldSupplyUM.Content( @@ -322,6 +324,7 @@ private class PreviewProvider : PreviewParameterProvider { subtitle = stringReference("Interest accrues automatically"), rewardsApy = stringReference("5.1 % APY"), onClick = {}, + apy = "5.1", isAllowedToSpend = true, ), YieldSupplyUM.Loading, diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/entity/YieldSupplyPromoUM.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/entity/YieldSupplyPromoUM.kt index 51eefdad41..fca29d33ea 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/entity/YieldSupplyPromoUM.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/entity/YieldSupplyPromoUM.kt @@ -6,4 +6,5 @@ data class YieldSupplyPromoUM( val tosLink: String, val policyLink: String, val title: TextReference, + val subtitle: TextReference, ) \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/model/YieldSupplyPromoModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/model/YieldSupplyPromoModel.kt index 4a3d0411df..09c57f0305 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/model/YieldSupplyPromoModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/model/YieldSupplyPromoModel.kt @@ -15,6 +15,7 @@ import com.tangem.features.yield.supply.impl.R import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics import com.tangem.features.yield.supply.impl.promo.YieldSupplyPromoConfig import com.tangem.features.yield.supply.impl.promo.entity.YieldSupplyPromoUM +import com.tangem.utils.TangemBlogUrlBuilder import com.tangem.utils.TangemBlogUrlBuilder.YIELD_SUPPLY_HOW_IT_WORKS_URL import com.tangem.utils.coroutines.CoroutineDispatcherProvider import javax.inject.Inject @@ -31,9 +32,13 @@ internal class YieldSupplyPromoModel @Inject constructor( val params: YieldSupplyPromoComponent.Params = paramsContainer.require() val uiState: YieldSupplyPromoUM = YieldSupplyPromoUM( - tosLink = "https://tangem.com/terms-of-service/", // TODO replace with real link - policyLink = "https://tangem.com/privacy-policy/", // TODO replace with real link - title = resourceReference(R.string.yield_module_promo_screen_title, wrappedList("5.3")), + tosLink = TangemBlogUrlBuilder.YIELD_SUPPLY_TOS_URL, + policyLink = TangemBlogUrlBuilder.YIELD_SUPPLY_PRIVACY_URL, + title = resourceReference(R.string.yield_module_promo_screen_title), + subtitle = resourceReference( + R.string.yield_module_promo_screen_variable_rate_info, + wrappedList(params.apy), + ), ) init { diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/ui/YieldSupplyPromoContent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/ui/YieldSupplyPromoContent.kt index 5839f6fb28..e036e90dd0 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/ui/YieldSupplyPromoContent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/ui/YieldSupplyPromoContent.kt @@ -73,7 +73,7 @@ internal fun YieldSupplyPromoContent( SpacerH8() Label( state = LabelUM( - text = resourceReference(R.string.yield_module_promo_screen_variable_rate_info), + text = yieldSupplyPromoUM.subtitle, style = LabelStyle.REGULAR, icon = R.drawable.ic_information_24, onIconClick = clickIntents::onApyInfoClick, @@ -223,7 +223,8 @@ private fun YieldSupplyPromoContent_Preview() { yieldSupplyPromoUM = YieldSupplyPromoUM( tosLink = "https://tangem.com/terms-of-service/", policyLink = "https://tangem.com/privacy-policy/", - title = resourceReference(R.string.yield_module_promo_screen_title, wrappedList("5.3")), + title = resourceReference(R.string.yield_module_promo_screen_title), + subtitle = resourceReference(R.string.yield_module_promo_screen_variable_rate_info, wrappedList("5.3")), ), clickIntents = object : YieldSupplyPromoClickIntents { override fun onBackClick() {} diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/entity/YieldSupplyActiveContentUM.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/entity/YieldSupplyActiveContentUM.kt index 7b1e55f50a..9b93df6c2a 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/entity/YieldSupplyActiveContentUM.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/entity/YieldSupplyActiveContentUM.kt @@ -10,6 +10,9 @@ internal data class YieldSupplyActiveContentUM( val subtitle: TextReference, val subtitleLink: TextReference, val notificationUM: NotificationUM?, - val apy: TextReference? = null, val minAmount: TextReference?, + val currentFee: TextReference?, + val feeDescription: TextReference?, + val apy: TextReference? = null, + val isHighFee: Boolean = false, ) \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/model/YieldSupplyActiveModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/model/YieldSupplyActiveModel.kt index f54b17d1f9..a74719003c 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/model/YieldSupplyActiveModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/model/YieldSupplyActiveModel.kt @@ -12,6 +12,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency @@ -19,9 +20,11 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.yield.supply.usecase.YieldSupplyGetProtocolBalanceUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyGetTokenStatusUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyMinAmountUseCase -import com.tangem.features.yield.supply.impl.R +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetCurrentFeeUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetMaxFeeUseCase import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics -import com.tangem.features.yield.supply.impl.common.formatter.YieldSupplyMinAmountFormatter +import com.tangem.features.yield.supply.impl.R +import com.tangem.features.yield.supply.impl.common.formatter.YieldSupplyAmountFormatter import com.tangem.features.yield.supply.impl.subcomponents.active.YieldSupplyActiveComponent import com.tangem.features.yield.supply.impl.subcomponents.active.entity.YieldSupplyActiveContentUM import com.tangem.utils.StringsSigns.DASH_SIGN @@ -40,6 +43,8 @@ internal class YieldSupplyActiveModel @Inject constructor( private val yieldSupplyGetProtocolBalanceUseCase: YieldSupplyGetProtocolBalanceUseCase, private val yieldSupplyGetTokenStatusUseCase: YieldSupplyGetTokenStatusUseCase, private val yieldSupplyMinAmountUseCase: YieldSupplyMinAmountUseCase, + private val yieldSupplyGetCurrentFeeUseCase: YieldSupplyGetCurrentFeeUseCase, + private val yieldSupplyGetMaxFeeUseCase: YieldSupplyGetMaxFeeUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, ) : Model() { @@ -62,6 +67,9 @@ internal class YieldSupplyActiveModel @Inject constructor( subtitleLink = resourceReference(R.string.common_read_more), notificationUM = null, minAmount = null, + currentFee = null, + feeDescription = null, + isHighFee = false, ), ) @@ -94,10 +102,11 @@ internal class YieldSupplyActiveModel @Inject constructor( private fun subscribeOnCurrencyUpdates() { cryptoCurrencyStatusFlow.onEach { cryptoCurrencyStatus -> - val protocolBalance = yieldSupplyGetProtocolBalanceUseCase( - userWalletId = params.userWallet.walletId, - cryptoCurrency = cryptoCurrency, - ).getOrNull() + val protocolBalance = cryptoCurrencyStatus.value.yieldSupplyStatus?.effectiveProtocolBalance + ?: yieldSupplyGetProtocolBalanceUseCase( + userWalletId = params.userWallet.walletId, + cryptoCurrency = cryptoCurrency, + ).getOrNull() val approvalNotification = if (cryptoCurrencyStatus.value.yieldSupplyStatus?.isAllowedToSpend != true) { NotificationUM.Error( @@ -115,6 +124,7 @@ internal class YieldSupplyActiveModel @Inject constructor( loadApy() loadMinAmount() + loadFees() uiState.update { it.copy( @@ -154,10 +164,14 @@ internal class YieldSupplyActiveModel @Inject constructor( params.userWallet, cryptoCurrencyStatusFlow.value, ).onRight { minAmount -> - val minAmountTextReference = YieldSupplyMinAmountFormatter( + val minAmountTextReference = YieldSupplyAmountFormatter( cryptoCurrencyStatusFlow.value.currency, appCurrency, - ).invoke(minAmount, cryptoCurrencyStatusFlow.value.value.fiatRate) + ).invoke( + feeValue = minAmount, + fiatRate = cryptoCurrencyStatusFlow.value.value.fiatRate, + showCrypto = false, + ) uiState.update { it.copy(minAmount = minAmountTextReference) } @@ -169,6 +183,51 @@ internal class YieldSupplyActiveModel @Inject constructor( } } + private fun loadFees() { + modelScope.launch(dispatchers.default) { + val cryptoStatus = cryptoCurrencyStatusFlow.value + + val currentFee = yieldSupplyGetCurrentFeeUseCase( + userWallet = params.userWallet, + cryptoCurrencyStatus = cryptoStatus, + ).getOrNull() + + val maxFee = yieldSupplyGetMaxFeeUseCase( + userWallet = params.userWallet, + cryptoCurrencyStatus = cryptoStatus, + ).getOrNull() + + val currentFeeText = currentFee?.let { + YieldSupplyAmountFormatter( + cryptoStatus.currency, + appCurrency, + ).invoke( + feeValue = it, + fiatRate = cryptoStatus.value.fiatRate, + showCrypto = false, + ) + } + + val isHighFee = if (currentFee != null && maxFee != null) currentFee > maxFee else false + + val maxFiatFee = maxFee?.multiply(cryptoStatus.value.fiatRate) + .format { fiat(appCurrency.code, appCurrency.symbol) } + val feeDescription = if (isHighFee) { + resourceReference(R.string.yield_module_earn_sheet_high_fee_description, wrappedList(maxFiatFee)) + } else { + resourceReference(R.string.yield_module_earn_sheet_fee_description, wrappedList(maxFiatFee)) + } + + uiState.update { + it.copy( + currentFee = currentFeeText ?: stringReference(DASH_SIGN), + isHighFee = isHighFee, + feeDescription = feeDescription, + ) + } + } + } + private companion object { const val AAVEV3_PREFIX = "a" } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/ui/YieldSupplyActiveContent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/ui/YieldSupplyActiveContent.kt index a20183fb10..9e2346fe2f 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/ui/YieldSupplyActiveContent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/ui/YieldSupplyActiveContent.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withLink import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -70,6 +71,22 @@ internal fun YieldSupplyActiveContent( } YieldSupplyActiveMyFunds(state = state, isBalanceHidden = isBalanceHidden) + + AnimatedVisibility(state.feeDescription != null) { + Text( + modifier = Modifier.padding(horizontal = 12.dp), + text = state.feeDescription?.resolveReference().orEmpty(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + + Text( + modifier = Modifier.padding(horizontal = 12.dp), + text = stringResourceSafe(R.string.yield_module_fee_policy_sheet_min_amount_note), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) } } @@ -99,7 +116,9 @@ private fun CurrentApy(apy: TextReference?, modifier: Modifier = Modifier) { painterResource(R.drawable.ic_arrow_up_8), tint = TangemTheme.colors.text.accent, contentDescription = null, - modifier = Modifier.padding(end = 6.dp).size(12.dp), + modifier = Modifier + .padding(end = 6.dp) + .size(12.dp), ) Text( modifier = modifier, @@ -113,6 +132,7 @@ private fun CurrentApy(apy: TextReference?, modifier: Modifier = Modifier) { } } +@Suppress("LongMethod") @Composable private fun YieldSupplyActiveMyFunds(state: YieldSupplyActiveContentUM, isBalanceHidden: Boolean) { Column( @@ -173,6 +193,15 @@ private fun YieldSupplyActiveMyFunds(state: YieldSupplyActiveContentUM, isBalanc info = state.minAmount, isBalanceHidden = false, ) + HorizontalDivider( + thickness = 0.5.dp, + color = TangemTheme.colors.stroke.primary, + ) + HighComissionInfoRow( + title = resourceReference(R.string.common_network_fee_title), + info = state.currentFee, + isHighComission = state.isHighFee, + ) } } @@ -220,6 +249,7 @@ private fun InfoRow(title: TextReference, isBalanceHidden: Boolean, info: TextRe text = currentInfo.orMaskWithStars(isBalanceHidden).resolveReference(), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.End, ) } else { TextShimmer( @@ -231,6 +261,57 @@ private fun InfoRow(title: TextReference, isBalanceHidden: Boolean, info: TextRe } } +@Composable +private fun HighComissionInfoRow(title: TextReference, info: TextReference?, isHighComission: Boolean) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.padding(horizontal = 4.dp, vertical = 12.dp), + ) { + Text( + text = title.resolveReference(), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + SpacerWMax() + + AnimatedContent(info) { currentInfo -> + if (currentInfo != null) { + if (isHighComission) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Icon( + painterResource(R.drawable.ic_token_info_24), + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = TangemTheme.colors.text.warning, + ) + Text( + text = currentInfo.resolveReference(), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.warning, + textAlign = TextAlign.End, + ) + } + } else { + Text( + text = currentInfo.resolveReference(), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.End, + ) + } + } else { + TextShimmer( + text = title.resolveReference(), + style = TangemTheme.typography.body1, + ) + } + } + } +} + // region Preview @Composable @Preview(showBackground = true, widthDp = 360) @@ -262,6 +343,12 @@ private class YieldSupplyActiveBottomSheetPreviewProvider : PreviewParameterProv notificationUM = NotificationUM.Error.InvalidAmount, apy = stringReference("5,14%"), minAmount = stringReference("50 USDT"), + isHighFee = true, + feeDescription = stringReference( + "The network fee is currently too high to execute lending." + + "Funds will be supplied once it drops to \$12 or below. ", + ), + currentFee = stringReference("30 USDT"), ), ) } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt index 5a7ac6bd0a..66f9d0f195 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt @@ -119,6 +119,11 @@ internal class YieldSupplyApproveModel @Inject constructor( val yieldSupplyFeeUM = uiState.value.yieldSupplyFeeUM as? YieldSupplyFeeUM.Content ?: return uiState.update(YieldSupplyTransactionInProgressTransformer) + analyticsEventHandler.send(YieldSupplyAnalytics.ButtonGiveApprove( + token = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + )) + modelScope.launch(dispatchers.default) { sendTransactionUseCase( txData = yieldSupplyFeeUM.transactionDataList.first(), diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt index f7390118e6..aaa41615e2 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt @@ -15,7 +15,6 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.transaction.error.GetFeeError @@ -29,6 +28,7 @@ import com.tangem.domain.yield.supply.usecase.YieldSupplyStartEarningUseCase import com.tangem.features.yield.supply.impl.R import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics import com.tangem.features.yield.supply.impl.common.YieldSupplyAlertFactory +import com.tangem.features.yield.supply.impl.common.YieldSupplyProtocolTrigger import com.tangem.features.yield.supply.impl.common.entity.YieldSupplyActionUM import com.tangem.features.yield.supply.impl.common.entity.YieldSupplyFeeUM import com.tangem.features.yield.supply.impl.common.entity.transformer.YieldSupplyTransactionInProgressTransformer @@ -41,7 +41,6 @@ import com.tangem.features.yield.supply.impl.subcomponents.startearning.model.tr import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.orZero import com.tangem.utils.transformer.update -import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber @@ -63,11 +62,11 @@ internal class YieldSupplyStartEarningModel @Inject constructor( private val yieldSupplyEstimateEnterFeeUseCase: YieldSupplyEstimateEnterFeeUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val yieldSupplyNotificationsUpdateTrigger: YieldSupplyNotificationsUpdateTrigger, - private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val yieldSupplyAlertFactory: YieldSupplyAlertFactory, private val yieldSupplyActivateUseCase: YieldSupplyActivateUseCase, private val yieldSupplyGetTokenStatusUseCase: YieldSupplyGetTokenStatusUseCase, private val yieldSupplyMinAmountUseCase: YieldSupplyMinAmountUseCase, + private val yieldSupplyProtocolTrigger: YieldSupplyProtocolTrigger, ) : Model(), YieldSupplyNotificationsComponent.ModelCallback { private val params: YieldSupplyStartEarningComponent.Params = paramsContainer.require() @@ -253,14 +252,9 @@ internal class YieldSupplyStartEarningModel @Inject constructor( ) }, ifRight = { - yieldSupplyActivateUseCase(cryptoCurrency) - modelScope.launch(NonCancellable) { - fetchCurrencyStatusUseCase( - userWalletId = userWallet.walletId, - cryptoCurrency.id, - ) - } + yieldSupplyProtocolTrigger.onEnterProtocol() analytics.send(YieldSupplyAnalytics.FundsEarned) + yieldSupplyActivateUseCase(cryptoCurrency) modelScope.launch { params.callback.onTransactionSent() } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/transformers/YieldSupplyStartEarningFeeContentTransformer.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/transformers/YieldSupplyStartEarningFeeContentTransformer.kt index 5e0ba9a4a9..51e5126eb7 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/transformers/YieldSupplyStartEarningFeeContentTransformer.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/transformers/YieldSupplyStartEarningFeeContentTransformer.kt @@ -10,7 +10,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.yield.supply.impl.common.entity.YieldSupplyActionUM import com.tangem.features.yield.supply.impl.common.entity.YieldSupplyFeeUM -import com.tangem.features.yield.supply.impl.common.formatter.YieldSupplyMinAmountFormatter +import com.tangem.features.yield.supply.impl.common.formatter.YieldSupplyAmountFormatter import com.tangem.utils.StringsSigns.DOT import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.toPersistentList @@ -49,7 +49,7 @@ internal class YieldSupplyStartEarningFeeContentTransformer( } val maxFiatFee = maxFiatFeeValue.format { fiat(appCurrency.code, appCurrency.symbol) } - val minAmountTextReference = YieldSupplyMinAmountFormatter( + val minAmountTextReference = YieldSupplyAmountFormatter( cryptoCurrency, appCurrency, ).invoke(minAmount, cryptoCurrencyStatus.value.fiatRate) diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt index a6f111df3a..268ab35f31 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt @@ -13,7 +13,6 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase @@ -22,6 +21,7 @@ import com.tangem.domain.yield.supply.usecase.YieldSupplyStopEarningUseCase import com.tangem.features.yield.supply.impl.R import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics import com.tangem.features.yield.supply.impl.common.YieldSupplyAlertFactory +import com.tangem.features.yield.supply.impl.common.YieldSupplyProtocolTrigger import com.tangem.features.yield.supply.impl.common.entity.YieldSupplyActionUM import com.tangem.features.yield.supply.impl.common.entity.YieldSupplyFeeUM import com.tangem.features.yield.supply.impl.common.entity.transformer.YieldSupplyTransactionInProgressTransformer @@ -35,7 +35,6 @@ import com.tangem.utils.TangemBlogUrlBuilder import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.orZero import com.tangem.utils.transformer.update -import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber @@ -56,7 +55,7 @@ internal class YieldSupplyStopEarningModel @Inject constructor( private val yieldSupplyNotificationsUpdateTrigger: YieldSupplyNotificationsUpdateTrigger, private val yieldSupplyAlertFactory: YieldSupplyAlertFactory, private val yieldSupplyDeactivateUseCase: YieldSupplyDeactivateUseCase, - private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, + private val yieldSupplyProtocolTrigger: YieldSupplyProtocolTrigger, ) : Model(), YieldSupplyNotificationsComponent.ModelCallback { private val params: YieldSupplyStopEarningComponent.Params = paramsContainer.require() @@ -97,6 +96,13 @@ internal class YieldSupplyStopEarningModel @Inject constructor( ) init { + val currency = params.cryptoCurrencyStatusFlow.value.currency + analytics.send( + YieldSupplyAnalytics.StopEarningScreen( + token = currency.symbol, + blockchain = currency.network.name, + ), + ) modelScope.launch { appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default } subscribeOnCurrencyStatusUpdates() @@ -133,6 +139,10 @@ internal class YieldSupplyStopEarningModel @Inject constructor( ifLeft = { error -> Timber.e(error.toString()) uiState.update(YieldSupplyTransactionReadyTransformer) + analytics.send(YieldSupplyAnalytics.EarnErrors( + action = YieldSupplyAnalytics.Action.Stop, + errorDescription = error.getAnalyticsDescription(), + )) yieldSupplyAlertFactory.getSendTransactionErrorState( error = error, popBack = params.callback::onBackClick, @@ -148,6 +158,7 @@ internal class YieldSupplyStopEarningModel @Inject constructor( ) }, ifRight = { + yieldSupplyProtocolTrigger.onExitProtocol() analytics.send( YieldSupplyAnalytics.FundsWithdrawn( token = cryptoCurrency.symbol, @@ -155,10 +166,9 @@ internal class YieldSupplyStopEarningModel @Inject constructor( ), ) yieldSupplyDeactivateUseCase(cryptoCurrency) - modelScope.launch(NonCancellable) { - fetchCurrencyStatusUseCase(userWalletId = userWallet.walletId, cryptoCurrency.id) + modelScope.launch { + params.callback.onTransactionSent() } - params.callback.onTransactionSent() }, ) } diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 0f2f752b9f..f7e514619e 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1281" +tangemBlockchainSdk = "develop-1287" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-564" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ diff --git a/tangem-android-tools b/tangem-android-tools index 888c626fc5..b64f8659fc 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit 888c626fc514542c3d7568b83d8afe92777b1e0c +Subproject commit b64f8659fce65e7749d50923b32bfad13e6b8cac