Updated on 2026-08-14
This commit is contained in:
parent
55641cab14
commit
cd34a586bb
16 changed files with 496 additions and 22 deletions
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.core.ui.components.transactions.intents
|
||||
|
||||
interface TxHistoryClickIntents {
|
||||
|
||||
fun onBuyClick()
|
||||
|
||||
fun onReloadClick()
|
||||
|
||||
fun onExploreClick()
|
||||
}
|
||||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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 {},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<TokenDetailsState>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
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<TxHistoryStateError, Int>): TokenDetailsState {
|
||||
return loadingTransactionsStateConverter.convert(value = itemsCountEither)
|
||||
}
|
||||
|
||||
fun getLoadedTxHistoryState(
|
||||
txHistoryEither: Either<TxHistoryListError, Flow<PagingData<TxHistoryItem>>>,
|
||||
): TokenDetailsState {
|
||||
return loadedTxHistoryConverter.convert(txHistoryEither)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TokenDetailsState>,
|
||||
private val clickIntents: TxHistoryClickIntents,
|
||||
symbol: String,
|
||||
decimals: Int,
|
||||
) : Converter<Either<TxHistoryListError, Flow<PagingData<TxHistoryItem>>>, TokenDetailsState> {
|
||||
|
||||
private val txHistoryItemFlowConverter by lazy {
|
||||
TokenDetailsTxHistoryItemFlowConverter(
|
||||
currentStateProvider = currentStateProvider,
|
||||
symbol = symbol,
|
||||
decimals = decimals,
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
}
|
||||
|
||||
override fun convert(value: Either<TxHistoryListError, Flow<PagingData<TxHistoryItem>>>): 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<PagingData<TxHistoryItem>>): TokenDetailsState {
|
||||
return currentStateProvider().copy(
|
||||
txHistoryState = txHistoryItemFlowConverter.convert(value = items),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TokenDetailsState>,
|
||||
private val clickIntents: TxHistoryClickIntents,
|
||||
) : Converter<Either<TxHistoryStateError, Int>, TokenDetailsState> {
|
||||
|
||||
override fun convert(value: Either<TxHistoryStateError, Int>): 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TokenDetailsState>,
|
||||
private val symbol: String,
|
||||
private val decimals: Int,
|
||||
private val clickIntents: TxHistoryClickIntents,
|
||||
) : Converter<Flow<PagingData<TxHistoryItem>>, 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<PagingData<TxHistoryItem>>): TxHistoryState {
|
||||
val txHistoryContent = currentStateProvider().txHistoryState as TxHistoryState.Content
|
||||
|
||||
// FIXME: TxHistoryRepository should send loading transactions
|
||||
// [REDACTED_JIRA]
|
||||
value
|
||||
.onEach { txHistoryStatePagingData ->
|
||||
txHistoryContent.contentItems.update {
|
||||
txHistoryStatePagingData
|
||||
.map<TxHistoryItem, TxHistoryItemState> { 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<TxHistoryItemState>.insertGroupTitle(): PagingData<TxHistoryItemState> {
|
||||
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<TxHistoryItemState>.formatTransactionsTimestamp(): PagingData<TxHistoryItemState> {
|
||||
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()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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<AppCurrency> {
|
||||
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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue