From dcbe3fefb34b3cba0690d1d52681cf306baade70 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Sep 2025 13:01:11 +0500 Subject: [PATCH] Updated on 2026-08-14 --- features/tokendetails/impl/build.gradle.kts | 1 + .../DefaultTokenDetailsComponent.kt | 13 +- .../tokendetails/TokenDetailsPreviewData.kt | 2 + .../tokendetails/model/TokenDetailsModel.kt | 3 + .../tokendetails/state/TokenDetailsState.kt | 1 + .../TokenDetailsSkeletonStateConverter.kt | 3 + .../state/factory/TokenDetailsStateFactory.kt | 3 + .../tokendetails/ui/TokenDetailsScreen.kt | 13 + .../yield/supply/api/YieldSupplyComponent.kt | 16 ++ .../impl/main/DefaultYieldSupplyComponent.kt | 65 +++++ .../impl/main/di/YieldSupplyBindsModule.kt | 32 +++ .../supply/impl/main/entity/YieldSupplyUM.kt | 24 ++ .../main/model/YieldSupplyClickIntents.kt | 6 + .../impl/main/model/YieldSupplyModel.kt | 124 +++++++++ .../impl/main/ui/YieldSupplyBlockContent.kt | 254 ++++++++++++++++++ .../promo/DefaultYieldSupplyPromoComponent.kt | 29 +- .../model/YieldSupplyStartEarningModel.kt | 4 +- 17 files changed, 588 insertions(+), 5 deletions(-) create mode 100644 features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/YieldSupplyComponent.kt create mode 100644 features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/DefaultYieldSupplyComponent.kt create mode 100644 features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/di/YieldSupplyBindsModule.kt create mode 100644 features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/entity/YieldSupplyUM.kt create mode 100644 features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyClickIntents.kt create mode 100644 features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt create mode 100644 features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/ui/YieldSupplyBlockContent.kt diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 5da95d55a3..3b363faddf 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -102,6 +102,7 @@ dependencies { implementation(projects.features.txhistory.api) implementation(projects.features.sendV2.api) implementation(projects.features.tokenRecieve.api) + implementation(projects.features.yieldSupply.api) implementation(deps.decompose.ext.compose) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt index 2291d82965..6c5befecc6 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt @@ -5,8 +5,8 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.decompose.ComponentContext -import com.arkivanov.decompose.router.slot.childSlot import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot import com.arkivanov.decompose.router.slot.dismiss import com.arkivanov.essenty.lifecycle.subscribe import com.tangem.core.decompose.context.AppComponentContext @@ -23,6 +23,7 @@ import com.tangem.features.markets.token.block.TokenMarketBlockComponent import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.tokenreceive.TokenReceiveComponent import com.tangem.features.txhistory.component.TxHistoryComponent +import com.tangem.features.yield.supply.api.YieldSupplyComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -34,6 +35,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor( tokenMarketBlockComponentFactory: TokenMarketBlockComponent.Factory, txHistoryComponentFactory: TxHistoryComponent.Factory, private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, + yieldSupplyComponentFactory: YieldSupplyComponent.Factory, ) : TokenDetailsComponent, AppComponentContext by appComponentContext { private val model: TokenDetailsModel = getOrCreateModel(params) @@ -67,6 +69,14 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor( ) } + private val yieldSupplyComponent = yieldSupplyComponentFactory.create( + context = child("tokenYieldSupplyComponent"), + params = YieldSupplyComponent.Params( + userWalletId = params.userWalletId, + cryptoCurrency = params.currency, + ), + ) + @Composable override fun Content(modifier: Modifier) { val state by model.uiState.collectAsStateWithLifecycle() @@ -76,6 +86,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor( state = state, tokenMarketBlockComponent = tokenMarketBlockComponent, txHistoryComponent = txHistoryComponent, + yieldSupplyComponent = yieldSupplyComponent, ) bottomSheet.child?.instance?.BottomSheet() } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index 01040f4abc..66afedfdbf 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -178,6 +178,7 @@ internal object TokenDetailsPreviewData { bottomSheetConfig = null, isBalanceHidden = false, isMarketPriceAvailable = false, + isYieldSupplyFeatureEnabled = false, ) val tokenDetailsState_2 = TokenDetailsState( @@ -203,6 +204,7 @@ internal object TokenDetailsPreviewData { bottomSheetConfig = null, isBalanceHidden = false, isMarketPriceAvailable = true, + isYieldSupplyFeatureEnabled = true, ) val tokenDetailsState_3 = tokenDetailsState_2.copy(stakingBlocksState = stakingBalanceBlock) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index 3e626329c4..12d907cfef 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -81,6 +81,7 @@ import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.tokendetails.impl.R import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter +import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import com.tangem.utils.extensions.isZero @@ -137,6 +138,7 @@ internal class TokenDetailsModel @Inject constructor( private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle, private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase, private val getEnsNameUseCase: GetEnsNameUseCase, + private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, ) : Model(), TokenDetailsClickIntents { private val params = paramsContainer.require() @@ -170,6 +172,7 @@ internal class TokenDetailsModel @Inject constructor( networkHasDerivationUseCase = networkHasDerivationUseCase, getUserWalletUseCase = getUserWalletUseCase, userWalletId = userWalletId, + yieldSupplyFeatureToggles = yieldSupplyFeatureToggles, ) private val expressStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index d3a16f09cd..40805e48b9 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -23,4 +23,5 @@ internal data class TokenDetailsState( val bottomSheetConfig: TangemBottomSheetConfig?, val isBalanceHidden: Boolean, val isMarketPriceAvailable: Boolean, + val isYieldSupplyFeatureEnabled: Boolean, ) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index ee0b33124a..faca9bd311 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -18,6 +18,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDeta import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton import com.tangem.features.tokendetails.impl.R +import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles import com.tangem.lib.crypto.BlockchainUtils.isBitcoin import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList @@ -29,6 +30,7 @@ internal class TokenDetailsSkeletonStateConverter( private val networkHasDerivationUseCase: NetworkHasDerivationUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val userWalletId: UserWalletId, + private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, ) : Converter { private val iconStateConverter by lazy { TokenDetailsIconStateConverter() } @@ -69,6 +71,7 @@ internal class TokenDetailsSkeletonStateConverter( bottomSheetConfig = null, isBalanceHidden = true, isMarketPriceAvailable = value.id.rawCurrencyId != null, + isYieldSupplyFeatureEnabled = yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 77eb685821..0ff211dbf7 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -31,6 +31,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDeta import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig import com.tangem.features.tokendetails.impl.R +import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles import com.tangem.utils.Provider import kotlinx.collections.immutable.toImmutableList @@ -43,6 +44,7 @@ internal class TokenDetailsStateFactory( private val networkHasDerivationUseCase: NetworkHasDerivationUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val userWalletId: UserWalletId, + private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, ) { private val skeletonStateConverter by lazy { @@ -51,6 +53,7 @@ internal class TokenDetailsStateFactory( networkHasDerivationUseCase = networkHasDerivationUseCase, getUserWalletUseCase = getUserWalletUseCase, userWalletId = userWalletId, + yieldSupplyFeatureToggles = yieldSupplyFeatureToggles, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index 14a17ee2e1..f3466d9025 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -40,6 +40,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.s import com.tangem.features.markets.token.block.TokenMarketBlockComponent import com.tangem.features.txhistory.component.TxHistoryComponent import com.tangem.features.txhistory.entity.TxHistoryUM +import com.tangem.features.yield.supply.api.YieldSupplyComponent import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -50,6 +51,7 @@ internal fun TokenDetailsScreen( state: TokenDetailsState, tokenMarketBlockComponent: TokenMarketBlockComponent?, txHistoryComponent: TxHistoryComponent, + yieldSupplyComponent: YieldSupplyComponent, ) { val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } @@ -145,6 +147,12 @@ internal fun TokenDetailsScreen( ) } + if (state.isYieldSupplyFeatureEnabled) { + item { + yieldSupplyComponent.Content(modifier = itemModifier) + } + } + expressTransactionsItems( expressTxs = state.expressTxsToDisplay, modifier = itemModifier, @@ -190,6 +198,11 @@ private fun TokenDetailsScreenPreview( override fun LazyListScope.txHistoryContent(listState: LazyListState, state: TxHistoryUM) = Unit }, + yieldSupplyComponent = object : YieldSupplyComponent { + @Composable + override fun Content(modifier: Modifier) { + } + }, ) } } diff --git a/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/YieldSupplyComponent.kt b/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/YieldSupplyComponent.kt new file mode 100644 index 0000000000..af868285e3 --- /dev/null +++ b/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/YieldSupplyComponent.kt @@ -0,0 +1,16 @@ +package com.tangem.features.yield.supply.api + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId + +interface YieldSupplyComponent : ComposableContentComponent { + + data class Params( + val userWalletId: UserWalletId, + val cryptoCurrency: CryptoCurrency, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/DefaultYieldSupplyComponent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/DefaultYieldSupplyComponent.kt new file mode 100644 index 0000000000..1943c230d1 --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/DefaultYieldSupplyComponent.kt @@ -0,0 +1,65 @@ +package com.tangem.features.yield.supply.impl.main + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss +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.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.yield.supply.api.YieldSupplyComponent +import com.tangem.features.yield.supply.impl.main.model.YieldSupplyModel +import com.tangem.features.yield.supply.impl.main.ui.YieldSupplyBlockContent +import com.tangem.features.yield.supply.impl.subcomponents.active.YieldSupplyActiveEntryComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultYieldSupplyComponent @AssistedInject constructor( + @Assisted private val appComponentContext: AppComponentContext, + @Assisted private val params: YieldSupplyComponent.Params, +) : YieldSupplyComponent, AppComponentContext by appComponentContext { + + private val model: YieldSupplyModel = getOrCreateModel(params = params) + + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = null, + handleBackButton = false, + childFactory = { _, context -> bottomSheetChild(context) }, + ) + + @Composable + override fun Content(modifier: Modifier) { + val yieldSupplyUM by model.uiState.collectAsStateWithLifecycle() + val bottomSheet by bottomSheetSlot.subscribeAsState() + + YieldSupplyBlockContent(yieldSupplyUM = yieldSupplyUM, modifier = modifier) + + bottomSheet.child?.instance?.BottomSheet() + } + + private fun bottomSheetChild(componentContext: ComponentContext): ComposableBottomSheetComponent { + return YieldSupplyActiveEntryComponent( + appComponentContext = childByContext(componentContext), + params = YieldSupplyActiveEntryComponent.Params( + userWallet = model.userWallet, + cryptoCurrencyStatusFlow = model.cryptoCurrencyStatusFlow, + onDismiss = model.bottomSheetNavigation::dismiss, + ), + ) + } + + @AssistedFactory + interface Factory : YieldSupplyComponent.Factory { + override fun create( + context: AppComponentContext, + params: YieldSupplyComponent.Params, + ): DefaultYieldSupplyComponent + } +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/di/YieldSupplyBindsModule.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/di/YieldSupplyBindsModule.kt new file mode 100644 index 0000000000..8bee792210 --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/di/YieldSupplyBindsModule.kt @@ -0,0 +1,32 @@ +package com.tangem.features.yield.supply.impl.main.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.yield.supply.api.YieldSupplyComponent +import com.tangem.features.yield.supply.impl.main.DefaultYieldSupplyComponent +import com.tangem.features.yield.supply.impl.main.model.YieldSupplyModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface YieldSupplyBindsModule { + @Binds + @Singleton + fun provideYieldSupplyComponentFactory(impl: DefaultYieldSupplyComponent.Factory): YieldSupplyComponent.Factory +} + +@Module +@InstallIn(ModelComponent::class) +internal interface YieldSupplyModelModule { + + @Binds + @IntoMap + @ClassKey(YieldSupplyModel::class) + fun provideYieldSupplyModel(impl: YieldSupplyModel): Model +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/entity/YieldSupplyUM.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/entity/YieldSupplyUM.kt new file mode 100644 index 0000000000..1ea3ceddd4 --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/entity/YieldSupplyUM.kt @@ -0,0 +1,24 @@ +package com.tangem.features.yield.supply.impl.main.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +@Immutable +internal sealed class YieldSupplyUM { + + data class Initial( + val title: TextReference, + val onClick: () -> Unit, + ) : YieldSupplyUM() + + data object Loading : YieldSupplyUM() + + data class Content( + val rewardsBalance: TextReference, + val rewardsApy: TextReference, + val onClick: () -> Unit, + val isAllowedToSpend: Boolean, + ) : YieldSupplyUM() + + data object Processing : YieldSupplyUM() +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyClickIntents.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyClickIntents.kt new file mode 100644 index 0000000000..a13e1b98fe --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyClickIntents.kt @@ -0,0 +1,6 @@ +package com.tangem.features.yield.supply.impl.main.model + +interface YieldSupplyClickIntents { + fun onStartEarningClick() + fun onActiveClick() +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt new file mode 100644 index 0000000000..327f65e671 --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt @@ -0,0 +1,124 @@ +package com.tangem.features.yield.supply.impl.main.model + +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.tangem.common.routing.AppRoute.YieldSupplyPromo +import com.tangem.common.routing.AppRouter +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.features.yield.supply.api.YieldSupplyComponent +import com.tangem.features.yield.supply.impl.R +import com.tangem.features.yield.supply.impl.main.entity.YieldSupplyUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject +import kotlin.properties.Delegates + +@ModelScoped +internal class YieldSupplyModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val appRouter: AppRouter, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, +) : Model(), YieldSupplyClickIntents { + + private val params = paramsContainer.require() + + val uiState: StateFlow + field = MutableStateFlow(YieldSupplyUM.Loading) + + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + + private val cryptoCurrency = params.cryptoCurrency + var userWallet: UserWallet by Delegates.notNull() + + val cryptoCurrencyStatusFlow: StateFlow + field = MutableStateFlow( + CryptoCurrencyStatus( + currency = params.cryptoCurrency, + value = CryptoCurrencyStatus.Loading, + ), + ) + + init { + subscribeOnCurrencyStatusUpdates() + } + + private fun subscribeOnCurrencyStatusUpdates() { + modelScope.launch { + getUserWalletUseCase(params.userWalletId).fold( + ifRight = { wallet -> + userWallet = wallet + + getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( + userWalletId = params.userWalletId, + currencyId = cryptoCurrency.id, + isSingleWalletWithTokens = false, + ).onEach { maybeCryptoCurrency -> + maybeCryptoCurrency.fold( + ifRight = { cryptoCurrencyStatus -> + cryptoCurrencyStatusFlow.update { cryptoCurrencyStatus } + onDataLoaded(cryptoCurrencyStatus) + }, + ifLeft = { + // todo error + }, + ) + }.launchIn(modelScope) + }, + ifLeft = { + Timber.w(it.toString()) + return@launch + }, + ) + } + } + + override fun onStartEarningClick() { + appRouter.push( + YieldSupplyPromo( + userWalletId = params.userWalletId, + cryptoCurrency = params.cryptoCurrency, + ), + ) + } + + override fun onActiveClick() { + bottomSheetNavigation.activate(Unit) + } + + private fun onDataLoaded(cryptoCurrencyStatus: CryptoCurrencyStatus) { + val yieldSupplyStatus = cryptoCurrencyStatus.value.yieldSupplyStatus + + // todo yield supply add processing state + val yieldSupplyUM = when { + yieldSupplyStatus?.isActive == true -> + YieldSupplyUM.Content( + rewardsBalance = TextReference.EMPTY, + rewardsApy = TextReference.EMPTY, + onClick = ::onActiveClick, + isAllowedToSpend = yieldSupplyStatus.isAllowedToSpend, + ) + else -> YieldSupplyUM.Initial( + title = resourceReference( + id = R.string.yield_module_token_details_earn_notification_title, + formatArgs = wrappedList("5.1"), + ), + onClick = ::onStartEarningClick, + ) + } + + uiState.update { yieldSupplyUM } + } +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/ui/YieldSupplyBlockContent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/ui/YieldSupplyBlockContent.kt new file mode 100644 index 0000000000..7930a4eb89 --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/ui/YieldSupplyBlockContent.kt @@ -0,0 +1,254 @@ +package com.tangem.features.yield.supply.impl.main.ui + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +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.SecondaryButton +import com.tangem.core.ui.components.SpacerW8 +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.buttons.common.TangemButtonSize +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.main.entity.YieldSupplyUM +import com.tangem.utils.StringsSigns + +@Composable +internal fun YieldSupplyBlockContent(yieldSupplyUM: YieldSupplyUM, modifier: Modifier = Modifier) { + AnimatedContent( + targetState = yieldSupplyUM, + modifier = modifier, + ) { supplyUM -> + when (supplyUM) { + is YieldSupplyUM.Initial -> SupplyInitial(supplyUM) + YieldSupplyUM.Loading -> SupplyLoading() + is YieldSupplyUM.Content -> SupplyContent(supplyUM) + YieldSupplyUM.Processing -> SupplyProcessing() + } + } +} + +@Composable +private fun SupplyInitial(supplyUM: YieldSupplyUM.Initial) { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .background(TangemTheme.colors.background.primary) + .padding(12.dp), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_analytics_up_24), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + modifier = Modifier + .background(TangemTheme.colors.icon.accent.copy(alpha = 0.1f), CircleShape) + .padding(6.dp) + .size(24.dp), + ) + Column( + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = supplyUM.title.resolveReference(), + style = TangemTheme.typography.button, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = stringResourceSafe(R.string.yield_module_token_details_earn_notification_description), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + } + SecondaryButton( + text = stringResourceSafe(R.string.common_learn_more), + onClick = supplyUM.onClick, + size = TangemButtonSize.WideAction, + modifier = Modifier.fillMaxWidth(), + ) + } +} + +@Composable +private fun SupplyContent(supplyUM: YieldSupplyUM.Content) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .background(TangemTheme.colors.background.primary) + .clickable(onClick = supplyUM.onClick) + .padding(12.dp), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.weight(1f), + ) { + Text( + text = stringResourceSafe( + R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title, + ), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = supplyUM.rewardsBalance.resolveReference(), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = StringsSigns.DOT, + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.tertiary, + ) + Text( + text = supplyUM.rewardsApy.resolveReference(), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.tertiary, + ) + } + } + SpacerW8() + AnimatedVisibility(supplyUM.isAllowedToSpend.not()) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_alert_triangle_20), + contentDescription = null, + tint = TangemTheme.colors.icon.attention, + ) + } + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_chevron_right_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier.size(20.dp), + ) + } +} + +@Composable +private fun SupplyProcessing() { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(TangemTheme.colors.background.primary) + .padding(12.dp), + ) { + Text( + text = stringResourceSafe( + R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title, + ), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = stringResourceSafe( + R.string.yield_module_token_details_earn_notification_processing, + ), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.tertiary, + ) + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + color = TangemTheme.colors.icon.accent, + strokeWidth = 2.dp, + ) + } + } +} + +@Composable +private fun SupplyLoading() { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(TangemTheme.colors.background.primary) + .padding(12.dp), + ) { + Text( + text = stringResourceSafe( + R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title, + ), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + TextShimmer( + style = TangemTheme.typography.subtitle2, + text = stringResourceSafe( + R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title, + ), + ) + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun YieldSupplyBlockContent_Preview(@PreviewParameter(PreviewProvider::class) params: YieldSupplyUM) { + TangemThemePreview { + YieldSupplyBlockContent(params) + } +} + +private class PreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + YieldSupplyUM.Initial( + title = TextReference.Res( + R.string.yield_module_token_details_earn_notification_title, + wrappedList("5.1"), + ), + onClick = {}, + ), + YieldSupplyUM.Content( + rewardsBalance = stringReference("1 USDT"), + rewardsApy = stringReference("5.1 % APY"), + onClick = {}, + isAllowedToSpend = false, + ), + YieldSupplyUM.Content( + rewardsBalance = stringReference("1 USDT"), + rewardsApy = stringReference("5.1 % APY"), + onClick = {}, + isAllowedToSpend = true, + ), + YieldSupplyUM.Loading, + YieldSupplyUM.Processing, + ) +} +// endregion \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/DefaultYieldSupplyPromoComponent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/DefaultYieldSupplyPromoComponent.kt index 7e9414c4a1..e5125df605 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/DefaultYieldSupplyPromoComponent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/DefaultYieldSupplyPromoComponent.kt @@ -3,15 +3,19 @@ package com.tangem.features.yield.supply.impl.promo import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss 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.ui.decompose.ComposableBottomSheetComponent -import com.tangem.core.ui.decompose.getEmptyComposableBottomSheetComponent +import com.tangem.core.ui.decompose.EmptyComposableBottomSheetComponent import com.tangem.features.yield.supply.api.YieldSupplyPromoComponent import com.tangem.features.yield.supply.impl.promo.model.YieldSupplyPromoModel import com.tangem.features.yield.supply.impl.promo.ui.YieldSupplyPromoContent +import com.tangem.features.yield.supply.impl.subcomponents.startearning.YieldSupplyStartEarningEntryComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -27,7 +31,7 @@ internal class DefaultYieldSupplyPromoComponent @AssistedInject constructor( source = model.bottomSheetNavigation, serializer = null, handleBackButton = false, - childFactory = { _, _ -> bottomSheetChild() }, + childFactory = { config, componentContext -> bottomSheetChild(config, componentContext) }, ) @Composable @@ -44,7 +48,26 @@ internal class DefaultYieldSupplyPromoComponent @AssistedInject constructor( bottomSheet.child?.instance?.BottomSheet() } - private fun bottomSheetChild(): ComposableBottomSheetComponent = getEmptyComposableBottomSheetComponent() + private fun bottomSheetChild( + config: YieldSupplyPromoConfig, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent = when (config) { + YieldSupplyPromoConfig.Apy -> EmptyComposableBottomSheetComponent + YieldSupplyPromoConfig.Action -> YieldSupplyStartEarningEntryComponent( + appComponentContext = childByContext(componentContext), + params = YieldSupplyStartEarningEntryComponent.Params( + userWalletId = params.userWalletId, + cryptoCurrency = params.currency, + onDismiss = { isProcessing -> + if (isProcessing) { + router.pop() + } else { + model.bottomSheetNavigation.dismiss() + } + }, + ), + ) + } @AssistedFactory interface Factory : YieldSupplyPromoComponent.Factory { diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt index 40180b5882..25b22b91ae 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt @@ -160,7 +160,9 @@ internal class YieldSupplyStartEarningModel @Inject constructor( uiState.update { it.copy(isPrimaryButtonEnabled = true) } }, ifRight = { - params.callback.onTransactionSent() + modelScope.launch { + params.callback.onTransactionSent() + } }, ) }