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 6c84240db5..23e3efbe1d 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 @@ -45,7 +45,7 @@ class YieldSupplyEstimateEnterFeeUseCase( ).normal, ) } else { - transaction.copy(fee = withCalculatedFee.first().fee?.fixFee(cryptoCurrency)) + transaction.copy(fee = withCalculatedFee.firstOrNull()?.fee?.fixFee(cryptoCurrency)) } } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/YieldSupplyActiveComponent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/YieldSupplyActiveComponent.kt new file mode 100644 index 0000000000..2b3bd18f57 --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/YieldSupplyActiveComponent.kt @@ -0,0 +1,63 @@ +package com.tangem.features.yield.supply.impl.subcomponents.active + +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.SecondaryButton +import com.tangem.core.ui.decompose.ComposableModularContentComponent +import com.tangem.core.ui.extensions.stringResourceSafe +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 kotlinx.coroutines.flow.StateFlow + +internal class YieldSupplyActiveComponent( + appComponentContext: AppComponentContext, + private val params: Params, +) : ComposableModularContentComponent, AppComponentContext by appComponentContext { + + private val model: YieldSupplyActiveModel = getOrCreateModel(params = params) + + @Composable + override fun Title() { + YieldSupplyActiveTitle(onCloseClick = params.callback::onBackClick) + } + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + + YieldSupplyActiveContent(state = state, modifier = Modifier) + } + + @Composable + override fun Footer() { + SecondaryButton( + text = stringResourceSafe(R.string.yield_module_stop_earning), + onClick = params.callback::onStopEarning, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) + } + + data class Params( + val userWallet: UserWallet, + val cryptoCurrencyStatusFlow: StateFlow, + val callback: ModelCallback, + ) + + interface ModelCallback { + fun onBackClick() + fun onStopEarning() + } +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/YieldSupplyActiveEntryComponent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/YieldSupplyActiveEntryComponent.kt new file mode 100644 index 0000000000..258da13fe2 --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/YieldSupplyActiveEntryComponent.kt @@ -0,0 +1,103 @@ +package com.tangem.features.yield.supply.impl.subcomponents.active + +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.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.features.yield.supply.impl.subcomponents.active.model.YieldSupplyActiveEntryModel +import com.tangem.features.yield.supply.impl.subcomponents.active.model.YieldSupplyActiveRoute +import com.tangem.features.yield.supply.impl.subcomponents.active.ui.YieldSupplyActiveEntryBottomSheet +import com.tangem.features.yield.supply.impl.subcomponents.stopearning.YieldSupplyStopEarningComponent +import kotlinx.coroutines.flow.StateFlow + +internal class YieldSupplyActiveEntryComponent( + 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: YieldSupplyActiveEntryModel = getOrCreateModel(params = params, router = innerRouter) + + private val innerStack = childStack( + key = "yieldSupplyActiveStack", + source = stackNavigation, + serializer = null, + initialConfiguration = YieldSupplyActiveRoute.Info, + handleBackButton = true, + childFactory = { configuration, factoryContext -> + createChild( + configuration, + childByContext( + componentContext = factoryContext, + router = innerRouter, + ), + ) + }, + ) + + override fun dismiss() { + params.onDismiss() + } + + @Composable + override fun BottomSheet() { + val stackState by innerStack.subscribeAsState() + + YieldSupplyActiveEntryBottomSheet( + stackState = stackState, + onDismiss = ::dismiss, + ) + } + + private fun createChild( + route: YieldSupplyActiveRoute, + factoryContext: AppComponentContext, + ): ComposableModularContentComponent = when (route) { + YieldSupplyActiveRoute.Info -> YieldSupplyActiveComponent( + appComponentContext = factoryContext, + params = YieldSupplyActiveComponent.Params( + userWallet = params.userWallet, + cryptoCurrencyStatusFlow = params.cryptoCurrencyStatusFlow, + callback = model, + ), + ) + YieldSupplyActiveRoute.Action -> YieldSupplyStopEarningComponent( + appComponentContext = factoryContext, + params = YieldSupplyStopEarningComponent.Params( + userWallet = params.userWallet, + cryptoCurrencyStatusFlow = params.cryptoCurrencyStatusFlow, + callback = model, + ), + ) + } + + private fun onChildBack() { + if (innerStack.value.backStack.isEmpty()) { + dismiss() + } else { + stackNavigation.pop() + } + } + + data class Params( + val userWallet: UserWallet, + val cryptoCurrencyStatusFlow: StateFlow, + val onDismiss: () -> Unit, + ) +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/di/YieldSupplyActiveModule.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/di/YieldSupplyActiveModule.kt new file mode 100644 index 0000000000..4e062d0d10 --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/di/YieldSupplyActiveModule.kt @@ -0,0 +1,26 @@ +package com.tangem.features.yield.supply.impl.subcomponents.active.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.yield.supply.impl.subcomponents.active.model.YieldSupplyActiveEntryModel +import com.tangem.features.yield.supply.impl.subcomponents.active.model.YieldSupplyActiveModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(ModelComponent::class) +internal interface YieldSupplyActiveModule { + + @Binds + @IntoMap + @ClassKey(YieldSupplyActiveModel::class) + fun provideYieldSupplyActiveModel(impl: YieldSupplyActiveModel): Model + + @Binds + @IntoMap + @ClassKey(YieldSupplyActiveEntryModel::class) + fun provideYieldSupplyActiveEntryModel(impl: YieldSupplyActiveEntryModel): Model +} \ No newline at end of file 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 new file mode 100644 index 0000000000..fe2157d2ef --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/entity/YieldSupplyActiveContentUM.kt @@ -0,0 +1,10 @@ +package com.tangem.features.yield.supply.impl.subcomponents.active.entity + +import com.tangem.core.ui.extensions.TextReference + +internal data class YieldSupplyActiveContentUM( + val totalEarnings: TextReference, + val availableBalance: TextReference, + val providerTitle: 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/subcomponents/active/model/YieldSupplyActiveEntryModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/model/YieldSupplyActiveEntryModel.kt new file mode 100644 index 0000000000..57878b486a --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/model/YieldSupplyActiveEntryModel.kt @@ -0,0 +1,33 @@ +package com.tangem.features.yield.supply.impl.subcomponents.active.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.active.YieldSupplyActiveComponent +import com.tangem.features.yield.supply.impl.subcomponents.active.YieldSupplyActiveEntryComponent +import com.tangem.features.yield.supply.impl.subcomponents.stopearning.YieldSupplyStopEarningComponent +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import javax.inject.Inject + +@ModelScoped +internal class YieldSupplyActiveEntryModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + paramsContainer: ParamsContainer, + private val router: Router, +) : Model(), YieldSupplyActiveComponent.ModelCallback, YieldSupplyStopEarningComponent.ModelCallback { + + private val params = paramsContainer.require() + + override fun onStopEarning() { + router.push(YieldSupplyActiveRoute.Action) + } + + override fun onBackClick() { + router.pop() + } + + override fun onTransactionSent() { + params.onDismiss() + } +} \ 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 new file mode 100644 index 0000000000..698ba4b032 --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/model/YieldSupplyActiveModel.kt @@ -0,0 +1,50 @@ +package com.tangem.features.yield.supply.impl.subcomponents.active.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.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.format +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.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@ModelScoped +internal class YieldSupplyActiveModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val params: YieldSupplyActiveComponent.Params = paramsContainer.require() + + private val cryptoCurrencyStatusFlow = params.cryptoCurrencyStatusFlow + private val cryptoCurrency = cryptoCurrencyStatusFlow.value.currency + + val uiState: StateFlow + field = MutableStateFlow( + YieldSupplyActiveContentUM( + totalEarnings = stringReference("0"), + availableBalance = stringReference( + cryptoCurrencyStatusFlow.value.value.amount.format { + crypto(cryptoCurrency = cryptoCurrencyStatusFlow.value.currency) + }, + ), + providerTitle = resourceReference(R.string.yield_module_provider), + subtitle = combinedReference( + resourceReference( + id = R.string.yield_module_earn_sheet_provider_description, + formatArgs = wrappedList(cryptoCurrency.symbol, cryptoCurrency.symbol), + ), + resourceReference(R.string.common_read_more), + ), + ), + ) +} \ 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/YieldSupplyActiveRoute.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/model/YieldSupplyActiveRoute.kt new file mode 100644 index 0000000000..a143cc777f --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/model/YieldSupplyActiveRoute.kt @@ -0,0 +1,8 @@ +package com.tangem.features.yield.supply.impl.subcomponents.active.model + +import com.tangem.core.decompose.navigation.Route + +internal sealed class YieldSupplyActiveRoute : Route { + data object Info : YieldSupplyActiveRoute() + data object Action : YieldSupplyActiveRoute() +} \ No newline at end of file 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 new file mode 100644 index 0000000000..d0bb88752b --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/ui/YieldSupplyActiveContent.kt @@ -0,0 +1,164 @@ +package com.tangem.features.yield.supply.impl.subcomponents.active.ui + +import android.content.res.Configuration +import androidx.compose.foundation.Image +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.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.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +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.ResizableText +import com.tangem.core.ui.components.SpacerH4 +import com.tangem.core.ui.components.SpacerH8 +import com.tangem.core.ui.components.SpacerWMax +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.subcomponents.active.entity.YieldSupplyActiveContentUM + +@Composable +internal fun YieldSupplyActiveContent(state: YieldSupplyActiveContentUM, modifier: Modifier = Modifier) { + Column( + verticalArrangement = Arrangement.spacedBy(14.dp), + modifier = modifier.padding( + vertical = 8.dp, + horizontal = 16.dp, + ), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .background(TangemTheme.colors.background.action) + .fillMaxWidth() + .padding(12.dp), + ) { + Text( + text = stringResourceSafe(R.string.yield_module_earn_sheet_total_earnings_title), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + ResizableText( + text = state.totalEarnings.resolveReference(), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + ) + } + YieldSupplyActiveMyFunds(state = state) + } +} + +@Composable +private fun YieldSupplyActiveMyFunds(state: YieldSupplyActiveContentUM) { + Column( + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .background(TangemTheme.colors.background.action) + .fillMaxWidth() + .padding(12.dp), + ) { + Text( + text = stringResourceSafe(R.string.yield_module_earn_sheet_my_funds_title), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + SpacerH4() + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Image( + imageVector = ImageVector.vectorResource(R.drawable.ic_aave_36), + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + Text( + text = state.providerTitle.resolveReference(), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + ) + } + SpacerH8() + Text( + text = state.subtitle.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + SpacerH8() + HorizontalDivider( + thickness = 0.5.dp, + color = TangemTheme.colors.stroke.primary, + ) + InfoRow( + title = resourceReference(R.string.yield_module_earn_sheet_transfers_title), + info = resourceReference(R.string.yield_module_transfer_mode_automatic), + ) + HorizontalDivider( + thickness = 0.5.dp, + color = TangemTheme.colors.stroke.primary, + ) + InfoRow( + title = resourceReference(R.string.yield_module_earn_sheet_available_title), + info = state.availableBalance, + ) + } +} + +@Composable +private fun InfoRow(title: TextReference, info: TextReference) { + 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() + Text( + text = info.resolveReference(), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.tertiary, + ) + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun YieldSupplyActiveBottomSheet_Preview( + @PreviewParameter(YieldSupplyActiveBottomSheetPreviewProvider::class) params: YieldSupplyActiveContentUM, +) { + TangemThemePreview { + YieldSupplyActiveContent(params) + } +} + +private class YieldSupplyActiveBottomSheetPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + YieldSupplyActiveContentUM( + totalEarnings = stringReference("0.006994219 USDT"), + availableBalance = stringReference("3,210.006994 aUSDT"), + providerTitle = stringReference("Aave"), + subtitle = resourceReference( + R.string.yield_module_earn_sheet_provider_description, + wrappedList("USDT", "USDT"), + ), + ), + ) +} +// endregion \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/ui/YieldSupplyActiveEntryBottomSheet.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/ui/YieldSupplyActiveEntryBottomSheet.kt new file mode 100644 index 0000000000..124b736fa4 --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/ui/YieldSupplyActiveEntryBottomSheet.kt @@ -0,0 +1,48 @@ +package com.tangem.features.yield.supply.impl.subcomponents.active.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.active.model.YieldSupplyActiveRoute + +@Composable +internal fun YieldSupplyActiveEntryBottomSheet( + stackState: ChildStack, + onDismiss: () -> Unit, +) { + TangemModalBottomSheetWithFooter( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = onDismiss, + content = TangemBottomSheetConfigContent.Empty, + ), + containerColor = TangemTheme.colors.background.tertiary, + title = { state -> + AnimatedContent( + stackState.active.instance, + ) { currentState -> + currentState.Title() + } + }, + footer = { state -> + AnimatedContent( + stackState.active.instance, + ) { currentState -> + currentState.Footer() + } + }, + content = { state -> + AnimatedContent( + stackState.active.instance, + ) { currentState -> + currentState.Content(modifier = Modifier) + } + }, + ) +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/ui/YieldSupplyActiveTitle.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/ui/YieldSupplyActiveTitle.kt new file mode 100644 index 0000000000..300e051ba1 --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/ui/YieldSupplyActiveTitle.kt @@ -0,0 +1,51 @@ +package com.tangem.features.yield.supply.impl.subcomponents.active.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.buttons.small.TangemIconButton +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.yield.supply.impl.R + +@Composable +internal fun YieldSupplyActiveTitle(onCloseClick: () -> Unit) { + Box(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.align(Alignment.Center)) { + Text( + text = stringResourceSafe(R.string.yield_module_earn_sheet_title), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .size(8.dp) + .background(TangemTheme.colors.icon.accent, CircleShape), + ) + Text( + text = stringResourceSafe(R.string.yield_module_status_active), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier, + ) + } + } + TangemIconButton( + iconRes = R.drawable.ic_close_24, + onClick = onCloseClick, + modifier = Modifier + .padding(16.dp) + .align(Alignment.CenterEnd), + ) + } +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/YieldSupplyStopEarningComponent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/YieldSupplyStopEarningComponent.kt new file mode 100644 index 0000000000..86ae2929ca --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/YieldSupplyStopEarningComponent.kt @@ -0,0 +1,96 @@ +package com.tangem.features.yield.supply.impl.subcomponents.stopearning + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +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.decompose.ComposableModularContentComponent +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +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.stopearning.model.YieldSupplyStopEarningModel +import kotlinx.coroutines.flow.StateFlow + +internal class YieldSupplyStopEarningComponent( + private val appComponentContext: AppComponentContext, + private val params: Params, +) : ComposableModularContentComponent, AppComponentContext by appComponentContext { + + private val model: YieldSupplyStopEarningModel = getOrCreateModel(params = params) + + @Composable + override fun Title() { + TangemModalBottomSheetTitle( + startIconRes = R.drawable.ic_back_24, + onStartClick = 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 + .size(56.dp) + .background(TangemTheme.colors.icon.attention.copy(0.1f), CircleShape), + ) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_alert_triangle_20), + contentDescription = null, + tint = TangemTheme.colors.icon.attention, + modifier = Modifier + .padding(12.dp) + .size(32.dp), + ) + } + } + } + + @Composable + override fun Footer() { + val state by model.uiState.collectAsStateWithLifecycle() + + PrimaryButtonIconEnd( + text = stringResourceSafe(R.string.common_confirm), + onClick = model::onClick, + iconResId = R.drawable.ic_tangem_24, + enabled = state.isPrimaryButtonEnabled, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) + } + + data class Params( + val userWallet: UserWallet, + val cryptoCurrencyStatusFlow: StateFlow, + val callback: ModelCallback, + ) + + interface ModelCallback { + fun onBackClick() + 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/stopearning/di/YieldSupplyStopEarningModule.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/di/YieldSupplyStopEarningModule.kt new file mode 100644 index 0000000000..2b8e83f294 --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/di/YieldSupplyStopEarningModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.yield.supply.impl.subcomponents.stopearning.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.yield.supply.impl.subcomponents.stopearning.model.YieldSupplyStopEarningModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(ModelComponent::class) +internal interface YieldSupplyStopEarningModule { + + @Binds + @IntoMap + @ClassKey(YieldSupplyStopEarningModel::class) + fun provideYieldSupplyStopEarningModel(impl: YieldSupplyStopEarningModel): Model +} \ No newline at end of file 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 new file mode 100644 index 0000000000..8aa0b80853 --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt @@ -0,0 +1,160 @@ +package com.tangem.features.yield.supply.impl.subcomponents.stopearning.model + +import arrow.core.getOrElse +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.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase +import com.tangem.domain.transaction.usecase.GetFeeUseCase +import com.tangem.domain.transaction.usecase.SendTransactionUseCase +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.stopearning.YieldSupplyStopEarningComponent +import com.tangem.utils.StringsSigns.DOT +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 kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@Suppress("LongParameterList") +@ModelScoped +internal class YieldSupplyStopEarningModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + paramsContainer: ParamsContainer, + private val getFeeUseCase: GetFeeUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, + private val sendTransactionUseCase: SendTransactionUseCase, + private val yieldSupplyStopEarningUseCase: YieldSupplyStopEarningUseCase, +) : Model() { + + private val params: YieldSupplyStopEarningComponent.Params = paramsContainer.require() + + private val cryptoCurrencyStatus + get() = params.cryptoCurrencyStatusFlow.value + private val cryptoCurrency = cryptoCurrencyStatus.currency + private var userWallet = params.userWallet + + private val feeCryptoCurrencyStatusFlow: StateFlow + field = MutableStateFlow( + CryptoCurrencyStatus( + cryptoCurrencyStatus.currency, + value = CryptoCurrencyStatus.Loading, + ), + ) + + private var appCurrency = AppCurrency.Default + + val uiState: StateFlow + field: MutableStateFlow = MutableStateFlow( + YieldSupplyActionUM( + title = resourceReference(R.string.yield_module_stop_earning), + subtitle = resourceReference( + id = R.string.yield_module_stop_earning_sheet_description, + formatArgs = wrappedList(cryptoCurrency.symbol), + ), + footer = combinedReference( + resourceReference(R.string.yield_module_stop_earning_sheet_fee_note), + resourceReference(R.string.common_read_more), + ), + currencyIconState = CryptoCurrencyToIconStateConverter().convert(cryptoCurrency), + yieldSupplyFeeUM = YieldSupplyFeeUM.Loading, + isPrimaryButtonEnabled = false, + ), + ) + + init { + modelScope.launch { + appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default } + subscribeOnCurrencyStatusUpdates() + } + } + + fun onClick() { + val yieldSupplyFeeUM = uiState.value.yieldSupplyFeeUM as? YieldSupplyFeeUM.Content ?: return + uiState.update { it.copy(isPrimaryButtonEnabled = false) } + + modelScope.launch(dispatchers.default) { + sendTransactionUseCase( + txData = yieldSupplyFeeUM.transactionDataList.first(), + userWallet = userWallet, + network = cryptoCurrency.network, + ).fold( + ifLeft = { + Timber.e(it.toString()) + }, + ifRight = { + params.callback.onTransactionSent() + }, + ) + } + } + + private fun subscribeOnCurrencyStatusUpdates() { + modelScope.launch { + feeCryptoCurrencyStatusFlow.update { + getFeePaidCryptoCurrencyStatusSyncUseCase( + userWalletId = userWallet.walletId, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ).getOrNull() ?: cryptoCurrencyStatus + } + onLoadFee() + } + } + + private suspend fun onLoadFee() { + val exitTransitionData = yieldSupplyStopEarningUseCase( + userWalletId = userWallet.walletId, + cryptoCurrencyStatus = cryptoCurrencyStatus, + fee = null, + ).getOrNull() ?: return + + val fee = getFeeUseCase( + transactionData = exitTransitionData, + userWallet = userWallet, + network = cryptoCurrency.network, + ).getOrNull() ?: return + + val crypto = fee.normal.amount.value.format { crypto(feeCryptoCurrencyStatusFlow.value.currency) } + val fiatFeeValue = cryptoCurrencyStatus.value.fiatRate?.let { rate -> + fee.normal.amount.value?.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, + yieldSupplyFeeUM = YieldSupplyFeeUM.Content( + transactionDataList = persistentListOf(exitTransitionData.copy(fee = fee.normal)), + feeValue = combinedReference( + stringReference(crypto), + stringReference(" $DOT "), + stringReference(fiat), + ), + ), + ) + } + } + } +} \ No newline at end of file