diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index c6d4c83b91..0807a628f1 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -2,6 +2,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.staking.* import com.tangem.domain.staking.repositories.StakingErrorResolver +import com.tangem.domain.staking.repositories.StakingPendingTransactionRepository import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.repositories.StakingTransactionHashRepository import dagger.Module @@ -122,6 +123,42 @@ internal object StakingDomainModule { ) } + @Provides + @Singleton + fun provideSavePendingTransactionUseCase( + stakingPendingTransactionRepository: StakingPendingTransactionRepository, + stakingErrorResolver: StakingErrorResolver, + ): SavePendingTransactionUseCase { + return SavePendingTransactionUseCase( + stakingPendingTransactionRepository = stakingPendingTransactionRepository, + stakingErrorResolver = stakingErrorResolver, + ) + } + + @Provides + @Singleton + fun provideInvalidatePendingTransactionsUseCase( + stakingPendingTransactionRepository: StakingPendingTransactionRepository, + stakingErrorResolver: StakingErrorResolver, + ): InvalidatePendingTransactionsUseCase { + return InvalidatePendingTransactionsUseCase( + stakingPendingTransactionRepository = stakingPendingTransactionRepository, + stakingErrorResolver = stakingErrorResolver, + ) + } + + @Provides + @Singleton + fun provideGetPendingTransactionsUseCase( + stakingPendingTransactionRepository: StakingPendingTransactionRepository, + stakingErrorResolver: StakingErrorResolver, + ): GetPendingTransactionsUseCase { + return GetPendingTransactionsUseCase( + stakingPendingTransactionRepository = stakingPendingTransactionRepository, + stakingErrorResolver = stakingErrorResolver, + ) + } + @Provides @Singleton fun provideSendUnsubmittedHashesUseCase( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt index 6db1972160..bd65f92ab5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt @@ -24,6 +24,7 @@ import com.tangem.core.ui.components.inputrow.inner.InputRowAsyncImage import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveAnnotatedReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -38,10 +39,12 @@ import com.tangem.core.ui.res.TangemThemePreview * @param subtitleColor subtitle text color * @param captionColor caption text color * @param isGrayscaleImage whether to display grayscale image + * @param subtitleEndIconRes icon to show after subtitle * @param iconEndRes icon to end of row * @param onImageError composable to show if image loading failed * @param extraContent extra content */ +@Suppress("LongMethod") @Composable internal fun InputRowImageBase( subtitle: TextReference, @@ -53,7 +56,9 @@ internal fun InputRowImageBase( captionColor: Color = TangemTheme.colors.text.tertiary, iconTint: Color = TangemTheme.colors.icon.informative, isGrayscaleImage: Boolean = false, - iconEndRes: Int? = null, + @DrawableRes subtitleEndIconRes: Int? = null, + subtitleEndIconTint: Color = TangemColorPalette.Azure, + @DrawableRes iconEndRes: Int? = null, onImageError: (@Composable () -> Unit)? = null, extraContent: (@Composable RowScope.() -> Unit)? = null, ) { @@ -89,11 +94,17 @@ internal fun InputRowImageBase( SpacerW12() } Column { - Text( - text = subtitle.resolveReference(), - style = TangemTheme.typography.subtitle2, - color = subtitleColor, - ) + Row { + Text( + text = subtitle.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = subtitleColor, + ) + SubtitleEndIconRes( + subtitleEndIconRes = subtitleEndIconRes, + subtitleEndIconTint = subtitleEndIconTint, + ) + } if (caption != null) { Text( text = caption.resolveAnnotatedReference(), @@ -113,11 +124,28 @@ internal fun InputRowImageBase( } } +@Composable +private fun RowScope.SubtitleEndIconRes(subtitleEndIconRes: Int?, subtitleEndIconTint: Color) { + AnimatedVisibility( + visible = subtitleEndIconRes != null, + label = "Subtitle end icon visibility animation", + modifier = Modifier.align(Alignment.CenterVertically), + ) { + val icon = remember(this) { requireNotNull(subtitleEndIconRes) } + Icon( + painter = rememberVectorPainter(image = ImageVector.vectorResource(id = icon)), + tint = subtitleEndIconTint, + contentDescription = null, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), + ) + } +} + @Composable private fun RowScope.InputRowEndIcon(iconRes: Int?) { AnimatedVisibility( visible = iconRes != null, - label = "Icon visibility animation", + label = "End icon visibility animation", ) { val icon = remember(this) { requireNotNull(iconRes) } Icon( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt index 2905e007b7..366bd4e48d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt @@ -34,6 +34,7 @@ import com.tangem.core.ui.res.TangemThemePreview * @param subtitleColor subtitle text color * @param captionColor caption text color * @param isGrayscaleImage whether to display grayscale image + * @param showPendingIcon whether to show pending icon * @param iconEndRes icon to end of row * @param onImageError composable to show if image loading failed */ @@ -52,6 +53,7 @@ fun InputRowImageInfo( captionColor: Color = TangemTheme.colors.text.tertiary, iconTint: Color = TangemTheme.colors.icon.informative, isGrayscaleImage: Boolean = false, + @DrawableRes subtitleEndIconRes: Int? = null, @DrawableRes iconEndRes: Int? = null, onImageError: (@Composable () -> Unit)? = null, ) { @@ -76,6 +78,7 @@ fun InputRowImageInfo( subtitleColor = subtitleColor, captionColor = captionColor, isGrayscaleImage = isGrayscaleImage, + subtitleEndIconRes = subtitleEndIconRes, iconEndRes = iconEndRes, onImageError = onImageError, ) { @@ -132,6 +135,7 @@ private fun InputRowImageInfo_Preview( infoTitle = data.infoTitle, infoSubtitle = data.infoSubtitle, imageUrl = "", + subtitleEndIconRes = data.subtitleEndIconRes, iconEndRes = R.drawable.ic_chevron_right_24, ) } @@ -162,6 +166,22 @@ private class InputRowImageInfoPreviewDataProvider : PreviewParameterProvider + + + + diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingPendingTransactionRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingPendingTransactionRepository.kt new file mode 100644 index 0000000000..0b637744ab --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingPendingTransactionRepository.kt @@ -0,0 +1,27 @@ +package com.tangem.data.staking + +import com.tangem.domain.staking.model.PendingTransaction +import com.tangem.domain.staking.model.stakekit.BalanceItem +import com.tangem.domain.staking.repositories.StakingPendingTransactionRepository +import java.util.concurrent.CopyOnWriteArrayList + +internal class DefaultStakingPendingTransactionRepository : StakingPendingTransactionRepository { + + private val pendingTransactions = CopyOnWriteArrayList() + + override fun saveTransaction(transaction: PendingTransaction) { + pendingTransactions.add(transaction) + } + + override fun removeTransactions(transactions: Set) { + pendingTransactions.removeAll(transactions) + } + + override fun getTransactionsWithBalanceItems(): List> { + return pendingTransactions.mapNotNull { pendingTransaction -> + PendingTransactionItemConverter.convert(pendingTransaction)?.let { balanceItem -> + pendingTransaction to balanceItem + } + } + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/PendingTransactionItemConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/PendingTransactionItemConverter.kt new file mode 100644 index 0000000000..a18b61f591 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/PendingTransactionItemConverter.kt @@ -0,0 +1,22 @@ +package com.tangem.data.staking + +import com.tangem.domain.staking.model.PendingTransaction +import com.tangem.domain.staking.model.stakekit.BalanceItem +import com.tangem.utils.converter.Converter +import org.joda.time.DateTime + +internal object PendingTransactionItemConverter : Converter { + + override fun convert(value: PendingTransaction): BalanceItem? { + return BalanceItem( + groupId = value.groupId ?: return null, + type = value.type ?: return null, + amount = value.amount ?: return null, + rawCurrencyId = value.rawCurrencyId, + validatorAddress = value.validator?.address, + date = DateTime.now(), + pendingActions = emptyList(), + isPending = true, + ) + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt index 90b35354b7..5a7f56b25e 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt @@ -19,12 +19,10 @@ internal class YieldBalanceConverter : Converter BalanceItem( - id = item.groupId, + groupId = item.groupId, type = BalanceType.valueOf(item.type.name), amount = item.amount, - pricePerShare = item.pricePerShare, rawCurrencyId = item.tokenDTO.coinGeckoId, - rawNetworkId = item.tokenDTO.network.name, // tron-specific. operates validatorAddresses instead of validatorAddress validatorAddress = item.validatorAddress ?: item.validatorAddresses?.get(0), date = item.date?.toDateTime(), @@ -32,6 +30,7 @@ internal class YieldBalanceConverter : Converter, + val isPending: Boolean, ) data class PendingAction( diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetPendingTransactionsUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetPendingTransactionsUseCase.kt new file mode 100644 index 0000000000..1f4be9dea2 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetPendingTransactionsUseCase.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import com.tangem.domain.staking.model.stakekit.BalanceItem +import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.staking.repositories.StakingErrorResolver +import com.tangem.domain.staking.repositories.StakingPendingTransactionRepository + +/** + * Use case for getting saved staking pending transactions. + */ +class GetPendingTransactionsUseCase( + private val stakingPendingTransactionRepository: StakingPendingTransactionRepository, + private val stakingErrorResolver: StakingErrorResolver, +) { + + operator fun invoke(): Either> { + return Either.catch { + stakingPendingTransactionRepository.getTransactionsWithBalanceItems().map { it.second } + }.mapLeft { + stakingErrorResolver.resolve(it) + } + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt new file mode 100644 index 0000000000..daddce0ad9 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt @@ -0,0 +1,54 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import com.tangem.domain.staking.model.PendingTransaction +import com.tangem.domain.staking.model.stakekit.BalanceItem +import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.staking.repositories.StakingErrorResolver +import com.tangem.domain.staking.repositories.StakingPendingTransactionRepository + +class InvalidatePendingTransactionsUseCase( + private val stakingPendingTransactionRepository: StakingPendingTransactionRepository, + private val stakingErrorResolver: StakingErrorResolver, +) { + + operator fun invoke(yieldBalance: YieldBalance): Either> { + return Either.catch { + if (yieldBalance is YieldBalance.Data) { + val (balancesToDisplay, transactionsToRemove) = mergeRealAndPendingTransactions( + real = yieldBalance.balance.items, + pending = stakingPendingTransactionRepository.getTransactionsWithBalanceItems(), + ) + + stakingPendingTransactionRepository.removeTransactions(transactionsToRemove.toSet()) + + balancesToDisplay + } else { + emptyList() + } + }.mapLeft { + stakingErrorResolver.resolve(it) + } + } + + private fun mergeRealAndPendingTransactions( + real: List, + pending: List>, + ): Pair, List> { + val map = real.associateBy { Triple(it.groupId, it.type, it.amount) }.toMutableMap() + + val toRemove = mutableListOf() + + pending.forEach { (pendingTransaction, balanceItem) -> + val key = Triple(balanceItem.groupId, balanceItem.type, balanceItem.amount) + if (map.containsKey(key)) { + map[key] = balanceItem + } else { + toRemove.add(pendingTransaction) + } + } + + return map.values.toList() to toRemove + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/SavePendingTransactionUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/SavePendingTransactionUseCase.kt new file mode 100644 index 0000000000..64ce855ae5 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/SavePendingTransactionUseCase.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import com.tangem.domain.staking.model.PendingTransaction +import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.staking.repositories.StakingErrorResolver +import com.tangem.domain.staking.repositories.StakingPendingTransactionRepository + +/** + * Use case for saving hash that failed to submit during staking confirmation + */ +class SavePendingTransactionUseCase( + private val stakingPendingTransactionRepository: StakingPendingTransactionRepository, + private val stakingErrorResolver: StakingErrorResolver, +) { + + operator fun invoke(pendingTransaction: PendingTransaction): Either { + return Either.catch { + stakingPendingTransactionRepository.saveTransaction(pendingTransaction) + }.mapLeft { + stakingErrorResolver.resolve(it) + } + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/SubmitHashUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/SubmitHashUseCase.kt index 83d1b151b7..fba8daa29b 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/SubmitHashUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/SubmitHashUseCase.kt @@ -1,6 +1,7 @@ package com.tangem.domain.staking import arrow.core.Either +import com.tangem.domain.staking.model.SubmitHashData import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingTransactionHashRepository @@ -13,12 +14,12 @@ class SubmitHashUseCase( private val stakingErrorResolver: StakingErrorResolver, ) { - suspend fun submitHash(transactionId: String, transactionHash: String): Either { + suspend operator fun invoke(submitHashData: SubmitHashData): Either { return Either .catch { stakingTransactionHashRepository.submitHash( - transactionId = transactionId, - transactionHash = transactionHash, + transactionId = submitHashData.transactionId, + transactionHash = submitHashData.transactionHash, ) }.mapLeft { stakingErrorResolver.resolve(it) diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingPendingTransactionRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingPendingTransactionRepository.kt new file mode 100644 index 0000000000..02bb96806b --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingPendingTransactionRepository.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.staking.repositories + +import com.tangem.domain.staking.model.PendingTransaction +import com.tangem.domain.staking.model.stakekit.BalanceItem + +interface StakingPendingTransactionRepository { + + fun getTransactionsWithBalanceItems(): List> + + fun saveTransaction(transaction: PendingTransaction) + + fun removeTransactions(transactions: Set) +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt index c989de7fb1..92c74bd946 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt @@ -22,7 +22,7 @@ internal sealed class InnerYieldBalanceState { @Immutable internal data class BalanceState( - val id: String, + val groupId: String, val title: TextReference, val type: BalanceType, val subtitle: TextReference?, @@ -34,4 +34,5 @@ internal data class BalanceState( val rawCurrencyId: String?, val validator: Yield.Validator?, val pendingActions: ImmutableList, + val isPending: Boolean, ) \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 9e4db06b4e..c490284116 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -98,6 +98,7 @@ internal sealed class StakingStates { val transactionDoneState: TransactionDoneState, val isApprovalNeeded: Boolean, val reduceAmountBy: BigDecimal?, + val balanceState: BalanceState?, ) : ConfirmationState() data class Empty( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt index 9fd91d3cb8..141759adfb 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt @@ -22,6 +22,7 @@ internal class BalanceItemConverter( private val appCurrencyProvider: Provider, private val yield: Yield, ) : Converter { + override fun convert(value: BalanceItem): BalanceState? { val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val appCurrency = appCurrencyProvider() @@ -37,7 +38,7 @@ internal class BalanceItemConverter( val title = value.type.getTitle(validator?.name) return title?.let { BalanceState( - id = value.id, + groupId = value.groupId, validator = validator, title = title, subtitle = getSubtitle(value), @@ -59,7 +60,8 @@ internal class BalanceItemConverter( ), rawCurrencyId = value.rawCurrencyId, pendingActions = value.pendingActions.toPersistentList(), - isClickable = value.type.isClickable(), + isClickable = value.type.isClickable() && !value.isPending, + isPending = value.isPending, ) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt index e3cc2f8e07..295f07c032 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt @@ -78,7 +78,7 @@ internal class RewardsValidatorStateConverter( ) return BalanceState( - id = balance.id, + groupId = balance.groupId, validator = this, title = stringReference(this.name), subtitle = null, @@ -90,6 +90,7 @@ internal class RewardsValidatorStateConverter( pendingActions = balance.pendingActions.toPersistentList(), isClickable = true, type = balance.type, + isPending = balance.isPending, ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index ae45ca8d1f..5c82de46c3 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -14,6 +14,7 @@ import kotlinx.collections.immutable.toPersistentList internal class YieldBalancesConverter( private val cryptoCurrencyStatusProvider: Provider, private val appCurrencyProvider: Provider, + private val balancesToShowProvider: Provider>, private val yield: Yield, ) : Converter { @@ -32,6 +33,8 @@ internal class YieldBalancesConverter( val cryptoRewardsValue = yieldBalance.getRewardStakingBalance() val fiatRewardsValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoRewardsValue) + val balanceToShowItems = balancesToShowProvider() + InnerYieldBalanceState.Data( rewardsCrypto = BigDecimalFormatter.formatCryptoAmount( cryptoAmount = cryptoRewardsValue, @@ -43,7 +46,7 @@ internal class YieldBalancesConverter( fiatCurrencySymbol = appCurrency.symbol, ), rewardBlockType = getRewardBlockType(), - balance = yieldBalance.balance.items.mapBalances(), + balance = balanceToShowItems.mapBalances(), ) } else { InnerYieldBalanceState.Empty diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt index e6e0b2b6e0..109dafb912 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt @@ -5,10 +5,10 @@ import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.domain.staking.GetConstructedStakingTransactionUseCase -import com.tangem.domain.staking.GetStakingTransactionUseCase -import com.tangem.domain.staking.SaveUnsubmittedHashUseCase -import com.tangem.domain.staking.SubmitHashUseCase +import com.tangem.domain.staking.* +import com.tangem.domain.staking.model.PendingTransaction +import com.tangem.domain.staking.model.SubmitHashData +import com.tangem.domain.staking.model.stakekit.BalanceType import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.model.stakekit.Yield @@ -45,6 +45,7 @@ internal class StakingTransactionSender @AssistedInject constructor( private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val submitHashUseCase: SubmitHashUseCase, private val saveUnsubmittedHashUseCase: SaveUnsubmittedHashUseCase, + private val savePendingTransactionUseCase: SavePendingTransactionUseCase, private val analyticsEventHandler: AnalyticsEventHandler, @Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus, @Assisted private val userWallet: UserWallet, @@ -89,6 +90,7 @@ internal class StakingTransactionSender @AssistedInject constructor( sendStakingTransaction( fullTransactionsData = fullTransactionsData, + balanceState = confirmationState.balanceState, onSendSuccess = onSendSuccess, onSendError = onSendError, ) @@ -191,6 +193,7 @@ internal class StakingTransactionSender @AssistedInject constructor( private suspend fun sendStakingTransaction( fullTransactionsData: List, + balanceState: BalanceState?, onSendSuccess: (txUrl: String) -> Unit, onSendError: (SendTransactionError?) -> Unit, ) { @@ -206,6 +209,11 @@ internal class StakingTransactionSender @AssistedInject constructor( submitHash( transactionIds = fullTransactionsData.map { it.stakeKitTransaction.id }, transactionHashes = transactionHashes, + groupId = balanceState?.groupId, + validator = balanceState?.validator, + amount = balanceState?.cryptoDecimal, + balanceType = balanceState?.type, + rawCurrencyId = balanceState?.rawCurrencyId, ) val txUrl = getExplorerTransactionUrlUseCase( txHash = transactionHashes.last(), @@ -218,13 +226,27 @@ internal class StakingTransactionSender @AssistedInject constructor( ) } - private suspend fun submitHash(transactionIds: List, transactionHashes: List) { + private suspend fun submitHash( + transactionIds: List, + transactionHashes: List, + groupId: String?, + validator: Yield.Validator?, + amount: BigDecimal?, + balanceType: BalanceType?, + rawCurrencyId: String?, + ) { transactionIds .zip(transactionHashes) .forEach { (transactionId, transactionHash) -> - submitHashUseCase.submitHash( - transactionId = transactionId, - transactionHash = transactionHash, + submitHashUseCase( + SubmitHashData( + transactionId = transactionId, + transactionHash = transactionHash, + validator = validator, + amount = amount, + balanceType = balanceType, + rawCurrencyId = rawCurrencyId, + ), ) .onLeft { analyticsEventHandler.send( @@ -236,6 +258,15 @@ internal class StakingTransactionSender @AssistedInject constructor( ) }.onRight { Timber.d("Successful hash submission") + savePendingTransactionUseCase.invoke( + PendingTransaction( + groupId = groupId, + type = balanceType, + amount = amount, + rawCurrencyId = rawCurrencyId, + validator = validator, + ), + ) } } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt index b5b6e9061f..51b7ec69a2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt @@ -93,5 +93,6 @@ internal object ConfirmationStatePreviewData { isApprovalNeeded = false, reduceAmountBy = null, pendingActions = null, + balanceState = null, ) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index 6ad1e50645..e42df87aba 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -68,7 +68,7 @@ internal object InitialStakingStatePreview { rewardBlockType = RewardBlockType.RewardUnavailable, balance = persistentListOf( BalanceState( - id = "id", + groupId = "groupId", title = stringReference("Binance"), cryptoValue = "100", cryptoAmount = stringReference("100 SOL"), @@ -91,6 +91,7 @@ internal object InitialStakingStatePreview { isClickable = true, type = BalanceType.STAKED, subtitle = null, + isPending = false, ), ), ), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index b4c0e2a299..66ec803e8a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -20,8 +20,8 @@ internal object StakingClickIntentsStub : StakingClickIntents { actionTypeToOverwrite: StakingActionCommonType?, pendingAction: PendingAction?, pendingActions: ImmutableList?, - ) { - } + balanceState: BalanceState?, + ) { } override fun onActionClick() {} diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetBalanceStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetBalanceStateTransformer.kt new file mode 100644 index 0000000000..d6fae7cd7f --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetBalanceStateTransformer.kt @@ -0,0 +1,18 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.utils.transformer.Transformer + +internal class SetBalanceStateTransformer( + private val balanceState: BalanceState? = null, +) : Transformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + val possibleConfirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data + ?: return prevState + + return prevState.copy( + confirmationState = possibleConfirmationState.copy(balanceState = balanceState), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt index 27d5d30086..c2df5db972 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt @@ -41,6 +41,7 @@ internal class SetConfirmationStateLoadingTransformer( pendingActions = possibleConfirmationState?.pendingActions, isApprovalNeeded = false, reduceAmountBy = null, + balanceState = null, ), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index e20ee87384..66b56ea7aa 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -10,6 +10,7 @@ import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.core.serialization.SerializedBigDecimal +import com.tangem.domain.staking.model.stakekit.BalanceItem import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet @@ -37,6 +38,7 @@ internal class SetInitialDataStateTransformer( private val cryptoCurrencyStatusProvider: Provider, private val userWalletProvider: Provider, private val appCurrencyProvider: Provider, + private val balancesToShowProvider: Provider>, ) : Transformer { private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) @@ -59,6 +61,7 @@ internal class SetInitialDataStateTransformer( YieldBalancesConverter( cryptoCurrencyStatusProvider, appCurrencyProvider, + balancesToShowProvider, yield, ) } @@ -228,6 +231,7 @@ internal class SetInitialDataStateTransformer( pendingActions = null, isApprovalNeeded = isApprovalNeeded, reduceAmountBy = null, + balanceState = null, ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index 679dd0675d..589c784974 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -89,7 +89,7 @@ internal fun StakingInitialInfoContent( if (state.showBanner) { item(key = BANNER_BLOCK_KEY) { Column( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), ) { BannerBlock(onClick = clickIntents::onInitialInfoBannerClick) SpacerH12() @@ -105,7 +105,7 @@ internal fun StakingInitialInfoContent( if (state.yieldBalance is InnerYieldBalanceState.Data) { item(key = STAKING_REWARD_BLOCK_KEY) { - Column(modifier = Modifier.animateItemPlacement()) { + Column(modifier = Modifier.animateItem()) { StakingRewardBlock( rewardCrypto = state.yieldBalance.rewardsCrypto, rewardFiat = state.yieldBalance.rewardsFiat, @@ -138,7 +138,6 @@ internal fun StakingInitialInfoContent( } } -@OptIn(ExperimentalFoundationApi::class) private fun LazyListScope.activeStakingBlock( state: StakingStates.InitialInfoState.Data, clickIntents: StakingClickIntents, @@ -175,7 +174,7 @@ private fun LazyListScope.activeStakingBlock( onClick = clickIntents::onActiveStake, onAnalytic = clickIntents::onActiveStakeAnalytic, modifier = Modifier - .animateItemPlacement() + .animateItem() .then( if (state.yieldBalance.balance.last() == balance) { Modifier.clip(CornersToRound.BOTTOM_2.getShape()) @@ -279,6 +278,7 @@ private fun ActiveStakingBlock( imageUrl = balance.getImage(), iconRes = icon, iconTint = iconTint, + subtitleEndIconRes = R.drawable.ic_staking_pending_transaction.takeIf { balance.isPending }, onImageError = { ValidatorImagePlaceholder() }, modifier = modifier .background(TangemTheme.colors.background.action) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index c6fe6235d4..4cad63c4c7 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -20,6 +20,7 @@ internal interface StakingClickIntents : AmountScreenClickIntents { actionTypeToOverwrite: StakingActionCommonType? = null, pendingAction: PendingAction? = null, pendingActions: ImmutableList? = null, + balanceState: BalanceState? = null, ) fun onActionClick() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index cebace0c6e..a689d68b40 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -23,9 +23,11 @@ import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.staking.InvalidatePendingTransactionsUseCase import com.tangem.domain.staking.IsAnyTokenStakedUseCase import com.tangem.domain.staking.IsApproveNeededUseCase import com.tangem.domain.staking.model.StakingApproval +import com.tangem.domain.staking.model.stakekit.BalanceItem import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.YieldBalance @@ -98,6 +100,7 @@ internal class StakingViewModel @Inject constructor( private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase, private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, private val isAnyTokenStakedUseCase: IsAnyTokenStakedUseCase, + private val invalidatePendingTransactionsUseCase: InvalidatePendingTransactionsUseCase, private val stakingTransactionLoader: StakingTransactionSender.Factory, private val stakingFeeTransactionLoader: StakingFeeTransactionLoader.Factory, private val stakingBalanceUpdater: StakingBalanceUpdater.Factory, @@ -131,6 +134,13 @@ internal class StakingViewModel @Inject constructor( private var userWallet: UserWallet by Delegates.notNull() private var appCurrency: AppCurrency by Delegates.notNull() + private val balancesToShow: List + get() { + return cryptoCurrencyStatus.value.yieldBalance?.let { + invalidatePendingTransactionsUseCase(it).getOrElse { emptyList() } + } ?: emptyList() + } + private var isInitialInfoAnalyticSent: Boolean = false private val balanceUpdater by lazy(LazyThreadSafetyMode.NONE) { @@ -200,6 +210,7 @@ internal class StakingViewModel @Inject constructor( actionTypeToOverwrite: StakingActionCommonType?, pendingAction: PendingAction?, pendingActions: ImmutableList?, + balanceState: BalanceState?, ) { if (actionTypeToOverwrite != null) { stateController.update(SetActionToExecuteTransformer(actionTypeToOverwrite, pendingAction, pendingActions)) @@ -208,10 +219,12 @@ internal class StakingViewModel @Inject constructor( when { isInitState() -> { stateController.update(SetConfirmationStateLoadingTransformer(yield, appCurrency)) + stateController.update(SetBalanceStateTransformer(balanceState)) onRefreshSwipe(isRefreshing = false) } isAssentState() -> { getFee(pendingAction, pendingActions) + stateController.update(SetBalanceStateTransformer(balanceState)) val amountState = value.amountState as? AmountState.Data if (amountState?.amountTextField?.isWarning == true) { stateController.update( @@ -413,6 +426,7 @@ internal class StakingViewModel @Inject constructor( onNextClick( actionTypeToOverwrite = StakingActionCommonType.PENDING_OTHER, pendingAction = action, + balanceState = activeStake, ) stateController.update(DismissBottomSheetStateTransformer) }, @@ -428,6 +442,7 @@ internal class StakingViewModel @Inject constructor( actionTypeToOverwrite = null, pendingAction = activeStake.pendingActions.firstOrNull(), pendingActions = activeStake.pendingActions.takeIf { isAllWithdrawActions }, + balanceState = activeStake, ) } } @@ -749,6 +764,7 @@ internal class StakingViewModel @Inject constructor( cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, userWalletProvider = Provider { userWallet }, appCurrencyProvider = Provider { appCurrency }, + balancesToShowProvider = Provider { balancesToShow }, ), ) },