From 687d2e1423fefe5debb363b2ed55f81ceca63ed8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Sep 2025 16:20:57 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../YieldSupplyEstimateEnterFeeUseCase.kt | 38 ++- .../impl/common/entity/YieldSupplyFeeUM.kt | 26 ++ .../common/ui/YieldSupplyActionContent.kt | 147 ++++++++++++ .../YieldSupplyStartEarningComponent.kt | 107 +++++++++ .../YieldSupplyStartEarningEntryComponent.kt | 94 ++++++++ .../YieldSupplyStartEarningRoute.kt | 8 + .../di/YieldSupplyStartEarningModule.kt | 26 ++ .../YieldSupplyStartEarningContentUM.kt | 20 ++ .../YieldSupplyStartEarningEntryModel.kt | 33 +++ .../model/YieldSupplyStartEarningModel.kt | 222 ++++++++++++++++++ .../ui/YieldSupplyStartEarningBottomSheet.kt | 48 ++++ gradle/tangem_dependencies.toml | 3 +- 12 files changed, 762 insertions(+), 10 deletions(-) create mode 100644 features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/entity/YieldSupplyFeeUM.kt create mode 100644 features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/ui/YieldSupplyActionContent.kt create mode 100644 features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/YieldSupplyStartEarningComponent.kt create mode 100644 features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/YieldSupplyStartEarningEntryComponent.kt create mode 100644 features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/YieldSupplyStartEarningRoute.kt create mode 100644 features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/di/YieldSupplyStartEarningModule.kt create mode 100644 features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/entity/YieldSupplyStartEarningContentUM.kt create mode 100644 features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningEntryModel.kt create mode 100644 features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt create mode 100644 features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/ui/YieldSupplyStartEarningBottomSheet.kt 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 0c7dd8bd8c..6c84240db5 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 @@ -1,13 +1,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.models.currency.CryptoCurrency 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 class YieldSupplyEstimateEnterFeeUseCase( private val feeRepository: FeeRepository, @@ -18,19 +21,36 @@ class YieldSupplyEstimateEnterFeeUseCase( cryptoCurrency: CryptoCurrency, transactionDataList: List, ): Either> = Either.catch { - transactionDataList.mapIndexed { index, transaction -> - val fee = feeRepository.calculateFee( - userWallet = userWallet, - cryptoCurrency = cryptoCurrency, - transactionData = transaction, - ).normal + val withCalculatedFee = transactionDataList.filter { + (it.extras as? EthereumTransactionExtras)?.callData !is EthereumYieldSupplyEnterCallData + }.map { transaction -> + transaction.copy( + fee = feeRepository.calculateFee( + userWallet = userWallet, + cryptoCurrency = cryptoCurrency, + transactionData = transaction, + ).normal, + ) + } - if (index == transactionDataList.lastIndex) { // todo yield supply replace with check for contract - transaction.copy(fee = fee.fixFee(cryptoCurrency)) + val withEstimatedCalculatedFee = transactionDataList.filter { + (it.extras as? EthereumTransactionExtras)?.callData is EthereumYieldSupplyEnterCallData + }.map { transaction -> + if (transactionDataList.isSingleItem()) { + transaction.copy( + fee = feeRepository.calculateFee( + userWallet = userWallet, + cryptoCurrency = cryptoCurrency, + transactionData = transaction, + ).normal, + ) } else { - transaction.copy(fee = fee) + transaction.copy(fee = withCalculatedFee.first().fee?.fixFee(cryptoCurrency)) } } + + // Transactions order must be preserved + withCalculatedFee + withEstimatedCalculatedFee }.mapLeft(feeErrorResolver::resolve) private fun Fee.fixFee(cryptoCurrency: CryptoCurrency) = when (this) { diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/entity/YieldSupplyFeeUM.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/entity/YieldSupplyFeeUM.kt new file mode 100644 index 0000000000..9c30cb4f8e --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/entity/YieldSupplyFeeUM.kt @@ -0,0 +1,26 @@ +package com.tangem.features.yield.supply.impl.common.entity + +import androidx.compose.runtime.Immutable +import com.tangem.blockchain.common.TransactionData +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal sealed class YieldSupplyFeeUM { + data object Loading : YieldSupplyFeeUM() + data object Error : YieldSupplyFeeUM() + data class Content( + val transactionDataList: ImmutableList, + val feeValue: TextReference, + ) : YieldSupplyFeeUM() +} + +internal data class YieldSupplyActionUM( + val title: TextReference, + val subtitle: TextReference, + val footer: TextReference, + val currencyIconState: CurrencyIconState, + val yieldSupplyFeeUM: YieldSupplyFeeUM, + val isPrimaryButtonEnabled: Boolean, +) \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/ui/YieldSupplyActionContent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/ui/YieldSupplyActionContent.kt new file mode 100644 index 0000000000..98746a3d45 --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/ui/YieldSupplyActionContent.kt @@ -0,0 +1,147 @@ +package com.tangem.features.yield.supply.impl.common.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +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.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.* +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.extensions.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +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.utils.StringsSigns +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun YieldSupplyActionContent( + yieldSupplyActionUM: YieldSupplyActionUM, + modifier: Modifier = Modifier, + iconContent: @Composable ColumnScope.() -> Unit, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier.padding(bottom = 16.dp, start = 16.dp, end = 16.dp), + ) { + iconContent() + SpacerH24() + Text( + text = yieldSupplyActionUM.title.resolveReference(), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + ) + SpacerH8() + Text( + text = yieldSupplyActionUM.subtitle.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = 16.dp), + ) + SpacerH24() + FooterContainer( + footer = yieldSupplyActionUM.footer, + paddingValues = PaddingValues( + start = 12.dp, + end = 12.dp, + top = 8.dp, + ), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(14.dp)) + .background(TangemTheme.colors.background.action) + .padding(horizontal = 16.dp, vertical = 12.dp), + ) { + Text( + text = stringResourceSafe(R.string.common_network_fee_title), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + SpacerWMax() + when (val fee = yieldSupplyActionUM.yieldSupplyFeeUM) { + is YieldSupplyFeeUM.Content -> Text( + text = fee.feeValue.resolveReference(), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.tertiary, + ) + YieldSupplyFeeUM.Error -> Text( + text = stringResourceSafe(R.string.common_fee_error), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.attention, + ) + YieldSupplyFeeUM.Loading -> { + TextShimmer( + style = TangemTheme.typography.body1, + text = stringResourceSafe(R.string.common_fee_error), + ) + } + } + } + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 344) +@Preview(showBackground = true, widthDp = 344, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun YieldSupplyActionContent_Preview( + @PreviewParameter(YieldSupplyActionContentPreviewProvider::class) params: YieldSupplyActionUM, +) { + TangemThemePreview { + YieldSupplyActionContent( + yieldSupplyActionUM = params, + modifier = Modifier.background(TangemTheme.colors.background.tertiary), + ) { + CurrencyIcon( + state = CurrencyIconState.Loading, + shouldDisplayNetwork = false, + iconSize = 48.dp, + modifier = Modifier.size(48.dp), + ) + } + } +} + +private class YieldSupplyActionContentPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + YieldSupplyActionUM( + title = resourceReference(R.string.yield_module_start_earning), + subtitle = resourceReference( + R.string.yield_module_start_earning_sheet_description, + wrappedList("USDT"), + ), + footer = combinedReference( + resourceReference(R.string.yield_module_start_earning_sheet_next_deposits), + stringReference(StringsSigns.WHITE_SPACE), + resourceReference(R.string.yield_module_start_earning_sheet_fee_policy), + ), + currencyIconState = CurrencyIconState.Loading, + yieldSupplyFeeUM = YieldSupplyFeeUM.Content( + transactionDataList = persistentListOf(), + feeValue = stringReference("0.00020 ETH • \$0.99"), + ), + isPrimaryButtonEnabled = false, + ), + ) +} +// endregion \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/YieldSupplyStartEarningComponent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/YieldSupplyStartEarningComponent.kt new file mode 100644 index 0000000000..99e5075b5f --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/YieldSupplyStartEarningComponent.kt @@ -0,0 +1,107 @@ +package com.tangem.features.yield.supply.impl.subcomponents.startearning + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +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.vectorResource +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.PrimaryButtonIconEnd +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle +import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.decompose.ComposableModularContentComponent +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +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.ui.YieldSupplyActionContent +import com.tangem.features.yield.supply.impl.subcomponents.startearning.model.YieldSupplyStartEarningModel + +internal class YieldSupplyStartEarningComponent( + private val appComponentContext: AppComponentContext, + private val params: Params, +) : ComposableModularContentComponent, AppComponentContext by appComponentContext { + + private val model: YieldSupplyStartEarningModel = getOrCreateModel(params = params) + + @Composable + override fun Title() { + TangemModalBottomSheetTitle( + endIconRes = R.drawable.ic_close_24, + onEndClick = params.callback::onBackClick, + ) + } + + @Suppress("MagicNumber") + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + + YieldSupplyActionContent( + yieldSupplyActionUM = state, + modifier = modifier, + ) { + Box( + modifier = Modifier + .width(80.dp) + .padding(vertical = 1.dp), + ) { + CurrencyIcon( + state = state.currencyIconState, + shouldDisplayNetwork = false, + iconSize = 48.dp, + modifier = Modifier + .size(48.dp) + .clip(RoundedCornerShape(48.dp)) + .align(Alignment.CenterStart), + ) + Image( + imageVector = ImageVector.vectorResource(R.drawable.ic_aave_36), + contentDescription = null, + modifier = Modifier + .background(TangemTheme.colors.background.tertiary, RoundedCornerShape(51.dp)) + .padding(3.dp) + .size(48.dp) + .align(Alignment.CenterEnd), + ) + } + } + } + + @Composable + override fun Footer() { + val state by model.uiState.collectAsStateWithLifecycle() + + PrimaryButtonIconEnd( + text = stringResourceSafe(R.string.yield_module_start_earning), + onClick = model::onClick, + enabled = state.isPrimaryButtonEnabled, + iconResId = R.drawable.ic_tangem_24, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) + } + + data class Params( + val userWalletId: UserWalletId, + val cryptoCurrency: CryptoCurrency, + val callback: ModelCallback, + ) + + interface ModelCallback { + fun onBackClick() + fun onFeePolicyClick() + fun onTransactionSent() + } +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/YieldSupplyStartEarningEntryComponent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/YieldSupplyStartEarningEntryComponent.kt new file mode 100644 index 0000000000..d13e9293d2 --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/YieldSupplyStartEarningEntryComponent.kt @@ -0,0 +1,94 @@ +package com.tangem.features.yield.supply.impl.subcomponents.startearning + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +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.tangem.core.decompose.context.AppComponentContext +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.ComposableBottomSheetComponent +import com.tangem.core.ui.decompose.ComposableModularContentComponent +import com.tangem.core.ui.decompose.getEmptyComposableModularContentComponent +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.yield.supply.impl.subcomponents.startearning.model.YieldSupplyStartEarningEntryModel +import com.tangem.features.yield.supply.impl.subcomponents.startearning.ui.YieldSupplyStartEarningBottomSheet + +internal class YieldSupplyStartEarningEntryComponent( + private val appComponentContext: AppComponentContext, + private val params: Params, +) : ComposableBottomSheetComponent, AppComponentContext by appComponentContext { + + private val stackNavigation = StackNavigation() + + private val innerRouter = InnerRouter( + stackNavigation = stackNavigation, + popCallback = { onChildBack() }, + ) + + private val model: YieldSupplyStartEarningEntryModel = getOrCreateModel(params = params, router = innerRouter) + + private val innerStack = childStack( + key = "startEarningStack", + source = stackNavigation, + serializer = null, + initialConfiguration = YieldSupplyStartEarningRoute.Action, + handleBackButton = true, + childFactory = { configuration, factoryContext -> + createChild( + configuration, + childByContext( + componentContext = factoryContext, + router = innerRouter, + ), + ) + }, + ) + + override fun dismiss() { + params.onDismiss(false) + } + + @Composable + override fun BottomSheet() { + val stackState by innerStack.subscribeAsState() + + YieldSupplyStartEarningBottomSheet( + stackState = stackState, + onDismiss = ::dismiss, + ) + } + + private fun createChild( + route: YieldSupplyStartEarningRoute, + factoryContext: AppComponentContext, + ): ComposableModularContentComponent = when (route) { + YieldSupplyStartEarningRoute.FeePolicy -> getEmptyComposableModularContentComponent() // todo fee policy + YieldSupplyStartEarningRoute.Action -> YieldSupplyStartEarningComponent( + appComponentContext = factoryContext, + params = YieldSupplyStartEarningComponent.Params( + userWalletId = params.userWalletId, + cryptoCurrency = params.cryptoCurrency, + callback = model, + ), + ) + } + + private fun onChildBack() { + if (innerStack.value.backStack.isEmpty()) { + dismiss() + } else { + stackNavigation.pop() + } + } + + data class Params( + val userWalletId: UserWalletId, + val cryptoCurrency: CryptoCurrency, + val onDismiss: (Boolean) -> Unit, + ) +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/YieldSupplyStartEarningRoute.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/YieldSupplyStartEarningRoute.kt new file mode 100644 index 0000000000..9a8c832586 --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/YieldSupplyStartEarningRoute.kt @@ -0,0 +1,8 @@ +package com.tangem.features.yield.supply.impl.subcomponents.startearning + +import com.tangem.core.decompose.navigation.Route + +internal sealed class YieldSupplyStartEarningRoute : Route { + data object Action : YieldSupplyStartEarningRoute() + data object FeePolicy : YieldSupplyStartEarningRoute() +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/di/YieldSupplyStartEarningModule.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/di/YieldSupplyStartEarningModule.kt new file mode 100644 index 0000000000..a26ae6c3a0 --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/di/YieldSupplyStartEarningModule.kt @@ -0,0 +1,26 @@ +package com.tangem.features.yield.supply.impl.subcomponents.startearning.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.yield.supply.impl.subcomponents.startearning.model.YieldSupplyStartEarningEntryModel +import com.tangem.features.yield.supply.impl.subcomponents.startearning.model.YieldSupplyStartEarningModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(ModelComponent::class) +internal interface YieldSupplyStartEarningModule { + + @Binds + @IntoMap + @ClassKey(YieldSupplyStartEarningModel::class) + fun provideYieldSupplyStartEarningModel(impl: YieldSupplyStartEarningModel): Model + + @Binds + @IntoMap + @ClassKey(YieldSupplyStartEarningEntryModel::class) + fun provideYieldSupplyStartEarningEntryModel(impl: YieldSupplyStartEarningEntryModel): Model +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/entity/YieldSupplyStartEarningContentUM.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/entity/YieldSupplyStartEarningContentUM.kt new file mode 100644 index 0000000000..0354f3e6e1 --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/entity/YieldSupplyStartEarningContentUM.kt @@ -0,0 +1,20 @@ +package com.tangem.features.yield.supply.impl.subcomponents.startearning.entity + +import androidx.compose.runtime.Immutable +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.extensions.TextReference + +@Immutable +internal sealed class YieldSupplyStartEarningContentUM : TangemBottomSheetConfigContent { + + data class Main( + val currencyIconState: CurrencyIconState, + val fee: Fee?, + ) : YieldSupplyStartEarningContentUM() + + data class FeePolicy( + val title: TextReference, + ) : YieldSupplyStartEarningContentUM() +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningEntryModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningEntryModel.kt new file mode 100644 index 0000000000..c48e31cac8 --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningEntryModel.kt @@ -0,0 +1,33 @@ +package com.tangem.features.yield.supply.impl.subcomponents.startearning.model + +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.decompose.navigation.Router +import com.tangem.features.yield.supply.impl.subcomponents.startearning.YieldSupplyStartEarningComponent +import com.tangem.features.yield.supply.impl.subcomponents.startearning.YieldSupplyStartEarningEntryComponent +import com.tangem.features.yield.supply.impl.subcomponents.startearning.YieldSupplyStartEarningRoute +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import javax.inject.Inject + +@ModelScoped +internal class YieldSupplyStartEarningEntryModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + paramsContainer: ParamsContainer, +) : Model(), YieldSupplyStartEarningComponent.ModelCallback { + + private val params = paramsContainer.require() + + override fun onBackClick() { + router.pop() + } + + override fun onFeePolicyClick() { + router.push(YieldSupplyStartEarningRoute.FeePolicy) + } + + override fun onTransactionSent() { + params.onDismiss(true) + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..dd5fee8a5e --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt @@ -0,0 +1,222 @@ +package com.tangem.features.yield.supply.impl.subcomponents.startearning.model + +import arrow.core.getOrElse +import com.tangem.blockchain.common.TransactionSender +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 +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase +import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase +import com.tangem.domain.transaction.usecase.SendTransactionUseCase +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyEstimateEnterFeeUseCase +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.startearning.YieldSupplyStartEarningComponent +import com.tangem.utils.StringsSigns.DOT +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import timber.log.Timber +import java.math.BigDecimal +import javax.inject.Inject +import kotlin.properties.Delegates + +@Suppress("LongParameterList") +@ModelScoped +internal class YieldSupplyStartEarningModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + paramsContainer: ParamsContainer, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, + private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, + private val sendTransactionUseCase: SendTransactionUseCase, + private val yieldSupplyStartEarningUseCase: YieldSupplyStartEarningUseCase, + private val yieldSupplyEstimateEnterFeeUseCase: YieldSupplyEstimateEnterFeeUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, +) : Model() { + + private val params: YieldSupplyStartEarningComponent.Params = paramsContainer.require() + + private val cryptoCurrency = params.cryptoCurrency + private var userWallet: UserWallet by Delegates.notNull() + + private val cryptoCurrencyStatusFlow: StateFlow + field = MutableStateFlow( + CryptoCurrencyStatus( + currency = cryptoCurrency, + value = CryptoCurrencyStatus.Loading, + ), + ) + private val feeCryptoCurrencyStatusFlow: StateFlow + field = MutableStateFlow( + CryptoCurrencyStatus( + currency = cryptoCurrency, + value = CryptoCurrencyStatus.Loading, + ), + ) + + val uiState: StateFlow + field: MutableStateFlow = MutableStateFlow( + YieldSupplyActionUM( + title = resourceReference(R.string.yield_module_start_earning), + subtitle = resourceReference( + R.string.yield_module_start_earning_sheet_description, + wrappedList(cryptoCurrency.symbol), + ), + footer = combinedReference( + resourceReference(R.string.yield_module_start_earning_sheet_next_deposits), + resourceReference(R.string.yield_module_start_earning_sheet_fee_policy), + ), + currencyIconState = CryptoCurrencyToIconStateConverter().convert(params.cryptoCurrency), + yieldSupplyFeeUM = YieldSupplyFeeUM.Loading, + isPrimaryButtonEnabled = false, + ), + ) + + private val cryptoCurrencyStatus + get() = cryptoCurrencyStatusFlow.value + private var appCurrency = AppCurrency.Default + + init { + modelScope.launch { + appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default } + subscribeOnCurrencyStatusUpdates() + } + } + + private suspend fun onLoadFee() { + if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Loading) return + + val transactionListData = yieldSupplyStartEarningUseCase( + userWalletId = userWallet.walletId, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ).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 = cryptoCurrencyStatus.value.fiatRate?.let { rate -> + feeSum.multiply(rate) + } + + val fiat = fiatFeeValue.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), + ), + ), + ) + } + } + } + + fun onClick() { + val yieldSupplyFeeUM = uiState.value.yieldSupplyFeeUM as? YieldSupplyFeeUM.Content ?: return + + uiState.update { it.copy(isPrimaryButtonEnabled = false) } + modelScope.launch(dispatchers.default) { + sendTransactionUseCase.invoke( + txsData = yieldSupplyFeeUM.transactionDataList, + userWallet = userWallet, + network = cryptoCurrency.network, + sendMode = TransactionSender.MultipleTransactionSendMode.DEFAULT, + ).fold( + ifLeft = { + Timber.e(it.toString()) + uiState.update { it.copy(isPrimaryButtonEnabled = true) } + }, + ifRight = { + params.callback.onTransactionSent() + }, + ) + } + } + + private fun subscribeOnCurrencyStatusUpdates() { + modelScope.launch { + getUserWalletUseCase(params.userWalletId).fold( + ifRight = { wallet -> + userWallet = wallet + getCurrenciesStatusUpdates() + }, + ifLeft = { + Timber.w(it.toString()) + // showAlertError() todo yield supply error alert + return@launch + }, + ) + } + } + + private fun getCurrenciesStatusUpdates() { + getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( + userWalletId = params.userWalletId, + currencyId = cryptoCurrency.id, + isSingleWalletWithTokens = false, + ).onEach { maybeCryptoCurrency -> + maybeCryptoCurrency.fold( + ifRight = { cryptoCurrencyStatus -> + onDataLoaded( + currencyStatus = cryptoCurrencyStatus, + feeCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase( + userWalletId = params.userWalletId, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ).getOrNull() ?: cryptoCurrencyStatus, + ) + }, + ifLeft = { + // todo yield supply error + // sendConfirmAlertFactory.getGenericErrorState( + // onFailedTxEmailClick = { + // onFailedTxEmailClick(it.toString()) + // }, + // popBack = router::pop, + // ) + }, + ) + }.launchIn(modelScope) + } + + private fun onDataLoaded(currencyStatus: CryptoCurrencyStatus, feeCurrencyStatus: CryptoCurrencyStatus) { + cryptoCurrencyStatusFlow.update { currencyStatus } + feeCryptoCurrencyStatusFlow.update { feeCurrencyStatus } + + modelScope.launch { + onLoadFee() + } + } +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/ui/YieldSupplyStartEarningBottomSheet.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/ui/YieldSupplyStartEarningBottomSheet.kt new file mode 100644 index 0000000000..aba821a580 --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/ui/YieldSupplyStartEarningBottomSheet.kt @@ -0,0 +1,48 @@ +package com.tangem.features.yield.supply.impl.subcomponents.startearning.ui + +import androidx.compose.animation.AnimatedContent +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetWithFooter +import com.tangem.core.ui.decompose.ComposableModularContentComponent +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.yield.supply.impl.subcomponents.startearning.YieldSupplyStartEarningRoute + +@Composable +internal fun YieldSupplyStartEarningBottomSheet( + stackState: ChildStack, + onDismiss: () -> Unit, +) { + TangemModalBottomSheetWithFooter( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = onDismiss, + content = TangemBottomSheetConfigContent.Empty, + ), + containerColor = TangemTheme.colors.background.tertiary, + title = { + AnimatedContent( + stackState.active.instance, + ) { currentState -> + currentState.Title() + } + }, + footer = { + AnimatedContent( + stackState.active.instance, + ) { currentState -> + currentState.Footer() + } + }, + content = { + AnimatedContent( + stackState.active.instance, + ) { currentState -> + currentState.Content(modifier = Modifier) + } + }, + ) +} \ No newline at end of file diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 009240b10c..5f6869584a 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,8 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1229" +tangemBlockchainSdk = "develop-1230" +#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-561" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12"