Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-22 16:10:55 +05:00
commit f6a9dd4fd5
17 changed files with 579 additions and 139 deletions

View file

@ -10,13 +10,16 @@ interface ComposableBottomSheetComponent {
@Composable
fun BottomSheet()
}
fun getEmptyComposableBottomSheetComponent() = EmptyComposableBottomSheetComponent
companion object {
val EMPTY = EmptyComposableBottomSheetComponent
}
}
object EmptyComposableBottomSheetComponent : ComposableBottomSheetComponent {
override fun dismiss() {}
@Composable
override fun BottomSheet() {}
override fun BottomSheet() {
}
}

View file

@ -9,9 +9,11 @@ fun interface ComposableContentComponent {
@Composable
fun Content(modifier: Modifier)
}
fun getEmptyComposableContentComponent() = EmptyComposableContentComponent
companion object {
val EMPTY = EmptyComposableContentComponent
}
}
object EmptyComposableContentComponent : ComposableContentComponent {
@Composable

View file

@ -15,9 +15,11 @@ interface ComposableModularContentComponent {
@Composable
fun Footer()
}
fun getEmptyComposableModularContentComponent() = EmptyComposableModularContentComponent
companion object {
val EMPTY = EmptyComposableModularContentComponent
}
}
object EmptyComposableModularContentComponent : ComposableModularContentComponent {
@Composable

View file

@ -18,7 +18,6 @@ import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.decompose.navigation.inner.InnerRouter
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.decompose.getEmptyComposableContentComponent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.swap.models.R
import com.tangem.domain.swap.models.SwapDirection
@ -152,9 +151,9 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor(
private fun getDestinationComponent(factoryContext: AppComponentContext): ComposableContentComponent {
val amountContentUM = model.uiState.value.amountUM as? SwapAmountUM.Content
?: return getEmptyComposableContentComponent()
?: return ComposableContentComponent.EMPTY
val secondaryCryptoCurrency = amountContentUM.secondaryCryptoCurrencyStatus?.currency
?: return getEmptyComposableContentComponent()
?: return ComposableContentComponent.EMPTY
return sendDestinationComponentFactory.create(
context = factoryContext,

View file

@ -15,7 +15,6 @@ dependencies {
/** Feature */
implementation(projects.features.yieldSupply.api)
implementation(projects.features.sendV2.api)
/** Core */
implementation(projects.core.configToggles)
@ -43,6 +42,7 @@ dependencies {
implementation(projects.domain.transaction.models)
implementation(projects.domain.transaction)
implementation(projects.domain.yieldSupply)
implementation(projects.libs.crypto)
/** Compose */
implementation(deps.compose.foundation)

View file

@ -5,6 +5,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.LinkAnnotation
@ -19,6 +20,7 @@ import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.containers.FooterContainer
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
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
@ -32,12 +34,14 @@ import kotlinx.collections.immutable.persistentListOf
internal fun YieldSupplyActionContent(
yieldSupplyActionUM: YieldSupplyActionUM,
onFooterClick: () -> Unit,
yieldSupplyNotificationsComponent: ComposableContentComponent,
modifier: Modifier = Modifier,
enableFooterClick: Boolean = true,
iconContent: @Composable ColumnScope.() -> Unit,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.padding(bottom = 16.dp, start = 16.dp, end = 16.dp),
modifier = modifier.padding(bottom = 8.dp, start = 16.dp, end = 16.dp),
) {
iconContent()
SpacerH24()
@ -55,33 +59,43 @@ internal fun YieldSupplyActionContent(
modifier = Modifier.padding(horizontal = 16.dp),
)
SpacerH24()
FooterContainer(
footer = annotatedReference {
append(yieldSupplyActionUM.footer.resolveReference())
appendSpace()
withLink(link = LinkAnnotation.Clickable("LINK_TAG") { onFooterClick() }) {
appendColored(
text = yieldSupplyActionUM.footerLink.resolveReference(),
color = TangemTheme.colors.icon.accent,
)
key(enableFooterClick) {
FooterContainer(
footer = annotatedReference {
append(yieldSupplyActionUM.footer.resolveReference())
appendSpace()
if (enableFooterClick) {
withLink(link = LinkAnnotation.Clickable("LINK_TAG") { onFooterClick() }) {
appendColored(
text = yieldSupplyActionUM.footerLink.resolveReference(),
color = TangemTheme.colors.icon.accent,
)
}
} else {
appendColored(
text = yieldSupplyActionUM.footerLink.resolveReference(),
color = TangemTheme.colors.text.disabled,
)
}
},
paddingValues = PaddingValues(
start = 12.dp,
end = 12.dp,
top = 8.dp,
),
) {
val fee = when (val yieldSupplyFeeUM = yieldSupplyActionUM.yieldSupplyFeeUM) {
is YieldSupplyFeeUM.Content -> yieldSupplyFeeUM.feeValue
YieldSupplyFeeUM.Error -> stringReference(StringsSigns.DASH_SIGN)
YieldSupplyFeeUM.Loading -> null
}
},
paddingValues = PaddingValues(
start = 12.dp,
end = 12.dp,
top = 8.dp,
),
) {
val fee = when (val yieldSupplyFeeUM = yieldSupplyActionUM.yieldSupplyFeeUM) {
is YieldSupplyFeeUM.Content -> yieldSupplyFeeUM.feeValue
YieldSupplyFeeUM.Error -> stringReference(StringsSigns.DASH_SIGN)
YieldSupplyFeeUM.Loading -> null
YieldSupplyFeeRow(
title = resourceReference(R.string.common_network_fee_title),
value = fee,
)
}
YieldSupplyFeeRow(
title = resourceReference(R.string.common_network_fee_title),
value = fee,
)
}
yieldSupplyNotificationsComponent.Content(Modifier)
}
}
@ -96,6 +110,8 @@ private fun YieldSupplyActionContent_Preview(
YieldSupplyActionContent(
yieldSupplyActionUM = params,
onFooterClick = {},
yieldSupplyNotificationsComponent = ComposableContentComponent.EMPTY,
enableFooterClick = false,
modifier = Modifier.background(TangemTheme.colors.background.tertiary),
) {
CurrencyIcon(

View file

@ -0,0 +1,62 @@
package com.tangem.features.yield.supply.impl.subcomponents.notifications
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
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.model.getOrCreateModel
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.yield.supply.impl.subcomponents.notifications.model.YieldSupplyNotificationsModel
import kotlinx.coroutines.flow.StateFlow
internal class YieldSupplyNotificationsComponent(
private val appComponentContext: AppComponentContext,
params: Params,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: YieldSupplyNotificationsModel = getOrCreateModel(params = params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
Column(
modifier = modifier
.fillMaxWidth()
.animateContentSize(),
) {
state.forEachIndexed { index, item ->
Notification(
config = item.config,
modifier = Modifier.conditional(index == 0) {
Modifier.padding(top = 16.dp)
},
containerColor = TangemTheme.colors.background.action,
iconTint = null,
)
}
}
}
data class Params(
val userWalletId: UserWalletId,
val cryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>,
val feeCryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>,
val callback: ModelCallback,
)
interface ModelCallback {
fun onFeeReload()
}
}

View file

@ -0,0 +1,44 @@
package com.tangem.features.yield.supply.impl.subcomponents.notifications
import com.tangem.features.yield.supply.impl.subcomponents.notifications.entity.YieldSupplyNotificationData
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import javax.inject.Inject
import javax.inject.Singleton
interface YieldSupplyNotificationsUpdateTrigger {
/** Flow returns whether there is error notifications */
val hasErrorFlow: Flow<Boolean>
/** Trigger fee check reload */
suspend fun triggerUpdate(data: YieldSupplyNotificationData)
}
interface YieldSupplyNotificationsUpdateListener {
/** Flow triggers notifications update */
val updateTriggerFlow: Flow<YieldSupplyNotificationData>
/** Trigger return callback with check result */
suspend fun callbackHasError(hasError: Boolean)
}
@Singleton
internal class DefaultYieldSupplyNotificationsUpdateTrigger @Inject constructor() :
YieldSupplyNotificationsUpdateTrigger,
YieldSupplyNotificationsUpdateListener {
override val hasErrorFlow: Flow<Boolean>
field = MutableSharedFlow()
override val updateTriggerFlow: Flow<YieldSupplyNotificationData>
field = MutableSharedFlow()
override suspend fun triggerUpdate(data: YieldSupplyNotificationData) {
updateTriggerFlow.emit(data)
}
override suspend fun callbackHasError(hasError: Boolean) {
hasErrorFlow.emit(hasError)
}
}

View file

@ -0,0 +1,33 @@
package com.tangem.features.yield.supply.impl.subcomponents.notifications.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.yield.supply.impl.subcomponents.notifications.DefaultYieldSupplyNotificationsUpdateTrigger
import com.tangem.features.yield.supply.impl.subcomponents.notifications.YieldSupplyNotificationsUpdateListener
import com.tangem.features.yield.supply.impl.subcomponents.notifications.YieldSupplyNotificationsUpdateTrigger
import com.tangem.features.yield.supply.impl.subcomponents.notifications.model.YieldSupplyNotificationsModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(ModelComponent::class)
internal interface YieldSupplyNotificationsModule {
@Binds
@IntoMap
@ClassKey(YieldSupplyNotificationsModel::class)
fun provideYieldSupplyNotificationsModel(impl: YieldSupplyNotificationsModel): Model
@Binds
fun provideYieldSupplyNotificationsUpdateListener(
impl: DefaultYieldSupplyNotificationsUpdateTrigger,
): YieldSupplyNotificationsUpdateListener
@Binds
fun provideYieldSupplyNotificationsUpdateTrigger(
impl: DefaultYieldSupplyNotificationsUpdateTrigger,
): YieldSupplyNotificationsUpdateTrigger
}

View file

@ -0,0 +1,9 @@
package com.tangem.features.yield.supply.impl.subcomponents.notifications.entity
import com.tangem.domain.transaction.error.GetFeeError
import java.math.BigDecimal
data class YieldSupplyNotificationData(
val feeValue: BigDecimal?,
val feeError: GetFeeError?,
)

View file

@ -0,0 +1,89 @@
package com.tangem.features.yield.supply.impl.subcomponents.notifications.model
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.common.ui.notifications.NotificationsFactory.addExceedsBalanceNotification
import com.tangem.common.ui.notifications.NotificationsFactory.addFeeUnreachableNotification
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.GetBalanceNotEnoughForFeeWarningUseCase
import com.tangem.features.yield.supply.impl.subcomponents.notifications.YieldSupplyNotificationsComponent
import com.tangem.features.yield.supply.impl.subcomponents.notifications.YieldSupplyNotificationsUpdateListener
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.orZero
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.flow.*
import javax.inject.Inject
@ModelScoped
internal class YieldSupplyNotificationsModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val appRouter: AppRouter,
private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase,
private val yieldSupplyNotificationsUpdateListener: YieldSupplyNotificationsUpdateListener,
) : Model() {
private val params: YieldSupplyNotificationsComponent.Params = paramsContainer.require()
val uiState: StateFlow<ImmutableList<NotificationUM>>
field = MutableStateFlow(persistentListOf())
init {
subscribeToNotificationTrigger()
}
private fun subscribeToNotificationTrigger() {
yieldSupplyNotificationsUpdateListener.updateTriggerFlow
.onEach { data ->
val cryptoCurrencyStatus = params.cryptoCurrencyStatusFlow.value
val feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatusFlow.value
val notifications = buildList {
val cryptoCurrencyWarning = getBalanceNotEnoughForFeeWarningUseCase(
fee = data.feeValue.orZero(),
userWalletId = params.userWalletId,
tokenStatus = cryptoCurrencyStatus,
coinStatus = feeCryptoCurrencyStatus,
).getOrNull()
addExceedsBalanceNotification(
cryptoCurrencyWarning = cryptoCurrencyWarning,
cryptoCurrencyStatus = cryptoCurrencyStatus,
shouldMergeFeeNetworkName = BlockchainUtils.isArbitrum(
blockchainId = cryptoCurrencyStatus.currency.network.backendId,
),
onClick = ::openTokenDetails,
onAnalyticsEvent = { },
)
addFeeUnreachableNotification(
tokenStatus = cryptoCurrencyStatus,
coinStatus = feeCryptoCurrencyStatus,
feeError = data.feeError,
onClick = ::openTokenDetails,
onReload = params.callback::onFeeReload,
)
}
uiState.update { notifications.toPersistentList() }
yieldSupplyNotificationsUpdateListener.callbackHasError(notifications.any())
}.launchIn(modelScope)
}
private fun openTokenDetails(cryptoCurrency: CryptoCurrency) {
appRouter.push(
AppRoute.CurrencyDetails(
userWalletId = params.userWalletId,
currency = cryptoCurrency,
),
)
}
}

View file

@ -14,6 +14,7 @@ import androidx.compose.ui.res.vectorResource
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.PrimaryButtonIconEnd
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
@ -25,7 +26,9 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.yield.supply.impl.R
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.ui.YieldSupplyActionContent
import com.tangem.features.yield.supply.impl.subcomponents.notifications.YieldSupplyNotificationsComponent
import com.tangem.features.yield.supply.impl.subcomponents.startearning.model.YieldSupplyStartEarningModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@ -37,6 +40,16 @@ internal class YieldSupplyStartEarningComponent(
private val model: YieldSupplyStartEarningModel = getOrCreateModel(params = params)
private val yieldSupplyNotificationsComponent = YieldSupplyNotificationsComponent(
appComponentContext = child("yieldSupplyEnterNotifications"),
params = YieldSupplyNotificationsComponent.Params(
userWalletId = params.userWalletId,
cryptoCurrencyStatusFlow = model.cryptoCurrencyStatusFlow,
feeCryptoCurrencyStatusFlow = model.feeCryptoCurrencyStatusFlow,
callback = model,
),
)
init {
model.uiState.onEach(params.callback::onResult).launchIn(componentScope)
}
@ -57,6 +70,8 @@ internal class YieldSupplyStartEarningComponent(
YieldSupplyActionContent(
yieldSupplyActionUM = state,
onFooterClick = params.callback::onFeePolicyClick,
enableFooterClick = state.yieldSupplyFeeUM is YieldSupplyFeeUM.Content,
yieldSupplyNotificationsComponent = yieldSupplyNotificationsComponent,
modifier = modifier,
) {
Box(

View file

@ -6,13 +6,8 @@ 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.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.extensions.combinedReference
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
import com.tangem.domain.models.currency.CryptoCurrencyStatus
@ -26,15 +21,18 @@ import com.tangem.domain.yield.supply.usecase.YieldSupplyStartEarningUseCase
import com.tangem.features.yield.supply.impl.R
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.subcomponents.notifications.YieldSupplyNotificationsComponent
import com.tangem.features.yield.supply.impl.subcomponents.notifications.YieldSupplyNotificationsUpdateTrigger
import com.tangem.features.yield.supply.impl.subcomponents.notifications.entity.YieldSupplyNotificationData
import com.tangem.features.yield.supply.impl.subcomponents.startearning.YieldSupplyStartEarningComponent
import com.tangem.utils.StringsSigns.DOT
import com.tangem.features.yield.supply.impl.subcomponents.startearning.model.transformers.YieldSupplyStartEarningFeeContentTransformer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.toPersistentList
import com.tangem.utils.extensions.orZero
import com.tangem.utils.transformer.update
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import java.math.BigDecimal
import java.math.RoundingMode
import javax.inject.Inject
import kotlin.properties.Delegates
@ -50,21 +48,22 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
private val yieldSupplyStartEarningUseCase: YieldSupplyStartEarningUseCase,
private val yieldSupplyEstimateEnterFeeUseCase: YieldSupplyEstimateEnterFeeUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
) : Model() {
private val yieldSupplyNotificationsUpdateTrigger: YieldSupplyNotificationsUpdateTrigger,
) : Model(), YieldSupplyNotificationsComponent.ModelCallback {
private val params: YieldSupplyStartEarningComponent.Params = paramsContainer.require()
private val cryptoCurrency = params.cryptoCurrency
private var userWallet: UserWallet by Delegates.notNull()
private val cryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>
val cryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>
field = MutableStateFlow(
CryptoCurrencyStatus(
currency = cryptoCurrency,
value = CryptoCurrencyStatus.Loading,
),
)
private val feeCryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>
val feeCryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>
field = MutableStateFlow(
CryptoCurrencyStatus(
currency = cryptoCurrency,
@ -96,6 +95,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
modelScope.launch {
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
subscribeOnCurrencyStatusUpdates()
subscribeOnNotificationsErrors()
}
}
@ -108,61 +108,48 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
maxNetworkFee = MAX_NETWORK_FEE,
).getOrNull() ?: return
val updatedTransactionList = yieldSupplyEstimateEnterFeeUseCase.invoke(
userWallet = userWallet,
cryptoCurrency = cryptoCurrency,
transactionDataList = transactionListData,
).getOrNull() ?: return
val feeSum = updatedTransactionList.sumOf {
it.fee?.amount?.value ?: BigDecimal.ZERO
}
val crypto = feeSum.format { crypto(feeCryptoCurrencyStatusFlow.value.currency) }
val fiatFeeValue = feeCryptoCurrencyStatusFlow.value.value.fiatRate?.let { rate ->
feeSum.multiply(rate)
}
val fiat = fiatFeeValue.format { fiat(appCurrency.code, appCurrency.symbol) }
val tokenCryptoFeeValue = cryptoCurrencyStatus.value.fiatRate?.let { rate ->
fiatFeeValue?.divide(rate, cryptoCurrency.decimals, RoundingMode.HALF_UP)
}
val tokenCryptoFee = tokenCryptoFeeValue.format { crypto(cryptoCurrency) }
val maxCryptoFee = MAX_NETWORK_FEE.format { crypto(cryptoCurrency) }
val maxFiatFeeValue = cryptoCurrencyStatus.value.fiatRate?.let { rate ->
MAX_NETWORK_FEE.divide(rate, cryptoCurrency.decimals, RoundingMode.HALF_UP)
}
val maxFiatFee = maxFiatFeeValue.format { fiat(appCurrency.code, appCurrency.symbol) }
uiState.update {
if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Loading) {
it.copy(yieldSupplyFeeUM = YieldSupplyFeeUM.Loading)
} else {
it.copy(
isPrimaryButtonEnabled = true, // todo yield supply check for notifications
yieldSupplyFeeUM = YieldSupplyFeeUM.Content(
transactionDataList = updatedTransactionList.toPersistentList(),
feeValue = combinedReference(
stringReference(crypto),
stringReference(" $DOT "),
stringReference(fiat),
),
currentNetworkFeeValue = combinedReference(
stringReference(tokenCryptoFee),
stringReference(" $DOT "),
stringReference(fiat),
),
maxNetworkFeeValue = combinedReference(
stringReference(maxCryptoFee),
stringReference(" $DOT "),
stringReference(maxFiatFee),
),
it.copy(yieldSupplyFeeUM = YieldSupplyFeeUM.Loading)
}
yieldSupplyEstimateEnterFeeUseCase.invoke(
userWallet = userWallet,
cryptoCurrency = feeCryptoCurrencyStatusFlow.value.currency,
transactionDataList = transactionListData,
).fold(
ifLeft = { feeError ->
yieldSupplyNotificationsUpdateTrigger.triggerUpdate(
data = YieldSupplyNotificationData(
feeValue = null,
feeError = feeError,
),
)
}
}
uiState.update {
it.copy(yieldSupplyFeeUM = YieldSupplyFeeUM.Error)
}
},
ifRight = { updatedTransactionList ->
val feeSum = updatedTransactionList.sumOf {
it.fee?.amount?.value.orZero()
}
uiState.update(
YieldSupplyStartEarningFeeContentTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatusFlow.value,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatusFlow.value,
appCurrency = appCurrency,
updatedTransactionList = updatedTransactionList,
feeValue = feeSum,
maxNetworkFee = MAX_NETWORK_FEE,
),
)
yieldSupplyNotificationsUpdateTrigger.triggerUpdate(
data = YieldSupplyNotificationData(
feeValue = feeSum,
feeError = null,
),
)
},
)
}
fun onClick() {
@ -205,6 +192,21 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
}
}
private fun subscribeOnNotificationsErrors() {
yieldSupplyNotificationsUpdateTrigger.hasErrorFlow
.onEach { hasError ->
uiState.update {
it.copy(isPrimaryButtonEnabled = !hasError)
}
}.launchIn(modelScope)
}
override fun onFeeReload() {
modelScope.launch {
onLoadFee()
}
}
private fun getCurrenciesStatusUpdates() {
getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet(
userWalletId = params.userWalletId,

View file

@ -0,0 +1,74 @@
package com.tangem.features.yield.supply.impl.subcomponents.startearning.model.transformers
import com.tangem.blockchain.common.TransactionData
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.stringReference
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.CryptoCurrencyStatus
import com.tangem.features.yield.supply.impl.common.entity.YieldSupplyActionUM
import com.tangem.features.yield.supply.impl.common.entity.YieldSupplyFeeUM
import com.tangem.utils.StringsSigns.DOT
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.toPersistentList
import java.math.BigDecimal
import java.math.RoundingMode
internal class YieldSupplyStartEarningFeeContentTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrency: AppCurrency,
private val updatedTransactionList: List<TransactionData.Uncompiled>,
private val feeValue: BigDecimal,
private val maxNetworkFee: BigDecimal,
) : Transformer<YieldSupplyActionUM> {
override fun transform(prevState: YieldSupplyActionUM): YieldSupplyActionUM {
val cryptoCurrency = cryptoCurrencyStatus.currency
val fiatRate = cryptoCurrencyStatus.value.fiatRate
val feeFiatRate = feeCryptoCurrencyStatus.value.fiatRate
val crypto = feeValue.format { crypto(feeCryptoCurrencyStatus.currency) }
val fiatFeeValue = feeFiatRate?.let(feeValue::multiply)
val fiat = fiatFeeValue.format { fiat(appCurrency.code, appCurrency.symbol) }
val tokenCryptoFeeValue = fiatRate?.let { rate ->
fiatFeeValue?.divide(rate, cryptoCurrency.decimals, RoundingMode.HALF_UP)
}
val tokenCryptoFee = tokenCryptoFeeValue.format { crypto(cryptoCurrency) }
val maxCryptoFee = maxNetworkFee.format { crypto(cryptoCurrency) }
val maxFiatFeeValue = fiatRate?.let { rate ->
maxNetworkFee.divide(rate, cryptoCurrency.decimals, RoundingMode.HALF_UP)
}
val maxFiatFee = maxFiatFeeValue.format { fiat(appCurrency.code, appCurrency.symbol) }
return if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Loading) {
prevState.copy(yieldSupplyFeeUM = YieldSupplyFeeUM.Loading)
} else {
prevState.copy(
yieldSupplyFeeUM = YieldSupplyFeeUM.Content(
transactionDataList = updatedTransactionList.toPersistentList(),
feeValue = combinedReference(
stringReference(crypto),
stringReference(" $DOT "),
stringReference(fiat),
),
currentNetworkFeeValue = combinedReference(
stringReference(tokenCryptoFee),
stringReference(" $DOT "),
stringReference(fiat),
),
maxNetworkFeeValue = combinedReference(
stringReference(maxCryptoFee),
stringReference(" $DOT "),
stringReference(maxFiatFee),
),
),
)
}
}
}

View file

@ -15,6 +15,7 @@ import androidx.compose.ui.res.vectorResource
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.PrimaryButtonIconEnd
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
@ -25,6 +26,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.common.ui.YieldSupplyActionContent
import com.tangem.features.yield.supply.impl.subcomponents.notifications.YieldSupplyNotificationsComponent
import com.tangem.features.yield.supply.impl.subcomponents.stopearning.model.YieldSupplyStopEarningModel
import kotlinx.coroutines.flow.StateFlow
@ -35,6 +37,16 @@ internal class YieldSupplyStopEarningComponent(
private val model: YieldSupplyStopEarningModel = getOrCreateModel(params = params)
private val yieldSupplyNotificationsComponent = YieldSupplyNotificationsComponent(
appComponentContext = child("yieldSupplyExitNotifications"),
params = YieldSupplyNotificationsComponent.Params(
userWalletId = params.userWallet.walletId,
cryptoCurrencyStatusFlow = params.cryptoCurrencyStatusFlow,
feeCryptoCurrencyStatusFlow = model.feeCryptoCurrencyStatusFlow,
callback = model,
),
)
@Composable
override fun Title() {
TangemModalBottomSheetTitle(
@ -50,6 +62,7 @@ internal class YieldSupplyStopEarningComponent(
YieldSupplyActionContent(
yieldSupplyActionUM = state,
onFooterClick = model::onReadMoreClick,
yieldSupplyNotificationsComponent = yieldSupplyNotificationsComponent,
modifier = modifier,
) {
Box(

View file

@ -6,10 +6,8 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.extensions.*
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.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
@ -20,13 +18,15 @@ import com.tangem.domain.yield.supply.usecase.YieldSupplyStopEarningUseCase
import com.tangem.features.yield.supply.impl.R
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.subcomponents.notifications.YieldSupplyNotificationsComponent
import com.tangem.features.yield.supply.impl.subcomponents.notifications.YieldSupplyNotificationsUpdateTrigger
import com.tangem.features.yield.supply.impl.subcomponents.notifications.entity.YieldSupplyNotificationData
import com.tangem.features.yield.supply.impl.subcomponents.stopearning.YieldSupplyStopEarningComponent
import com.tangem.utils.StringsSigns.DOT
import com.tangem.features.yield.supply.impl.subcomponents.stopearning.model.transformer.YieldSupplyStopEarningFeeContentTransformer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import com.tangem.utils.extensions.orZero
import com.tangem.utils.transformer.update
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ -42,7 +42,8 @@ internal class YieldSupplyStopEarningModel @Inject constructor(
private val sendTransactionUseCase: SendTransactionUseCase,
private val yieldSupplyStopEarningUseCase: YieldSupplyStopEarningUseCase,
private val urlOpener: UrlOpener,
) : Model() {
private val yieldSupplyNotificationsUpdateTrigger: YieldSupplyNotificationsUpdateTrigger,
) : Model(), YieldSupplyNotificationsComponent.ModelCallback {
private val params: YieldSupplyStopEarningComponent.Params = paramsContainer.require()
@ -51,10 +52,10 @@ internal class YieldSupplyStopEarningModel @Inject constructor(
private val cryptoCurrency = cryptoCurrencyStatus.currency
private var userWallet = params.userWallet
private val feeCryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>
val feeCryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>
field = MutableStateFlow(
CryptoCurrencyStatus(
cryptoCurrencyStatus.currency,
currency = cryptoCurrencyStatus.currency,
value = CryptoCurrencyStatus.Loading,
),
)
@ -84,6 +85,13 @@ internal class YieldSupplyStopEarningModel @Inject constructor(
modelScope.launch {
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
subscribeOnCurrencyStatusUpdates()
subscribeOnNotificationsErrors()
}
}
override fun onFeeReload() {
modelScope.launch {
onLoadFee()
}
}
@ -130,39 +138,57 @@ internal class YieldSupplyStopEarningModel @Inject constructor(
fee = null,
).getOrNull() ?: return
val fee = getFeeUseCase(
uiState.update {
it.copy(yieldSupplyFeeUM = YieldSupplyFeeUM.Loading)
}
getFeeUseCase(
transactionData = exitTransitionData,
userWallet = userWallet,
network = cryptoCurrency.network,
).getOrNull() ?: return
val feeCryptoValue = fee.normal.amount.value
val feeFiatValue = feeCryptoCurrencyStatus.value.fiatRate?.let { rate ->
feeCryptoValue?.multiply(rate)
}
val cryptoFee = feeCryptoValue.format { crypto(feeCryptoCurrencyStatus.currency) }
val fiatFee = feeFiatValue.format { fiat(appCurrency.code, appCurrency.symbol) }
uiState.update {
if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Loading) {
it.copy(yieldSupplyFeeUM = YieldSupplyFeeUM.Loading)
} else {
it.copy(
isPrimaryButtonEnabled = true,
yieldSupplyFeeUM = YieldSupplyFeeUM.Content(
transactionDataList = persistentListOf(exitTransitionData.copy(fee = fee.normal)),
feeValue = combinedReference(
stringReference(cryptoFee),
stringReference(" $DOT "),
stringReference(fiatFee),
),
currentNetworkFeeValue = TextReference.EMPTY,
maxNetworkFeeValue = TextReference.EMPTY,
).fold(
ifLeft = { feeError ->
yieldSupplyNotificationsUpdateTrigger.triggerUpdate(
data = YieldSupplyNotificationData(
feeValue = null,
feeError = feeError,
),
)
}
}
uiState.update {
it.copy(yieldSupplyFeeUM = YieldSupplyFeeUM.Error)
}
},
ifRight = { fee ->
val fee = fee.normal
val feeCryptoValue = fee.amount.value.orZero()
uiState.update(
YieldSupplyStopEarningFeeContentTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
appCurrency = appCurrency,
transactions = listOf(exitTransitionData.copy(fee = fee)),
feeValue = feeCryptoValue,
),
)
yieldSupplyNotificationsUpdateTrigger.triggerUpdate(
data = YieldSupplyNotificationData(
feeValue = feeCryptoValue,
feeError = null,
),
)
},
)
}
private fun subscribeOnNotificationsErrors() {
yieldSupplyNotificationsUpdateTrigger.hasErrorFlow
.onEach { hasError ->
uiState.update {
it.copy(isPrimaryButtonEnabled = !hasError)
}
}.launchIn(modelScope)
}
private companion object {

View file

@ -0,0 +1,51 @@
package com.tangem.features.yield.supply.impl.subcomponents.stopearning.model.transformer
import com.tangem.blockchain.common.TransactionData
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.stringReference
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.CryptoCurrencyStatus
import com.tangem.features.yield.supply.impl.common.entity.YieldSupplyActionUM
import com.tangem.features.yield.supply.impl.common.entity.YieldSupplyFeeUM
import com.tangem.utils.StringsSigns.DOT
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.toPersistentList
import java.math.BigDecimal
internal class YieldSupplyStopEarningFeeContentTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrency: AppCurrency,
private val transactions: List<TransactionData.Uncompiled>,
private val feeValue: BigDecimal,
) : Transformer<YieldSupplyActionUM> {
override fun transform(prevState: YieldSupplyActionUM): YieldSupplyActionUM {
val feeFiatValue = feeCryptoCurrencyStatus.value.fiatRate?.let { rate ->
feeValue.multiply(rate)
}
val cryptoFee = feeValue.format { crypto(feeCryptoCurrencyStatus.currency) }
val fiatFee = feeFiatValue.format { fiat(appCurrency.code, appCurrency.symbol) }
return if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Loading) {
prevState.copy(yieldSupplyFeeUM = YieldSupplyFeeUM.Loading)
} else {
prevState.copy(
isPrimaryButtonEnabled = true,
yieldSupplyFeeUM = YieldSupplyFeeUM.Content(
transactionDataList = transactions.toPersistentList(),
feeValue = combinedReference(
stringReference(cryptoFee),
stringReference(" $DOT "),
stringReference(fiatFee),
),
currentNetworkFeeValue = TextReference.EMPTY,
maxNetworkFeeValue = TextReference.EMPTY,
),
)
}
}
}