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 360e77ad20..7f79abcb99 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 @@ -10,6 +10,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDeta import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsPullToRefreshConfig import com.tangem.features.tokendetails.impl.R import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow @@ -71,6 +72,11 @@ internal object TokenDetailsPreviewData { private val marketPriceLoading = MarketPriceBlockState.Loading(currencyName = "USDT") + private val pullToRefreshConfig = TokenDetailsPullToRefreshConfig( + isRefreshing = false, + onRefresh = {}, + ) + val tokenDetailsState = TokenDetailsState( topAppBarConfig = tokenDetailsTopAppBarConfig, tokenInfoBlockState = tokenInfoBlockState, @@ -83,5 +89,6 @@ internal object TokenDetailsPreviewData { ), dialogConfig = null, pendingTxs = persistentListOf(), + pullToRefreshConfig = pullToRefreshConfig, ) } \ No newline at end of file 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 7aece5e270..3c40416911 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 @@ -4,6 +4,7 @@ import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsPullToRefreshConfig import kotlinx.collections.immutable.PersistentList internal data class TokenDetailsState( @@ -14,4 +15,5 @@ internal data class TokenDetailsState( val pendingTxs: PersistentList, val txHistoryState: TxHistoryState, val dialogConfig: TokenDetailsDialogConfig?, + val pullToRefreshConfig: TokenDetailsPullToRefreshConfig, ) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsPullToRefreshConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsPullToRefreshConfig.kt new file mode 100644 index 0000000000..5015995638 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsPullToRefreshConfig.kt @@ -0,0 +1,3 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.components + +data class TokenDetailsPullToRefreshConfig(val isRefreshing: Boolean, val onRefresh: () -> Unit) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsRefreshStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsRefreshStateConverter.kt new file mode 100644 index 0000000000..23c045fe3f --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsRefreshStateConverter.kt @@ -0,0 +1,19 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory + +import com.tangem.common.Provider +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.utils.converter.Converter + +internal class TokenDetailsRefreshStateConverter( + private val currentStateProvider: Provider, +) : Converter { + + override fun convert(value: Boolean): TokenDetailsState { + val state = currentStateProvider() + return state.createPullToRefresh(value) + } + + private fun TokenDetailsState.createPullToRefresh(isRefreshing: Boolean): TokenDetailsState { + return copy(pullToRefreshConfig = pullToRefreshConfig.copy(isRefreshing = isRefreshing)) + } +} \ 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 9d666add03..8d4bd2f378 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 @@ -8,6 +8,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsPullToRefreshConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSkeletonStateConverter.SkeletonModel import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.features.tokendetails.impl.R @@ -49,6 +50,7 @@ internal class TokenDetailsSkeletonStateConverter( ), ), dialogConfig = null, + pullToRefreshConfig = createPullToRefresh(), ) } @@ -72,5 +74,10 @@ internal class TokenDetailsSkeletonStateConverter( ) } + private fun createPullToRefresh(): TokenDetailsPullToRefreshConfig = TokenDetailsPullToRefreshConfig( + isRefreshing = false, + onRefresh = clickIntents::onRefreshSwipe, + ) + data class SkeletonModel(val cryptoCurrency: CryptoCurrency) } \ No newline at end of file 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 eb3ee645d1..0ad3b96933 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 @@ -59,6 +59,12 @@ internal class TokenDetailsStateFactory( ) } + private val refreshStateConverter by lazy { + TokenDetailsRefreshStateConverter( + currentStateProvider = currentStateProvider, + ) + } + fun getInitialState(cryptoCurrency: CryptoCurrency): TokenDetailsState { return skeletonStateConverter.convert( TokenDetailsSkeletonStateConverter.SkeletonModel(cryptoCurrency = cryptoCurrency), @@ -117,4 +123,12 @@ internal class TokenDetailsStateFactory( ), ) } + + fun getRefreshingState(): TokenDetailsState { + return refreshStateConverter.convert(true) + } + + fun getRefreshedState(): TokenDetailsState { + return refreshStateConverter.convert(false) + } } \ No newline at end of file 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 9dbb266f47..b47ae3c260 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 @@ -1,11 +1,12 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -29,12 +30,18 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.T import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock import kotlinx.collections.immutable.PersistentList +@OptIn(ExperimentalMaterialApi::class) @Composable internal fun TokenDetailsScreen(state: TokenDetailsState) { Scaffold( topBar = { TokenDetailsTopAppBar(config = state.topAppBarConfig) }, containerColor = TangemTheme.colors.background.secondary, ) { scaffoldPaddings -> + val pullRefreshState = rememberPullRefreshState( + refreshing = state.pullToRefreshConfig.isRefreshing, + onRefresh = state.pullToRefreshConfig.onRefresh, + ) + val txHistoryItems = if (state.txHistoryState is TxHistoryState.Content) { state.txHistoryState.contentItems.collectAsLazyPagingItems() } else { @@ -45,34 +52,46 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { val itemModifier = Modifier .padding(top = betweenItemsPadding) .padding(horizontal = horizontalPadding) - LazyColumn( + + Box( modifier = Modifier - .padding(paddingValues = scaffoldPaddings) - .fillMaxSize(), + .padding(scaffoldPaddings) + .pullRefresh(pullRefreshState), ) { - item { - TokenInfoBlock( - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing4) - .padding(horizontal = horizontalPadding), - state = state.tokenInfoBlockState, - ) - } - item { TokenDetailsBalanceBlock(modifier = itemModifier, state = state.tokenBalanceBlockState) } - item( - key = MarketPriceBlockState::class.java, - contentType = MarketPriceBlockState::class.java, - content = { MarketPriceBlock(modifier = itemModifier, state = state.marketPriceBlockState) }, - ) - if (state.txHistoryState is TxHistoryState.NotSupported && state.pendingTxs.isNotEmpty()) { + LazyColumn( + modifier = Modifier + .fillMaxSize(), + ) { item { - PendingTxsBlock( - pendingTxs = state.pendingTxs, - modifier = itemModifier, + TokenInfoBlock( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing4) + .padding(horizontal = horizontalPadding), + state = state.tokenInfoBlockState, ) } + item { TokenDetailsBalanceBlock(modifier = itemModifier, state = state.tokenBalanceBlockState) } + item( + key = MarketPriceBlockState::class.java, + contentType = MarketPriceBlockState::class.java, + content = { MarketPriceBlock(modifier = itemModifier, state = state.marketPriceBlockState) }, + ) + if (state.txHistoryState is TxHistoryState.NotSupported && state.pendingTxs.isNotEmpty()) { + item { + PendingTxsBlock( + pendingTxs = state.pendingTxs, + modifier = itemModifier, + ) + } + } + txHistoryItems(state = state.txHistoryState, txHistoryItems = txHistoryItems) } - txHistoryItems(state = state.txHistoryState, txHistoryItems = txHistoryItems) + + PullRefreshIndicator( + modifier = Modifier.align(Alignment.TopCenter), + refreshing = state.pullToRefreshConfig.isRefreshing, + state = pullRefreshState, + ) } TokenDetailsDialogs(state = state) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index b0917c5d2a..0e1eab9fc1 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -19,4 +19,6 @@ interface TokenDetailsClickIntents : TxHistoryClickIntents { fun onHideClick() fun onHideConfirmed() + + fun onRefreshSwipe() } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 27b52fe003..0af0c850bd 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -10,10 +10,7 @@ import com.tangem.common.Provider import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase -import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase -import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase -import com.tangem.domain.tokens.RemoveCurrencyUseCase +import com.tangem.domain.tokens.* import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.models.CryptoCurrency @@ -44,6 +41,7 @@ internal class TokenDetailsViewModel @Inject constructor( private val getSelectedWalletUseCase: GetSelectedWalletUseCase, private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, private val getExploreUrlUseCase: GetExploreUrlUseCase, @@ -60,6 +58,7 @@ internal class TokenDetailsViewModel @Inject constructor( var router by Delegates.notNull() private val marketPriceJobHolder = JobHolder() + private val refreshStateJobHolder = JobHolder() private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null private var wallet by Delegates.notNull() @@ -116,14 +115,16 @@ internal class TokenDetailsViewModel @Inject constructor( .saveIn(marketPriceJobHolder) } - private fun updateTxHistory() { + private fun updateTxHistory(refresh: Boolean = false) { viewModelScope.launch(dispatchers.io) { val txHistoryItemsCountEither = txHistoryItemsCountUseCase( networkId = cryptoCurrency.network.id, derivationPath = cryptoCurrency.derivationPath, ) - uiState = stateFactory.getLoadingTxHistoryState(itemsCountEither = txHistoryItemsCountEither) + if (!refresh) { + uiState = stateFactory.getLoadingTxHistoryState(itemsCountEither = txHistoryItemsCountEither) + } txHistoryItemsCountEither.onRight { uiState = stateFactory.getLoadedTxHistoryState( @@ -258,4 +259,19 @@ internal class TokenDetailsViewModel @Inject constructor( ) } } + + override fun onRefreshSwipe() { + uiState = stateFactory.getRefreshingState() + + viewModelScope.launch(dispatchers.io) { + fetchCurrencyStatusUseCase.invoke( + userWalletId = wallet.walletId, + id = cryptoCurrency.id, + refresh = true, + ) + updateTxHistory(refresh = true) + + uiState = stateFactory.getRefreshedState() + }.saveIn(refreshStateJobHolder) + } } \ No newline at end of file