Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-06 22:38:15 +05:00
parent a3f7c6dcc4
commit 99dfa2a392
12 changed files with 189 additions and 36 deletions

View file

@ -4,11 +4,11 @@ import android.content.Context
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.tangemTech.models.YieldMarketsResponse
import com.tangem.datasource.local.yieldsupply.DefaultYieldMarketsStore
import com.tangem.datasource.local.yieldsupply.YieldMarketsStore
import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.listTypes
import com.tangem.domain.yield.supply.models.YieldMarketToken
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -34,7 +34,7 @@ object YieldSupplyModule {
persistenceStore = DataStoreFactory.create(
serializer = MoshiDataStoreSerializer(
moshi = moshi,
types = listTypes<YieldMarketToken>(),
types = listTypes<YieldMarketsResponse.MarketDto>(),
defaultValue = emptyList(),
),
produceFile = { context.dataStoreFile(fileName = "yield_markets_cache") },

View file

@ -1,21 +1,21 @@
package com.tangem.datasource.local.yieldsupply
import androidx.datastore.core.DataStore
import com.tangem.domain.yield.supply.models.YieldMarketToken
import com.tangem.datasource.api.tangemTech.models.YieldMarketsResponse
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
internal class DefaultYieldMarketsStore(
private val persistenceStore: DataStore<List<YieldMarketToken>>,
private val persistenceStore: DataStore<List<YieldMarketsResponse.MarketDto>>,
) : YieldMarketsStore {
override fun get(): Flow<List<YieldMarketToken>> = persistenceStore.data
override fun get(): Flow<List<YieldMarketsResponse.MarketDto>> = persistenceStore.data
override suspend fun getSyncOrNull(): List<YieldMarketToken>? {
override suspend fun getSyncOrNull(): List<YieldMarketsResponse.MarketDto>? {
return persistenceStore.data.firstOrNull()
}
override suspend fun store(items: List<YieldMarketToken>) {
override suspend fun store(items: List<YieldMarketsResponse.MarketDto>) {
persistenceStore.updateData { _ -> items }
}
}

View file

@ -1,13 +1,13 @@
package com.tangem.datasource.local.yieldsupply
import com.tangem.domain.yield.supply.models.YieldMarketToken
import com.tangem.datasource.api.tangemTech.models.YieldMarketsResponse
import kotlinx.coroutines.flow.Flow
interface YieldMarketsStore {
fun get(): Flow<List<YieldMarketToken>>
fun get(): Flow<List<YieldMarketsResponse.MarketDto>>
suspend fun getSyncOrNull(): List<YieldMarketToken>?
suspend fun getSyncOrNull(): List<YieldMarketsResponse.MarketDto>?
suspend fun store(items: List<YieldMarketToken>)
suspend fun store(items: List<YieldMarketsResponse.MarketDto>)
}

View file

@ -27,18 +27,20 @@ internal class DefaultYieldSupplyMarketRepository(
) : YieldSupplyMarketRepository {
override suspend fun getCachedMarkets(): List<YieldMarketToken>? = withContext(dispatchers.io) {
store.getSyncOrNull()?.enrichNetworkIds()
val cache = store.getSyncOrNull().orEmpty()
val domain = cache.map(YieldMarketTokenConverter::convert)
domain.enrichNetworkIds()
}
override suspend fun updateMarkets(): List<YieldMarketToken> = withContext(dispatchers.io) {
val response = yieldSupplyApi.getYieldMarkets().getOrThrow()
val domain = response.marketDtos.map(YieldMarketTokenConverter::convert)
store.store(domain)
store.store(response.marketDtos)
domain
}
override fun getMarketsFlow(): Flow<List<YieldMarketToken>> = store.get().map {
it.enrichNetworkIds()
it.map(YieldMarketTokenConverter::convert).enrichNetworkIds()
}
override suspend fun getTokenStatus(cryptoCurrencyToken: CryptoCurrency.Token): YieldMarketTokenStatus {

View file

@ -0,0 +1,6 @@
package com.tangem.features.yield.supply.impl.main.entity
internal enum class LoadingStatusMode {
Initial,
LoadApy,
}

View file

@ -8,8 +8,6 @@ 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.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
@ -20,8 +18,10 @@ import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetTokenStatusUseCase
import com.tangem.features.yield.supply.api.YieldSupplyComponent
import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.main.entity.YieldSupplyUM
import com.tangem.features.yield.supply.impl.main.entity.LoadingStatusMode
import com.tangem.features.yield.supply.impl.main.model.transformers.YieldSupplyTokenStatusFailureTransformer
import com.tangem.features.yield.supply.impl.main.model.transformers.YieldSupplyTokenStatusSuccessTransformer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.DelayedWork
import kotlinx.coroutines.CoroutineScope
@ -29,6 +29,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import com.tangem.utils.transformer.update
import javax.inject.Inject
import kotlin.properties.Delegates
@ -102,24 +103,20 @@ internal class YieldSupplyModel @Inject constructor(
}
}
private fun loadTokenStatus(cryptoCurrency: CryptoCurrency.Token) {
private fun loadTokenStatus(mode: LoadingStatusMode) {
val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return
modelScope.launch(dispatchers.default) {
yieldSupplyGetTokenStatusUseCase(cryptoCurrency)
yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken)
.onRight { tokenStatus ->
val newState = if (tokenStatus.isActive) {
YieldSupplyUM.Initial(
title = resourceReference(
id = R.string.yield_module_token_details_earn_notification_title,
formatArgs = wrappedList(tokenStatus.apy),
),
onClick = ::onStartEarningClick,
)
} else {
YieldSupplyUM.Unavailable
}
uiState.update { newState }
uiState.update(
YieldSupplyTokenStatusSuccessTransformer(
tokenStatus = tokenStatus,
onStartEarningClick = ::onStartEarningClick,
mode = mode,
),
)
}.onLeft {
uiState.update { YieldSupplyUM.Unavailable }
uiState.update(YieldSupplyTokenStatusFailureTransformer(mode))
}
}
}
@ -178,8 +175,10 @@ internal class YieldSupplyModel @Inject constructor(
uiState.update { yieldSupplyUM }
if (yieldSupplyUM is YieldSupplyUM.Loading) {
(cryptoCurrency as? CryptoCurrency.Token)?.let(::loadTokenStatus)
when (yieldSupplyUM) {
is YieldSupplyUM.Loading -> loadTokenStatus(LoadingStatusMode.Initial)
is YieldSupplyUM.Content -> loadTokenStatus(LoadingStatusMode.LoadApy)
else -> Unit
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.features.yield.supply.impl.main.model.transformers
import com.tangem.features.yield.supply.impl.main.entity.LoadingStatusMode
import com.tangem.features.yield.supply.impl.main.entity.YieldSupplyUM
import com.tangem.utils.transformer.Transformer
internal class YieldSupplyTokenStatusFailureTransformer(
private val mode: LoadingStatusMode,
) : Transformer<YieldSupplyUM> {
override fun transform(prevState: YieldSupplyUM): YieldSupplyUM {
return when (mode) {
LoadingStatusMode.Initial -> YieldSupplyUM.Unavailable
LoadingStatusMode.LoadApy -> prevState // TODO apply correct UI
}
}
}

View file

@ -0,0 +1,42 @@
package com.tangem.features.yield.supply.impl.main.model.transformers
import com.tangem.domain.yield.supply.models.YieldMarketTokenStatus
import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.main.entity.LoadingStatusMode
import com.tangem.features.yield.supply.impl.main.entity.YieldSupplyUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.utils.transformer.Transformer
internal class YieldSupplyTokenStatusSuccessTransformer(
private val tokenStatus: YieldMarketTokenStatus,
private val onStartEarningClick: () -> Unit,
private val mode: LoadingStatusMode,
) : Transformer<YieldSupplyUM> {
override fun transform(prevState: YieldSupplyUM): YieldSupplyUM {
if (!tokenStatus.isActive) return YieldSupplyUM.Unavailable
return when (mode) {
LoadingStatusMode.Initial -> {
YieldSupplyUM.Initial(
title = resourceReference(
id = R.string.yield_module_token_details_earn_notification_title,
formatArgs = wrappedList(tokenStatus.apy),
),
onClick = onStartEarningClick,
)
}
LoadingStatusMode.LoadApy -> {
if (prevState is YieldSupplyUM.Content) {
prevState.copy(
rewardsApy = stringReference("${tokenStatus.apy}%"),
)
} else {
prevState
}
}
}
}
}

View file

@ -8,16 +8,19 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.decompose.ComposableModularContentComponent
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.features.yield.supply.impl.subcomponents.active.model.YieldSupplyActiveModel
import com.tangem.features.yield.supply.impl.subcomponents.active.ui.YieldSupplyActiveContent
import com.tangem.features.yield.supply.impl.subcomponents.active.ui.YieldSupplyActiveTitle
import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.chart.DefaultYieldSupplyChartComponent
import kotlinx.coroutines.flow.StateFlow
internal class YieldSupplyActiveComponent(
@ -26,6 +29,12 @@ internal class YieldSupplyActiveComponent(
) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
private val model: YieldSupplyActiveModel = getOrCreateModel(params = params)
private val chartComponent = DefaultYieldSupplyChartComponent(
appComponentContext = child("chartComponent"),
params = DefaultYieldSupplyChartComponent.Params(
cryptoCurrency = params.cryptoCurrencyStatusFlow.value.currency as CryptoCurrency.Token,
),
)
@Composable
override fun Title() {
@ -37,7 +46,12 @@ internal class YieldSupplyActiveComponent(
val state by model.uiState.collectAsStateWithLifecycle()
val isBalanceHidden by params.isBalanceHiddenFlow.collectAsStateWithLifecycle()
YieldSupplyActiveContent(state = state, isBalanceHidden = isBalanceHidden, modifier = Modifier)
YieldSupplyActiveContent(
state = state,
isBalanceHidden = isBalanceHidden,
chartComponent = chartComponent,
modifier = Modifier,
)
}
@Composable

View file

@ -10,4 +10,5 @@ internal data class YieldSupplyActiveContentUM(
val subtitle: TextReference,
val subtitleLink: TextReference,
val notificationUM: NotificationUM?,
val apy: TextReference? = null,
)

View file

@ -5,18 +5,22 @@ 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.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
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.features.yield.supply.impl.R
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.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ModelScoped
@ -24,6 +28,7 @@ internal class YieldSupplyActiveModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val yieldSupplyGetProtocolBalanceUseCase: YieldSupplyGetProtocolBalanceUseCase,
private val yieldSupplyGetTokenStatusUseCase: YieldSupplyGetTokenStatusUseCase,
) : Model() {
private val params: YieldSupplyActiveComponent.Params = paramsContainer.require()
@ -87,6 +92,8 @@ internal class YieldSupplyActiveModel @Inject constructor(
null
}
loadApy()
uiState.update {
it.copy(
notificationUM = approvalNotification,
@ -104,6 +111,21 @@ internal class YieldSupplyActiveModel @Inject constructor(
.launchIn(modelScope)
}
private fun loadApy() {
val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return
modelScope.launch(dispatchers.default) {
yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken).onRight { tokenStatus ->
uiState.update {
it.copy(
apy = TextReference.Str("${tokenStatus.apy}%"),
)
}
}.onLeft {
Timber.e("Error loading token status")
}
}
}
private companion object {
const val AAVEV3_PREFIX = "a"
}

View file

@ -8,6 +8,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@ -15,6 +16,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
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.withLink
@ -25,6 +27,7 @@ import androidx.compose.ui.unit.dp
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -35,6 +38,7 @@ import com.tangem.features.yield.supply.impl.subcomponents.active.entity.YieldSu
internal fun YieldSupplyActiveContent(
state: YieldSupplyActiveContentUM,
isBalanceHidden: Boolean,
chartComponent: ComposableContentComponent,
modifier: Modifier = Modifier,
) {
Column(
@ -62,6 +66,9 @@ internal fun YieldSupplyActiveContent(
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
)
CurrentApy(state.apy)
chartComponent.Content(Modifier.padding(bottom = 12.dp))
}
YieldSupplyActiveMyFunds(state = state, isBalanceHidden = isBalanceHidden)
@ -76,6 +83,45 @@ internal fun YieldSupplyActiveContent(
}
}
@Composable
private fun CurrentApy(apy: TextReference?, modifier: Modifier = Modifier) {
Row(modifier = modifier.padding(vertical = 12.dp), verticalAlignment = Alignment.CenterVertically) {
Text(
modifier = modifier.weight(1.0f),
text = stringResourceSafe(R.string.yield_module_earn_sheet_current_apy_title),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.tertiary,
)
AnimatedContent(
targetState = apy?.resolveReference(),
label = "CurrentApy",
) { apyText ->
if (apyText == null) {
TextShimmer(
modifier = modifier.width(56.dp),
text = "",
style = TangemTheme.typography.body1,
)
} else {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
painterResource(R.drawable.ic_arrow_up_8),
tint = TangemTheme.colors.text.accent,
contentDescription = null,
modifier = Modifier.padding(end = 8.dp),
)
Text(
modifier = modifier,
text = apyText,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.accent,
)
}
}
}
}
}
@Composable
private fun YieldSupplyActiveMyFunds(state: YieldSupplyActiveContentUM, isBalanceHidden: Boolean) {
Column(
@ -193,7 +239,11 @@ private fun YieldSupplyActiveBottomSheet_Preview(
@PreviewParameter(YieldSupplyActiveBottomSheetPreviewProvider::class) params: YieldSupplyActiveContentUM,
) {
TangemThemePreview {
YieldSupplyActiveContent(params, true)
YieldSupplyActiveContent(
state = params,
isBalanceHidden = true,
chartComponent = ComposableContentComponent.EMPTY,
)
}
}