From cd34a586bbfa6249042c1076fdd6013c43f85574 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 22 Aug 2023 18:49:27 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../intents/TxHistoryClickIntents.kt | 10 + .../tangem/domain/tokens/models/Network.kt | 6 +- features/tokendetails/impl/build.gradle.kts | 9 +- .../router/DefaultTokenDetailsRouter.kt | 4 + .../router/InnerTokenDetailsRouter.kt | 4 + .../tokendetails/TokenDetailsPreviewData.kt | 7 + .../tokendetails/state/TokenDetailsState.kt | 2 + .../TokenDetailsSkeletonStateConverter.kt | 7 + .../state/factory/TokenDetailsStateFactory.kt | 32 +++ .../TokenDetailsLoadedTxHistoryConverter.kt | 49 ++++ .../TokenDetailsLoadingTxHistoryConverter.kt | 59 +++++ .../TokenDetailsTxHistoryItemFlowConverter.kt | 223 ++++++++++++++++++ .../tokendetails/ui/TokenDetailsScreen.kt | 39 ++- .../viewmodels/TokenDetailsClickIntents.kt | 4 +- .../viewmodels/TokenDetailsViewModel.kt | 54 +++++ .../wallet/viewmodels/WalletClickIntents.kt | 9 +- 16 files changed, 496 insertions(+), 22 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/transactions/intents/TxHistoryClickIntents.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadedTxHistoryConverter.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/intents/TxHistoryClickIntents.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/intents/TxHistoryClickIntents.kt new file mode 100644 index 0000000000..4abce062be --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/intents/TxHistoryClickIntents.kt @@ -0,0 +1,10 @@ +package com.tangem.core.ui.components.transactions.intents + +interface TxHistoryClickIntents { + + fun onBuyClick() + + fun onReloadClick() + + fun onExploreClick() +} \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt index 05c043d7f1..4c60938e9c 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt @@ -31,9 +31,8 @@ data class Network( * * @property value The string representation of the network ID. */ - // FIXME: Remove serialization [REDACTED_JIRA] @JvmInline - value class ID(val value: String) : Serializable { + value class ID(val value: String) { init { require(value.isNotBlank()) { "Network ID must not be blank" } @@ -49,7 +48,8 @@ data class Network( * * @property name The human-readable name of the standard type. */ - sealed class StandardType { + // FIXME: Remove serialization [REDACTED_JIRA] + sealed class StandardType : Serializable { abstract val name: String /** Represents the ERC20 token standard, common on the Ethereum network. */ diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 6ce4bed811..55d58498b5 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -14,19 +14,22 @@ android { dependencies { /** AndroidX */ implementation(deps.androidx.activity.compose) + implementation(deps.androidx.paging.runtime) /** Compose */ - implementation(deps.compose.material) + implementation(deps.compose.accompanist.systemUiController) + implementation(deps.compose.coil) implementation(deps.compose.foundation) + implementation(deps.compose.material) implementation(deps.compose.material3) implementation(deps.compose.navigation) implementation(deps.compose.navigation.hilt) + implementation(deps.compose.paging) implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) - implementation(deps.compose.accompanist.systemUiController) - implementation(deps.compose.coil) implementation(deps.arrow.core) + implementation(deps.jodatime) implementation(deps.kotlin.immutable.collections) implementation(deps.tangem.blockchain) implementation(deps.tangem.card.core) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt index 73819028da..797a2d9194 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt @@ -14,4 +14,8 @@ internal class DefaultTokenDetailsRouter( override fun popBackStack() { navigationStateHolder.navigate(NavigationAction.PopBackTo()) } + + override fun openUrl(url: String) { + navigationStateHolder.navigate(NavigationAction.OpenUrl(url = url)) + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt index 89448deb0d..718ceb5821 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt @@ -4,5 +4,9 @@ import com.tangem.features.tokendetails.navigation.TokenDetailsRouter internal interface InnerTokenDetailsRouter : TokenDetailsRouter { + /** Pop back stack */ fun popBackStack() + + /** Open website by [url] */ + fun openUrl(url: String) } \ No newline at end of file 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 f5e6b62437..9122d3dfbd 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 @@ -2,6 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.extensions.TextReference import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState @@ -10,6 +11,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfo import com.tangem.features.tokendetails.impl.R import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.MutableStateFlow internal object TokenDetailsPreviewData { @@ -82,5 +84,10 @@ internal object TokenDetailsPreviewData { tokenInfoBlockState = tokenInfoBlockState, tokenBalanceBlockState = balanceLoading, marketPriceBlockState = marketPriceLoading, + txHistoryState = TxHistoryState.Content( + contentItems = MutableStateFlow( + value = TxHistoryState.getDefaultLoadingTransactions {}, + ), + ), ) } \ 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 8c7ba02c14..c3902dc328 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 @@ -1,10 +1,12 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.state.TxHistoryState data class TokenDetailsState( val topAppBarConfig: TokenDetailsTopAppBarConfig, val tokenInfoBlockState: TokenInfoBlockState, val tokenBalanceBlockState: TokenDetailsBalanceBlockState, val marketPriceBlockState: MarketPriceBlockState, + val txHistoryState: TxHistoryState, ) \ 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 5a829656e3..880da1991e 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 @@ -1,6 +1,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState @@ -11,6 +12,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.T import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.features.tokendetails.impl.R import com.tangem.utils.converter.Converter +import kotlinx.coroutines.flow.MutableStateFlow internal class TokenDetailsSkeletonStateConverter( private val clickIntents: TokenDetailsClickIntents, @@ -39,6 +41,11 @@ internal class TokenDetailsSkeletonStateConverter( TokenDetailsPreviewData.disabledActionButtons, ), marketPriceBlockState = MarketPriceBlockState.Loading(value.cryptoCurrency.name), + txHistoryState = TxHistoryState.Content( + contentItems = MutableStateFlow( + value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), + ), + ), ) } 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 b43c200507..09c5094a0e 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 @@ -1,18 +1,27 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory +import androidx.paging.PagingData import arrow.core.Either import com.tangem.common.Provider import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.error.CurrencyError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.txhistory.models.TxHistoryListError +import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadedTxHistoryConverter +import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents +import kotlinx.coroutines.flow.Flow internal class TokenDetailsStateFactory( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, private val clickIntents: TokenDetailsClickIntents, + symbol: String, + decimals: Int, ) { private val skeletonStateConverter by lazy { @@ -26,6 +35,19 @@ internal class TokenDetailsStateFactory( ) } + private val loadingTransactionsStateConverter by lazy { + TokenDetailsLoadingTxHistoryConverter(currentStateProvider = currentStateProvider, clickIntents = clickIntents) + } + + private val loadedTxHistoryConverter by lazy { + TokenDetailsLoadedTxHistoryConverter( + currentStateProvider = currentStateProvider, + clickIntents = clickIntents, + symbol = symbol, + decimals = decimals, + ) + } + fun getInitialState(cryptoCurrency: CryptoCurrency): TokenDetailsState { return skeletonStateConverter.convert( TokenDetailsSkeletonStateConverter.SkeletonModel(cryptoCurrency = cryptoCurrency), @@ -37,4 +59,14 @@ internal class TokenDetailsStateFactory( ): TokenDetailsState { return tokenDetailsLoadedBalanceConverter.convert(cryptoCurrencyEither) } + + fun getLoadingTxHistoryState(itemsCountEither: Either): TokenDetailsState { + return loadingTransactionsStateConverter.convert(value = itemsCountEither) + } + + fun getLoadedTxHistoryState( + txHistoryEither: Either>>, + ): TokenDetailsState { + return loadedTxHistoryConverter.convert(txHistoryEither) + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadedTxHistoryConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadedTxHistoryConverter.kt new file mode 100644 index 0000000000..f9146ee1d3 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadedTxHistoryConverter.kt @@ -0,0 +1,49 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory + +import androidx.paging.PagingData +import arrow.core.Either +import com.tangem.common.Provider +import com.tangem.core.ui.components.transactions.intents.TxHistoryClickIntents +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.txhistory.models.TxHistoryListError +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.utils.converter.Converter +import kotlinx.coroutines.flow.Flow + +internal class TokenDetailsLoadedTxHistoryConverter( + private val currentStateProvider: Provider, + private val clickIntents: TxHistoryClickIntents, + symbol: String, + decimals: Int, +) : Converter>>, TokenDetailsState> { + + private val txHistoryItemFlowConverter by lazy { + TokenDetailsTxHistoryItemFlowConverter( + currentStateProvider = currentStateProvider, + symbol = symbol, + decimals = decimals, + clickIntents = clickIntents, + ) + } + + override fun convert(value: Either>>): TokenDetailsState { + return value.fold(ifLeft = ::convertError, ifRight = ::convert) + } + + private fun convertError(error: TxHistoryListError): TokenDetailsState { + return currentStateProvider().copy( + txHistoryState = when (error) { + is TxHistoryListError.DataError -> { + TxHistoryState.Error(onReloadClick = clickIntents::onReloadClick) + } + }, + ) + } + + private fun convert(items: Flow>): TokenDetailsState { + return currentStateProvider().copy( + txHistoryState = txHistoryItemFlowConverter.convert(value = items), + ) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt new file mode 100644 index 0000000000..9f98c1ccf3 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt @@ -0,0 +1,59 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory + +import androidx.paging.PagingData +import arrow.core.Either +import com.tangem.common.Provider +import com.tangem.core.ui.components.transactions.intents.TxHistoryClickIntents +import com.tangem.core.ui.components.transactions.state.TransactionState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.domain.txhistory.models.TxHistoryStateError +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.utils.converter.Converter +import kotlinx.coroutines.flow.update + +internal class TokenDetailsLoadingTxHistoryConverter( + private val currentStateProvider: Provider, + private val clickIntents: TxHistoryClickIntents, +) : Converter, TokenDetailsState> { + + override fun convert(value: Either): TokenDetailsState { + return value.fold(ifLeft = ::convertError, ifRight = ::convert) + } + + private fun convertError(error: TxHistoryStateError): TokenDetailsState { + return currentStateProvider().copy( + txHistoryState = when (error) { + is TxHistoryStateError.EmptyTxHistories -> { + TxHistoryState.Empty(onBuyClick = clickIntents::onBuyClick) + } + is TxHistoryStateError.DataError -> { + TxHistoryState.Error(onReloadClick = clickIntents::onReloadClick) + } + is TxHistoryStateError.TxHistoryNotImplemented -> { + TxHistoryState.NotSupported(onExploreClick = clickIntents::onExploreClick) + } + }, + ) + } + + private fun convert(value: Int): TokenDetailsState { + val state = currentStateProvider() + val txHistoryContent = state.txHistoryState as TxHistoryState.Content + + txHistoryContent.contentItems.update { + PagingData.from( + data = listOf(TxHistoryState.TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick)) + + MutableList( + size = value, + init = { + TxHistoryState.TxHistoryItemState.Transaction( + state = TransactionState.Loading(it.toString()), + ) + }, + ), + ) + } + + return state + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt new file mode 100644 index 0000000000..eafd6c9a31 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt @@ -0,0 +1,223 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory + +import android.text.format.DateUtils +import androidx.paging.* +import com.tangem.common.Provider +import com.tangem.core.ui.components.transactions.intents.TxHistoryClickIntents +import com.tangem.core.ui.components.transactions.state.TransactionState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.isToday +import com.tangem.utils.extensions.isYesterday +import com.tangem.utils.toBriefAddressFormat +import com.tangem.utils.toFormattedCurrencyString +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.* +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import org.joda.time.format.DateTimeFormatterBuilder +import java.math.BigDecimal +import java.util.Locale + +internal class TokenDetailsTxHistoryItemFlowConverter( + private val currentStateProvider: Provider, + private val symbol: String, + private val decimals: Int, + private val clickIntents: TxHistoryClickIntents, +) : Converter>, TxHistoryState> { + + /** Example, 2 Aug, 2023 */ + private val dateFormatter by lazy { + DateTimeFormatterBuilder() + .appendDayOfMonth(1) + .appendLiteral(' ') + .appendMonthOfYearShortText() + .appendLiteral(", ") + .appendYear(4, 4) + .toFormatter() + .withLocale(Locale.getDefault()) + } + + /** Example, 13:35 */ + private val timeFormatter by lazy { + DateTimeFormatterBuilder() + .appendHourOfDay(1) + .appendLiteral(':') + .appendMinuteOfHour(2) + .toFormatter() + .withLocale(Locale.getDefault()) + } + + override fun convert(value: Flow>): TxHistoryState { + val txHistoryContent = currentStateProvider().txHistoryState as TxHistoryState.Content + + // FIXME: TxHistoryRepository should send loading transactions + // [REDACTED_JIRA] + value + .onEach { txHistoryStatePagingData -> + txHistoryContent.contentItems.update { + txHistoryStatePagingData + .map { item -> + // [createTransactionState] returns timestamp without formatting + TxHistoryItemState.Transaction(state = createTransactionState(item)) + } + .insertHeaderItem( + terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE, + item = TxHistoryItemState.Title(clickIntents::onExploreClick), + ) + .insertGroupTitle() // method uses the raw timestamp + .formatTransactionsTimestamp() // method formats the timestamp + } + } + .launchIn(CoroutineScope(Dispatchers.IO)) + + return txHistoryContent + } + + private fun createTransactionState(item: TxHistoryItem): TransactionState { + return when (item.type) { + TxHistoryItem.TransactionType.Transfer -> { + when (val direction = item.direction) { + is TxHistoryItem.TransactionDirection.Incoming -> { + createIncomingTransferTransaction(item, direction) + } + is TxHistoryItem.TransactionDirection.Outgoing -> { + createOutgoingTransferTransaction(item, direction) + } + } + } + } + } + + private fun createIncomingTransferTransaction( + item: TxHistoryItem, + direction: TxHistoryItem.TransactionDirection.Incoming, + ): TransactionState { + return when (item.status) { + TxHistoryItem.TxStatus.Confirmed -> TransactionState.Receive( + txHash = item.txHash, + address = direction.from.toBriefAddressFormat(), + amount = item.amount.toCryptoCurrencyFormat(), + timestamp = item.getRawTimestamp(), + ) + TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Receiving( + txHash = item.txHash, + address = direction.from.toBriefAddressFormat(), + amount = item.amount.toCryptoCurrencyFormat(), + timestamp = item.getRawTimestamp(), + ) + } + } + + private fun createOutgoingTransferTransaction( + item: TxHistoryItem, + direction: TxHistoryItem.TransactionDirection.Outgoing, + ): TransactionState { + return when (item.status) { + TxHistoryItem.TxStatus.Confirmed -> TransactionState.Send( + txHash = item.txHash, + address = direction.to.toBriefAddressFormat(), + amount = item.amount.toCryptoCurrencyFormat(), + timestamp = item.getRawTimestamp(), + ) + TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Sending( + txHash = item.txHash, + address = direction.to.toBriefAddressFormat(), + amount = item.amount.toCryptoCurrencyFormat(), + timestamp = item.getRawTimestamp(), + ) + } + } + + private fun BigDecimal.toCryptoCurrencyFormat(): String { + return toFormattedCurrencyString(currency = symbol, decimals = decimals) + } + + private fun PagingData.insertGroupTitle(): PagingData { + return insertSeparators(terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE) { before, after -> + // Use raw timestamp to get date + + // If [afterDate] is the first transaction in the flow, add the group title + val afterDate = after.getTimestamp()?.toDateFormat() ?: return@insertSeparators null + if (before is TxHistoryItemState.Title) { + return@insertSeparators TxHistoryItemState.GroupTitle(afterDate) + } + + /* + * If [beforeDate] is not equals to [afterDate], then [afterDate] is first transaction in + * the new group + */ + val beforeDate = before.getTimestamp()?.toDateFormat() ?: return@insertSeparators null + return@insertSeparators if (beforeDate != afterDate) { + TxHistoryItemState.GroupTitle(afterDate) + } else { + null + } + } + } + + /** + * Map the [PagingData] to format the [TxHistoryItemState] timestamp + */ + private fun PagingData.formatTransactionsTimestamp(): PagingData { + return map { txHistoryItemState -> + if (txHistoryItemState is TxHistoryItemState.Transaction && + txHistoryItemState.state is TransactionState.Content + ) { + val txContent = txHistoryItemState.state as TransactionState.Content + txHistoryItemState.copy( + state = txContent.copySealed( + timestamp = txContent.timestamp.toTimeFormat(), + ), + ) + } else { + txHistoryItemState + } + } + } + + /** + * Get timestamp without formatting. + * It's life hack that help us to add transaction's group title to flow. + * + * @see [convert] + */ + private fun TxHistoryItem.getRawTimestamp() = this.timestampInMillis.toString() + + private fun TxHistoryItemState?.getTimestamp(): Long? { + return if (this is TxHistoryItemState.Transaction && this.state is TransactionState.Content) { + val txContent = this.state as TransactionState.Content + requireNotNull(txContent.timestamp.toLongOrNull()) { "Timestamp must be Long type" } + } else { + null + } + } + + /** + * If [this] timestamp is today or yesterday, returns relative date, + * otherwise returns formatting date by [dateFormatter] + */ + private fun Long.toDateFormat(): String { + val localDate = DateTime(this, DateTimeZone.getDefault()) + return if (localDate.isToday() || localDate.isYesterday()) { + DateUtils.getRelativeTimeSpanString( + this, + DateTime.now().millis, + DateUtils.DAY_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE, + ).toString() + } else { + dateFormatter.print(localDate) + } + } + + private fun String.toTimeFormat(): String { + return timeFormatter.print( + DateTime(this.toLong(), DateTimeZone.getDefault()), + ) + } +} \ 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 a8a20dfa34..a54eef8db7 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,14 +1,17 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui -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.lazy.LazyColumn import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import androidx.paging.compose.collectAsLazyPagingItems import com.tangem.core.ui.components.marketprice.MarketPriceBlock +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.components.transactions.txHistoryItems import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState @@ -22,16 +25,36 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { topBar = { TokenDetailsTopAppBar(config = state.topAppBarConfig) }, containerColor = TangemTheme.colors.background.secondary, ) { scaffoldPaddings -> - Column( + val txHistoryItems = if (state.txHistoryState is TxHistoryState.Content) { + state.txHistoryState.contentItems.collectAsLazyPagingItems() + } else { + null + } + val betweenItemsPadding = TangemTheme.dimens.spacing12 + val horizontalPadding = TangemTheme.dimens.spacing16 + val itemModifier = Modifier + .padding(top = betweenItemsPadding) + .padding(horizontal = horizontalPadding) + LazyColumn( modifier = Modifier .padding(paddingValues = scaffoldPaddings) - .padding(horizontal = TangemTheme.dimens.spacing16) .fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { - TokenInfoBlock(state = state.tokenInfoBlockState) - TokenDetailsBalanceBlock(state = state.tokenBalanceBlockState) - MarketPriceBlock(state = state.marketPriceBlockState) + 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) }, + ) + txHistoryItems(state = state.txHistoryState, txHistoryItems = txHistoryItems) } } } 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 e2735c652c..4f1008ac8c 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 @@ -1,6 +1,8 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels -interface TokenDetailsClickIntents { +import com.tangem.core.ui.components.transactions.intents.TxHistoryClickIntents + +interface TokenDetailsClickIntents : TxHistoryClickIntents { fun onBackClick() 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 4f3b42bdda..a417d04cf8 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 @@ -4,13 +4,17 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.* +import androidx.paging.cachedIn import arrow.core.getOrElse import com.tangem.common.Provider import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.GetCurrencyUseCase import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState @@ -21,15 +25,20 @@ import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import javax.inject.Inject import kotlin.properties.Delegates +@Suppress("LongParameterList") @HiltViewModel internal class TokenDetailsViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val getSelectedWalletUseCase: GetSelectedWalletUseCase, private val getCurrencyUseCase: GetCurrencyUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, + private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, + private val getExploreUrlUseCase: GetExploreUrlUseCase, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents { @@ -45,6 +54,8 @@ internal class TokenDetailsViewModel @Inject constructor( currentStateProvider = Provider { uiState }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), clickIntents = this, + symbol = cryptoCurrency.symbol, + decimals = cryptoCurrency.decimals, ) var uiState: TokenDetailsState by mutableStateOf(stateFactory.getInitialState(cryptoCurrency)) private set @@ -63,6 +74,7 @@ internal class TokenDetailsViewModel @Inject constructor( private fun updateContent(selectedWallet: UserWallet, refresh: Boolean) { updateMarketPrice(selectedWallet = selectedWallet, refresh = refresh) + updateTxHistory() } private fun updateMarketPrice(selectedWallet: UserWallet, refresh: Boolean) { @@ -74,6 +86,28 @@ internal class TokenDetailsViewModel @Inject constructor( .saveIn(marketPriceJobHolder) } + private fun updateTxHistory() { + viewModelScope.launch(dispatchers.io) { + val txHistoryItemsCountEither = txHistoryItemsCountUseCase( + networkId = cryptoCurrency.network.id, + derivationPath = cryptoCurrency.derivationPath, + ) + + uiState = stateFactory.getLoadingTxHistoryState(itemsCountEither = txHistoryItemsCountEither) + + txHistoryItemsCountEither.onRight { + uiState = stateFactory.getLoadedTxHistoryState( + txHistoryEither = txHistoryItemsUseCase( + networkId = cryptoCurrency.network.id, + derivationPath = cryptoCurrency.derivationPath, + ).map { + it.cachedIn(viewModelScope) + }, + ) + } + } + } + private fun createSelectedAppCurrencyFlow(): StateFlow { return getSelectedAppCurrencyUseCase() .map { maybeAppCurrency -> @@ -93,4 +127,24 @@ internal class TokenDetailsViewModel @Inject constructor( override fun onMoreClick() { TODO("Not yet implemented") } + + override fun onBuyClick() { + // TODO: [REDACTED_JIRA] + } + + override fun onReloadClick() { + updateTxHistory() + } + + override fun onExploreClick() { + viewModelScope.launch { + val wallet = getWallet() + router.openUrl( + url = getExploreUrlUseCase( + userWalletId = wallet.walletId, + networkId = cryptoCurrency.network.id, + ), + ) + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt index 2dcc2ddc06..48d9423033 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt @@ -1,8 +1,9 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels +import com.tangem.core.ui.components.transactions.intents.TxHistoryClickIntents import com.tangem.domain.tokens.models.CryptoCurrency -internal interface WalletClickIntents { +internal interface WalletClickIntents : TxHistoryClickIntents { fun onBackClick() @@ -28,12 +29,6 @@ internal interface WalletClickIntents { fun onOrganizeTokensClick() - fun onBuyClick() - - fun onReloadClick() - - fun onExploreClick() - fun onUnlockWalletClick() fun onUnlockWalletNotificationClick()