Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-06 12:18:13 +03:00
commit 531eb86ac5
1243 changed files with 49302 additions and 12422 deletions

View file

@ -0,0 +1,17 @@
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 YieldSupplyEntryComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
val cryptoCurrency: CryptoCurrency,
val apy: String,
)
interface Factory : ComponentFactory<Params, YieldSupplyEntryComponent>
}

View file

@ -3,4 +3,5 @@ package com.tangem.features.yield.supply.api
interface YieldSupplyFeatureToggles {
val isYieldSupplyFeatureEnabled: Boolean
val isYieldSupplyPendingTransactionsEnabled: Boolean
}

View file

@ -0,0 +1,24 @@
package com.tangem.features.yield.supply.api.entry
import com.tangem.core.decompose.navigation.Route
import com.tangem.domain.models.currency.CryptoCurrency
/**
* Route for switching yield supply promo and active flows.
*/
sealed class YieldSupplyEntryRoute : Route {
/** Initial empty route with no UI */
data object Empty : YieldSupplyEntryRoute()
/** Route to yield supply promo screen */
data class Promo(
val cryptoCurrency: CryptoCurrency,
val apy: String,
) : YieldSupplyEntryRoute()
/** Route to yield supply active screen */
data class Active(
val cryptoCurrency: CryptoCurrency,
) : YieldSupplyEntryRoute()
}

View file

@ -8,4 +8,7 @@ internal class DefaultYieldSupplyFeatureToggles(
) : YieldSupplyFeatureToggles {
override val isYieldSupplyFeatureEnabled: Boolean
get() = featureToggles.isFeatureEnabled("YIELD_SUPPLY_FEATURE_ENABLED")
override val isYieldSupplyPendingTransactionsEnabled: Boolean
get() = featureToggles.isFeatureEnabled("YIELD_SUPPLY_PENDING_TRANSACTIONS_ENABLED")
}

View file

@ -31,4 +31,5 @@ internal data class YieldSupplyActionUM(
val yieldSupplyFeeUM: YieldSupplyFeeUM,
val isPrimaryButtonEnabled: Boolean,
val isTransactionSending: Boolean,
val isHoldToConfirmEnabled: Boolean,
)

View file

@ -156,6 +156,7 @@ private class YieldSupplyActionContentPreviewProvider : PreviewParameterProvider
),
isPrimaryButtonEnabled = false,
isTransactionSending = false,
isHoldToConfirmEnabled = false,
),
)
}

View file

@ -0,0 +1,124 @@
package com.tangem.features.yield.supply.impl.entry
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.extensions.compose.stack.Children
import com.arkivanov.decompose.extensions.compose.stack.animation.fade
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
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.ComposableContentComponent
import com.tangem.features.yield.supply.api.YieldSupplyActiveComponent
import com.tangem.features.yield.supply.api.YieldSupplyEntryComponent
import com.tangem.features.yield.supply.api.YieldSupplyPromoComponent
import com.tangem.features.yield.supply.api.entry.YieldSupplyEntryRoute
import com.tangem.features.yield.supply.impl.entry.model.YieldSupplyEntryModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultYieldSupplyEntryComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: YieldSupplyEntryComponent.Params,
private val yieldSupplyPromoComponentFactory: YieldSupplyPromoComponent.Factory,
private val yieldSupplyActiveComponentFactory: YieldSupplyActiveComponent.Factory,
) : YieldSupplyEntryComponent, AppComponentContext by appComponentContext {
private val stackNavigation = StackNavigation<YieldSupplyEntryRoute>()
private val innerRouter = InnerRouter<YieldSupplyEntryRoute>(
stackNavigation = stackNavigation,
popCallback = { onChildBack() },
)
private val model: YieldSupplyEntryModel = getOrCreateModel(
params = params,
router = innerRouter,
)
private val childStack = childStack(
key = "yieldSupplyEntryStack",
source = stackNavigation,
serializer = null,
initialConfiguration = model.initialRoute,
handleBackButton = true,
childFactory = { configuration, factoryContext ->
getChildComponent(
configuration = configuration,
factoryContext = childByContext(
componentContext = factoryContext,
router = innerRouter,
),
)
},
)
@Composable
override fun Content(modifier: Modifier) {
val childStackValue by childStack.subscribeAsState()
Children(
stack = childStackValue,
modifier = modifier,
animation = stackAnimation { fade() },
) { child ->
child.instance.Content(Modifier.fillMaxSize())
}
}
private fun getChildComponent(
configuration: YieldSupplyEntryRoute,
factoryContext: AppComponentContext,
): ComposableContentComponent = when (configuration) {
is YieldSupplyEntryRoute.Empty -> EmptyComponent
is YieldSupplyEntryRoute.Promo -> yieldSupplyPromoComponentFactory.create(
context = factoryContext,
params = YieldSupplyPromoComponent.Params(
userWalletId = params.userWalletId,
currency = configuration.cryptoCurrency,
apy = configuration.apy,
),
)
is YieldSupplyEntryRoute.Active -> yieldSupplyActiveComponentFactory.create(
context = factoryContext,
params = YieldSupplyActiveComponent.Params(
userWalletId = params.userWalletId,
cryptoCurrency = configuration.cryptoCurrency,
),
)
}
private object EmptyComponent : ComposableContentComponent {
@Composable
override fun Content(modifier: Modifier) {
Box(modifier = modifier)
}
}
private fun onChildBack() {
if (childStack.value.backStack.isEmpty() ||
childStack.value.backStack.last().configuration is YieldSupplyEntryRoute.Empty
) {
router.pop()
} else {
stackNavigation.pop()
}
}
@AssistedFactory
interface Factory : YieldSupplyEntryComponent.Factory {
override fun create(
context: AppComponentContext,
params: YieldSupplyEntryComponent.Params,
): DefaultYieldSupplyEntryComponent
}
}

View file

@ -0,0 +1,34 @@
package com.tangem.features.yield.supply.impl.entry.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.yield.supply.api.YieldSupplyEntryComponent
import com.tangem.features.yield.supply.impl.entry.DefaultYieldSupplyEntryComponent
import com.tangem.features.yield.supply.impl.entry.model.YieldSupplyEntryModel
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 YieldSupplyEntryBindsModule {
@Binds
@Singleton
fun provideYieldSupplyEntryComponentFactory(
impl: DefaultYieldSupplyEntryComponent.Factory,
): YieldSupplyEntryComponent.Factory
}
@Module
@InstallIn(ModelComponent::class)
internal interface YieldSupplyEntryModelModule {
@Binds
@IntoMap
@ClassKey(YieldSupplyEntryModel::class)
fun provideYieldSupplyEntryModel(impl: YieldSupplyEntryModel): Model
}

View file

@ -0,0 +1,95 @@
package com.tangem.features.yield.supply.impl.entry.model
import com.tangem.common.routing.AppRoute
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.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.tokens.model.details.NavigationAction
import com.tangem.domain.yield.supply.usecase.YieldSupplyEnterStatusUseCase
import com.tangem.features.yield.supply.api.YieldSupplyEntryComponent
import com.tangem.features.yield.supply.api.entry.YieldSupplyEntryRoute
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
@ModelScoped
internal class YieldSupplyEntryModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val yieldSupplyEnterStatusUseCase: YieldSupplyEnterStatusUseCase,
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
) : Model() {
private val params = paramsContainer.require<YieldSupplyEntryComponent.Params>()
val initialRoute: YieldSupplyEntryRoute = YieldSupplyEntryRoute.Empty
init {
navigateToInitialRoute()
}
private fun navigateToInitialRoute() {
val userWalletId = params.userWalletId
val cryptoCurrency = params.cryptoCurrency
modelScope.launch(dispatchers.default) {
getSingleCryptoCurrencyStatusUseCase.invokeMultiWalletSync(
userWalletId = userWalletId,
cryptoCurrencyId = cryptoCurrency.id,
).onLeft { error ->
Timber.e("Failed to get CryptoCurrencyStatus: $error")
withContext(dispatchers.mainImmediate) {
router.pop()
}
}.onRight { cryptoCurrencyStatus ->
withContext(dispatchers.mainImmediate) {
val route = getInitialRoute(cryptoCurrencyStatus)
if (route != null) {
router.replaceCurrent(route)
}
}
}
}
}
private suspend fun getInitialRoute(cryptoCurrencyStatus: CryptoCurrencyStatus): YieldSupplyEntryRoute? {
val userWalletId = params.userWalletId
val cryptoCurrency = params.cryptoCurrency
val token = cryptoCurrency as? CryptoCurrency.Token
if (token == null) {
router.pop()
return null
}
val tokenEnterStatus = yieldSupplyEnterStatusUseCase(
userWalletId = userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
).getOrNull()
val isActiveYield = cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive == true
if (tokenEnterStatus != null) {
router.replaceCurrent(
AppRoute.CurrencyDetails(
userWalletId = userWalletId,
currency = token,
navigationAction = NavigationAction.YieldSupply(
isActive = isActiveYield,
),
),
)
return null
}
return if (isActiveYield) {
YieldSupplyEntryRoute.Active(cryptoCurrency = token)
} else {
YieldSupplyEntryRoute.Promo(cryptoCurrency = token, apy = params.apy)
}
}
}

View file

@ -2,8 +2,6 @@ package com.tangem.features.yield.supply.impl.main.model
import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute
import android.os.SystemClock
import com.tangem.common.routing.AppRoute.YieldSupplyPromo
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
@ -25,8 +23,7 @@ import com.tangem.domain.models.yield.supply.YieldSupplyStatus
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.yield.supply.models.YieldSupplyEnterStatus
import com.tangem.domain.yield.supply.models.YieldSupplyPendingStatus
import com.tangem.domain.yield.supply.usecase.*
import com.tangem.features.yield.supply.api.YieldSupplyComponent
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
@ -34,12 +31,7 @@ import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.main.entity.YieldSupplyUM
import com.tangem.features.yield.supply.impl.main.model.transformers.YieldSupplyTokenStatusSuccessTransformer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.DelayedWork
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import com.tangem.utils.transformer.update
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
@ -58,12 +50,12 @@ internal class YieldSupplyModel @Inject constructor(
private val getUserWalletUseCase: GetUserWalletUseCase,
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
@DelayedWork private val coroutineScope: CoroutineScope,
private val yieldSupplyGetTokenStatusUseCase: YieldSupplyGetTokenStatusUseCase,
private val yieldSupplyIsAvailableUseCase: YieldSupplyIsAvailableUseCase,
private val yieldSupplyActivateUseCase: YieldSupplyActivateUseCase,
private val yieldSupplyDeactivateUseCase: YieldSupplyDeactivateUseCase,
private val yieldSupplyRepository: YieldSupplyRepository,
private val yieldSupplyEnterStatusUseCase: YieldSupplyEnterStatusUseCase,
private val yieldSupplyEnterStatusFlowUseCase: YieldSupplyEnterStatusFlowUseCase,
private val yieldSupplyMinAmountUseCase: YieldSupplyMinAmountUseCase,
private val yieldSupplyGetDustMinAmountUseCase: YieldSupplyGetDustMinAmountUseCase,
) : Model(), YieldSupplyClickIntents {
@ -76,11 +68,8 @@ internal class YieldSupplyModel @Inject constructor(
private val cryptoCurrency = params.cryptoCurrency
private var appCurrency: AppCurrency = AppCurrency.Default
var userWallet: UserWallet by Delegates.notNull()
private var latestCryptoCurrencyStatus: CryptoCurrencyStatus? = null
private val fetchCurrencyJobHolder = JobHolder()
private val loadStatusJobHolder = JobHolder()
private var lastStatusCheckTimestamp = 0L
private val isFirstCryptoCurrencyStatusEmission = AtomicBoolean(true)
init {
@ -92,7 +81,7 @@ internal class YieldSupplyModel @Inject constructor(
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
val isAvailable = yieldSupplyIsAvailableUseCase(params.userWalletId, params.cryptoCurrency)
if (isAvailable) {
subscribeOnCurrencyStatusUpdates()
loadUserWalletData()
singleNetworkStatusFetcher(
params = SingleNetworkStatusFetcher.Params(
userWalletId = params.userWalletId,
@ -103,29 +92,12 @@ internal class YieldSupplyModel @Inject constructor(
}
}
private fun subscribeOnCurrencyStatusUpdates() {
private fun loadUserWalletData() {
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 ->
if (isFirstCryptoCurrencyStatusEmission.compareAndSet(true, false)) {
sendInfoAboutProtocolStatus(cryptoCurrencyStatus)
}
onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus)
},
ifLeft = {
Timber.w(it.toString())
},
)
}.launchIn(modelScope)
subscribeOnCurrencyStatusUpdates()
},
ifLeft = {
Timber.w(it.toString())
@ -135,6 +107,36 @@ internal class YieldSupplyModel @Inject constructor(
}
}
private fun subscribeOnCurrencyStatusUpdates() {
combine(
getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet(
userWalletId = params.userWalletId,
currencyId = cryptoCurrency.id,
isSingleWalletWithTokens = false,
),
yieldSupplyEnterStatusFlowUseCase(
userWalletId = params.userWalletId,
cryptoCurrency = cryptoCurrency,
),
) { maybeCryptoCurrency, _ ->
maybeCryptoCurrency
}.flowOn(dispatchers.io)
.onEach { maybeCryptoCurrency ->
maybeCryptoCurrency.fold(
ifRight = { cryptoCurrencyStatus ->
latestCryptoCurrencyStatus = cryptoCurrencyStatus
if (isFirstCryptoCurrencyStatusEmission.compareAndSet(true, false)) {
sendInfoAboutProtocolStatus(cryptoCurrencyStatus)
}
onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus)
},
ifLeft = {
Timber.w(it.toString())
},
)
}.launchIn(modelScope)
}
private suspend fun loadTokenStatus() {
val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return
yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken)
@ -152,108 +154,59 @@ internal class YieldSupplyModel @Inject constructor(
}
override fun onStartEarningClick() {
val apy = when (val yieldSupplyUM = uiState.value) {
is YieldSupplyUM.Available -> yieldSupplyUM.apy
is YieldSupplyUM.Content -> yieldSupplyUM.apy
else -> ""
}
appRouter.push(
YieldSupplyPromo(
userWalletId = params.userWalletId,
cryptoCurrency = params.cryptoCurrency,
apy = apy,
),
)
navigateToYieldSupplyEntry()
}
override fun onActiveClick() {
navigateToYieldSupplyEntry()
}
private fun navigateToYieldSupplyEntry() {
val cryptoCurrencyStatus = latestCryptoCurrencyStatus ?: return
val apy = when (val yieldSupplyUM = uiState.value) {
is YieldSupplyUM.Available -> yieldSupplyUM.apy
is YieldSupplyUM.Content -> yieldSupplyUM.apy
else -> ""
}
appRouter.push(
AppRoute.YieldSupplyActive(
AppRoute.YieldSupplyEntry(
userWalletId = params.userWalletId,
cryptoCurrency = params.cryptoCurrency,
cryptoCurrency = cryptoCurrencyStatus.currency,
apy = apy,
),
)
}
@Suppress("MaximumLineLength")
private fun onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus: CryptoCurrencyStatus) = modelScope.launch(
dispatchers.default,
) {
val yieldSupplyStatus = cryptoCurrencyStatus.value.yieldSupplyStatus
val tokenProtocolStatus = yieldSupplyRepository.getTokenProtocolStatus(
userWallet.walletId,
cryptoCurrency,
)
val tokenPendingStatus = yieldSupplyRepository.getTokenPendingStatus(
userWallet.walletId,
cryptoCurrencyStatus,
)
val isActive = yieldSupplyStatus?.isActive == true
private suspend fun onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus: CryptoCurrencyStatus) {
val isCryptoCurrencyStatusFromCache = cryptoCurrencyStatus.value.sources.networkSource != StatusSource.ACTUAL
val processing = uiState.value is YieldSupplyUM.Processing
Timber.d(
"YIELD " +
"yieldSupplyStatus $yieldSupplyStatus " +
"tokenProtocolStatus $tokenProtocolStatus " +
"tokenPendingStatus $tokenPendingStatus " +
"isActive $isActive " +
"processing $processing " +
"isCryptoCurrencyStatusFromCache $isCryptoCurrencyStatusFromCache",
)
if (isCryptoCurrencyStatusFromCache && processing) {
return@launch
return
}
when {
tokenProtocolStatus != null && tokenPendingStatus != null -> {
showProcessing(tokenPendingStatus)
lastStatusCheckTimestamp = 0L
}
tokenProtocolStatus == YieldSupplyEnterStatus.Exit && isActive ||
tokenProtocolStatus == YieldSupplyEnterStatus.Enter && !isActive -> {
if (lastStatusCheckTimestamp != 0L) {
if (SystemClock.elapsedRealtime() - lastStatusCheckTimestamp > MAX_STATUS_CHECK_LIMIT) {
loadStatus(cryptoCurrencyStatus)
lastStatusCheckTimestamp = 0L
} else {
showProcessing(tokenProtocolStatus)
}
} else {
showProcessing(tokenProtocolStatus)
lastStatusCheckTimestamp = SystemClock.elapsedRealtime()
}
}
else -> {
loadStatus(cryptoCurrencyStatus)
lastStatusCheckTimestamp = 0L
}
}
}.saveIn(loadStatusJobHolder)
val pendingStatus = yieldSupplyEnterStatusUseCase(
userWalletId = userWallet.walletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
).getOrNull()
private fun showProcessing(status: YieldSupplyEnterStatus) {
if (pendingStatus != null) {
showProcessing(pendingStatus)
} else {
loadStatus(cryptoCurrencyStatus)
}
}
private fun showProcessing(status: YieldSupplyPendingStatus) {
uiState.update {
when (status) {
YieldSupplyEnterStatus.Enter -> YieldSupplyUM.Processing.Enter
YieldSupplyEnterStatus.Exit -> YieldSupplyUM.Processing.Exit
is YieldSupplyPendingStatus.Enter -> YieldSupplyUM.Processing.Enter
is YieldSupplyPendingStatus.Exit -> YieldSupplyUM.Processing.Exit
}
}
fetchCurrencyWithDelay()
}
private suspend fun loadStatus(cryptoCurrencyStatus: CryptoCurrencyStatus) {
val yieldSupplyStatus = cryptoCurrencyStatus.value.yieldSupplyStatus
yieldSupplyRepository.saveTokenProtocolStatus(
userWalletId = userWallet.walletId,
cryptoCurrency = cryptoCurrency,
yieldSupplyEnterStatus = null,
)
if (yieldSupplyStatus?.isActive == true) {
loadActiveState(
cryptoCurrencyStatus = cryptoCurrencyStatus,
@ -264,20 +217,6 @@ internal class YieldSupplyModel @Inject constructor(
}
}
private fun fetchCurrencyWithDelay() {
coroutineScope.launch(dispatchers.io) {
delay(PROCESSING_UPDATE_DELAY)
singleNetworkStatusFetcher(
params = SingleNetworkStatusFetcher.Params(
userWalletId = userWallet.walletId,
network = cryptoCurrency.network,
),
).onLeft {
fetchCurrencyWithDelay()
}
}.saveIn(fetchCurrencyJobHolder)
}
private suspend fun loadActiveState(
cryptoCurrencyStatus: CryptoCurrencyStatus,
yieldSupplyStatus: YieldSupplyStatus,
@ -382,9 +321,4 @@ internal class YieldSupplyModel @Inject constructor(
}
}
}
private companion object {
const val PROCESSING_UPDATE_DELAY = 10_000L
const val MAX_STATUS_CHECK_LIMIT = 10_000L
}
}

View file

@ -18,7 +18,9 @@ import com.tangem.common.ui.userwallet.ext.walletInterationIcon
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.HoldToConfirmButton
import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.R as CoreR
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
@ -74,15 +76,30 @@ internal class YieldSupplyApproveComponent(
)
},
footer = {
PrimaryButtonIconEnd(
text = stringResourceSafe(R.string.common_confirm),
onClick = model::onClick,
iconResId = walletInterationIcon(params.userWallet),
enabled = state.isPrimaryButtonEnabled,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
)
val modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
if (state.isHoldToConfirmEnabled) {
HoldToConfirmButton(
text = stringResourceSafe(
CoreR.string.common_hold_to,
stringResourceSafe(R.string.common_confirm),
),
onConfirm = model::onClick,
enabled = state.isPrimaryButtonEnabled,
isLoading = state.isTransactionSending,
modifier = modifier,
)
} else {
PrimaryButtonIconEnd(
text = stringResourceSafe(R.string.common_confirm),
onClick = model::onClick,
iconResId = walletInterationIcon(params.userWallet),
enabled = state.isPrimaryButtonEnabled,
modifier = modifier,
)
}
},
content = {
YieldSupplyActionContent(

View file

@ -18,11 +18,13 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.isHotWallet
import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetContractAddressUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyPendingTracker
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.common.YieldSupplyAlertFactory
@ -58,6 +60,7 @@ internal class YieldSupplyApproveModel @Inject constructor(
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
private val yieldSupplyGetContractAddressUseCase: YieldSupplyGetContractAddressUseCase,
private val yieldSupplyPendingTracker: YieldSupplyPendingTracker,
private val yieldSupplyAlertFactory: YieldSupplyAlertFactory,
) : Model(), YieldSupplyNotificationsComponent.ModelCallback {
@ -95,6 +98,7 @@ internal class YieldSupplyApproveModel @Inject constructor(
yieldSupplyFeeUM = YieldSupplyFeeUM.Loading,
isPrimaryButtonEnabled = false,
isTransactionSending = false,
isHoldToConfirmEnabled = params.userWallet.isHotWallet,
),
)
@ -157,11 +161,17 @@ internal class YieldSupplyApproveModel @Inject constructor(
)
params.callback.onTransactionProgress(false)
},
ifRight = {
ifRight = { txHash ->
yieldSupplyPendingTracker.addPending(
userWalletId = userWallet.walletId,
cryptoCurrency = cryptoCurrency,
txIds = listOf(txHash),
)
val event = AnalyticsParam.TxSentFrom.Earning(
blockchain = cryptoCurrency.network.name,
token = cryptoCurrency.symbol,
feeType = AnalyticsParam.FeeType.Normal,
feeToken = feeCryptoCurrencyStatus.currency.symbol,
)
analyticsEventHandler.send(
Basic.TransactionSent(

View file

@ -54,7 +54,7 @@ internal class YieldSupplyNotificationsModel @Inject constructor(
fee = data.feeValue.orZero(),
userWalletId = params.userWalletId,
tokenStatus = cryptoCurrencyStatus,
coinStatus = feeCryptoCurrencyStatus,
feeStatus = feeCryptoCurrencyStatus,
).getOrNull()
addExceedsBalanceNotification(
@ -64,7 +64,8 @@ internal class YieldSupplyNotificationsModel @Inject constructor(
blockchainId = cryptoCurrencyStatus.currency.network.backendId,
),
onClick = ::openTokenDetails,
onAnalyticsEvent = { },
onAnalyticsEvent = { /*no-op*/ },
onResetAnalyticsEvent = { /*no-op*/ },
)
addFeeUnreachableNotification(

View file

@ -17,6 +17,7 @@ import com.tangem.common.ui.userwallet.ext.walletInterationIcon
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.HoldToConfirmButton
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
@ -25,6 +26,7 @@ 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.core.ui.R as CoreR
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
@ -106,22 +108,36 @@ internal class YieldSupplyStartEarningComponent(
override fun Footer() {
val state by model.uiState.collectAsStateWithLifecycle()
val icon = if (state.isPrimaryButtonEnabled) {
walletInterationIcon(model.userWallet)
} else {
null
}
val modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
PrimaryButtonIconEnd(
text = stringResourceSafe(R.string.yield_module_start_earning),
onClick = model::onClick,
enabled = state.isPrimaryButtonEnabled,
iconResId = icon,
showProgress = state.isTransactionSending,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
)
if (state.isHoldToConfirmEnabled) {
HoldToConfirmButton(
text = stringResourceSafe(
CoreR.string.common_hold_to,
stringResourceSafe(R.string.yield_module_start_earning),
),
onConfirm = model::onClick,
enabled = state.isPrimaryButtonEnabled,
isLoading = state.isTransactionSending,
modifier = modifier,
)
} else {
val icon = if (state.isPrimaryButtonEnabled) {
walletInterationIcon(model.userWallet)
} else {
null
}
PrimaryButtonIconEnd(
text = stringResourceSafe(R.string.yield_module_start_earning),
onClick = model::onClick,
enabled = state.isPrimaryButtonEnabled,
iconResId = icon,
showProgress = state.isTransactionSending,
modifier = modifier,
)
}
}
data class Params(

View file

@ -49,6 +49,7 @@ internal class YieldSupplyStartEarningEntryModel @Inject constructor(
yieldSupplyFeeUM = YieldSupplyFeeUM.Loading,
isPrimaryButtonEnabled = false,
isTransactionSending = false,
isHoldToConfirmEnabled = false,
),
)

View file

@ -16,13 +16,14 @@ 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.models.wallet.isHotWallet
import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.yield.supply.models.YieldSupplyEnterStatus
import com.tangem.domain.yield.supply.models.YieldSupplyPendingStatus
import com.tangem.domain.yield.supply.usecase.*
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
import com.tangem.features.yield.supply.impl.R
@ -66,6 +67,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
private val yieldSupplyGetMaxFeeUseCase: YieldSupplyGetMaxFeeUseCase,
private val yieldSupplyGetCurrentFeeUseCase: YieldSupplyGetCurrentFeeUseCase,
private val yieldSupplyRepository: YieldSupplyRepository,
private val yieldSupplyPendingTracker: YieldSupplyPendingTracker,
private val appsFlyerStore: AppsFlyerStore,
) : Model(), YieldSupplyNotificationsComponent.ModelCallback {
@ -108,6 +110,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
yieldSupplyFeeUM = YieldSupplyFeeUM.Loading,
isPrimaryButtonEnabled = false,
isTransactionSending = false,
isHoldToConfirmEnabled = false,
),
)
@ -247,23 +250,27 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
},
)
},
ifRight = {
onStartEarningTransactionSuccess(yieldSupplyFeeUM)
ifRight = { txsData ->
onStartEarningTransactionSuccess(yieldSupplyFeeUM, txsData)
},
)
}
}
private suspend fun onStartEarningTransactionSuccess(yieldSupplyFeeUM: YieldSupplyFeeUM.Content) {
yieldSupplyRepository.saveTokenProtocolStatus(
userWalletId,
cryptoCurrency,
YieldSupplyEnterStatus.Enter,
private suspend fun onStartEarningTransactionSuccess(
yieldSupplyFeeUM: YieldSupplyFeeUM.Content,
txsData: List<String>,
) {
yieldSupplyRepository.saveTokenProtocolPendingStatus(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
yieldSupplyPendingStatus = YieldSupplyPendingStatus.Enter(txsData),
)
val event = AnalyticsParam.TxSentFrom.Earning(
blockchain = cryptoCurrency.network.name,
token = cryptoCurrency.symbol,
feeType = AnalyticsParam.FeeType.Normal,
feeToken = feeCryptoCurrencyStatusFlow.value.currency.symbol,
)
analytics.send(
YieldSupplyAnalytics.FundsEarned(
@ -291,6 +298,11 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
}
modelScope.launch {
yieldSupplyPendingTracker.addPending(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
txIds = txsData,
)
params.callback.onTransactionSent()
}
}
@ -300,6 +312,9 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
getUserWalletUseCase(userWalletId).fold(
ifRight = { wallet ->
userWallet = wallet
uiState.update {
it.copy(isHoldToConfirmEnabled = wallet.isHotWallet)
}
getCurrenciesStatusUpdates()
},
ifLeft = {

View file

@ -19,7 +19,9 @@ import com.tangem.common.ui.userwallet.ext.walletInterationIcon
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.HoldToConfirmButton
import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.R as CoreR
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
@ -78,16 +80,31 @@ internal class YieldSupplyStopEarningComponent(
)
},
footer = {
PrimaryButtonIconEnd(
text = stringResourceSafe(R.string.common_confirm),
onClick = model::onClick,
iconResId = walletInterationIcon(params.userWallet),
enabled = state.isPrimaryButtonEnabled,
showProgress = state.isTransactionSending,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
)
val modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
if (state.isHoldToConfirmEnabled) {
HoldToConfirmButton(
text = stringResourceSafe(
CoreR.string.common_hold_to,
stringResourceSafe(R.string.common_confirm),
),
onConfirm = model::onClick,
enabled = state.isPrimaryButtonEnabled,
isLoading = state.isTransactionSending,
modifier = modifier,
)
} else {
PrimaryButtonIconEnd(
text = stringResourceSafe(R.string.common_confirm),
onClick = model::onClick,
iconResId = walletInterationIcon(params.userWallet),
enabled = state.isPrimaryButtonEnabled,
showProgress = state.isTransactionSending,
modifier = modifier,
)
}
},
content = {
YieldSupplyActionContent(

View file

@ -16,14 +16,16 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.isHotWallet
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.INCREASE_GAS_LIMIT_FOR_SUPPLY
import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.yield.supply.increaseGasLimitBy
import com.tangem.domain.yield.supply.models.YieldSupplyEnterStatus
import com.tangem.domain.yield.supply.models.YieldSupplyPendingStatus
import com.tangem.domain.yield.supply.usecase.YieldSupplyDeactivateUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyPendingTracker
import com.tangem.domain.yield.supply.usecase.YieldSupplyStopEarningUseCase
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
import com.tangem.features.yield.supply.impl.R
@ -62,6 +64,7 @@ internal class YieldSupplyStopEarningModel @Inject constructor(
private val yieldSupplyAlertFactory: YieldSupplyAlertFactory,
private val yieldSupplyDeactivateUseCase: YieldSupplyDeactivateUseCase,
private val yieldSupplyRepository: YieldSupplyRepository,
private val yieldSupplyPendingTracker: YieldSupplyPendingTracker,
private val appsFlyerStore: AppsFlyerStore,
) : Model(), YieldSupplyNotificationsComponent.ModelCallback {
@ -99,6 +102,7 @@ internal class YieldSupplyStopEarningModel @Inject constructor(
yieldSupplyFeeUM = YieldSupplyFeeUM.Loading,
isPrimaryButtonEnabled = false,
isTransactionSending = false,
isHoldToConfirmEnabled = params.userWallet.isHotWallet,
),
)
@ -169,18 +173,18 @@ internal class YieldSupplyStopEarningModel @Inject constructor(
)
params.callback.onTransactionProgress(false)
},
ifRight = {
onStopEarningTransactionSuccess()
ifRight = { txData ->
onStopEarningTransactionSuccess(txData)
},
)
}
}
private suspend fun onStopEarningTransactionSuccess() {
yieldSupplyRepository.saveTokenProtocolStatus(
userWallet.walletId,
cryptoCurrency,
YieldSupplyEnterStatus.Exit,
private suspend fun onStopEarningTransactionSuccess(txId: String) {
yieldSupplyRepository.saveTokenProtocolPendingStatus(
userWalletId = userWallet.walletId,
cryptoCurrency = cryptoCurrency,
yieldSupplyPendingStatus = YieldSupplyPendingStatus.Exit(listOf(txId)),
)
analytics.send(
YieldSupplyAnalytics.FundsWithdrawn(
@ -193,6 +197,7 @@ internal class YieldSupplyStopEarningModel @Inject constructor(
blockchain = cryptoCurrency.network.name,
token = cryptoCurrency.symbol,
feeType = AnalyticsParam.FeeType.Normal,
feeToken = feeCryptoCurrencyStatus.currency.symbol,
)
analytics.send(
Basic.TransactionSent(
@ -206,6 +211,11 @@ internal class YieldSupplyStopEarningModel @Inject constructor(
}
modelScope.launch {
yieldSupplyPendingTracker.addPending(
userWalletId = userWallet.walletId,
cryptoCurrency = cryptoCurrency,
txIds = listOf(txId),
)
params.callback.onStopEarningTransactionSent()
}
}