Updated on 2026-08-14
This commit is contained in:
commit
f2e19e40be
35 changed files with 1034 additions and 370 deletions
|
|
@ -41,6 +41,7 @@ import com.tangem.tap.common.chat.ChatManager
|
|||
import com.tangem.tap.common.feedback.AdditionalFeedbackInfo
|
||||
import com.tangem.tap.common.feedback.FeedbackManager
|
||||
import com.tangem.tap.common.images.createCoilImageLoader
|
||||
import com.tangem.tap.common.log.TimberFormatStrategy
|
||||
import com.tangem.tap.common.log.TangemLogCollector
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.appReducer
|
||||
|
|
@ -189,7 +190,7 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
)
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Logger.addLogAdapter(AndroidLogAdapter())
|
||||
Logger.addLogAdapter(AndroidLogAdapter(TimberFormatStrategy()))
|
||||
Timber.plant(
|
||||
object : Timber.DebugTree() {
|
||||
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
package com.tangem.tap.common.log
|
||||
|
||||
import com.orhanobut.logger.FormatStrategy
|
||||
import com.orhanobut.logger.LogStrategy
|
||||
import com.orhanobut.logger.LogcatLogStrategy
|
||||
|
||||
class TimberFormatStrategy : FormatStrategy {
|
||||
|
||||
private val logStrategy: LogStrategy = LogcatLogStrategy()
|
||||
|
||||
override fun log(priority: Int, tag: String?, message: String) {
|
||||
logTopBorder(priority, tag)
|
||||
val bytes = message.toByteArray()
|
||||
val length = bytes.size
|
||||
if (length <= CHUNK_SIZE) {
|
||||
logContent(priority, tag, message)
|
||||
logBottomBorder(priority, tag)
|
||||
return
|
||||
}
|
||||
var i = 0
|
||||
while (i < length) {
|
||||
val count = (length - i).coerceAtMost(CHUNK_SIZE)
|
||||
// create a new String with system's default charset (which is UTF-8 for Android)
|
||||
logContent(priority, tag, String(bytes, i, count))
|
||||
i += CHUNK_SIZE
|
||||
}
|
||||
logBottomBorder(priority, tag)
|
||||
}
|
||||
|
||||
private fun logTopBorder(logType: Int, tag: String?) {
|
||||
logChunk(logType, tag, TOP_BORDER)
|
||||
}
|
||||
|
||||
private fun logBottomBorder(logType: Int, tag: String?) {
|
||||
logChunk(logType, tag, BOTTOM_BORDER)
|
||||
}
|
||||
|
||||
private fun logContent(logType: Int, tag: String?, chunk: String) {
|
||||
chunk.split(System.lineSeparator()).forEach { line ->
|
||||
logChunk(logType, tag, "$HORIZONTAL_LINE $line")
|
||||
}
|
||||
}
|
||||
|
||||
private fun logChunk(priority: Int, tag: String?, chunk: String) {
|
||||
logStrategy.log(priority, tag, chunk)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
/**
|
||||
* Android's max limit for a log entry is ~4076 bytes,
|
||||
* so 4000 bytes is used as chunk size since default charset
|
||||
* is UTF-8
|
||||
*/
|
||||
private const val CHUNK_SIZE = 4000
|
||||
|
||||
const val TOP_LEFT_CORNER = "┌"
|
||||
const val BOTTOM_LEFT_CORNER = "└"
|
||||
const val HORIZONTAL_LINE = "│"
|
||||
const val DOUBLE_DIVIDER = "────────────────────────────────────────────────────────"
|
||||
const val TOP_BORDER = TOP_LEFT_CORNER + DOUBLE_DIVIDER + DOUBLE_DIVIDER
|
||||
const val BOTTOM_BORDER = BOTTOM_LEFT_CORNER + DOUBLE_DIVIDER + DOUBLE_DIVIDER
|
||||
}
|
||||
}
|
||||
|
|
@ -37,5 +37,8 @@ fun createNetworkLoggingInterceptor(): Interceptor {
|
|||
return LoggingInterceptor.Builder()
|
||||
.setLevel(Level.BODY)
|
||||
.log(Log.VERBOSE)
|
||||
.tag(NETWORK_LOGS_TAG)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
private const val NETWORK_LOGS_TAG = "NetworkLogs"
|
||||
|
|
@ -1,76 +1,67 @@
|
|||
package com.tangem.core.ui.components.transactions.state
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.TerminalSeparatorType
|
||||
import androidx.paging.insertHeaderItem
|
||||
import com.tangem.core.ui.components.wallet.WalletLockedContentState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
/**
|
||||
* Wallet transaction history state
|
||||
*/
|
||||
/** Wallet transaction history state */
|
||||
sealed interface TxHistoryState {
|
||||
|
||||
/**
|
||||
* Wallet transaction history state with content
|
||||
* Wallet transaction history state with content. Items contains a required [TxHistoryItemState.Title].
|
||||
*
|
||||
* @property items content items
|
||||
* @property contentItems content items
|
||||
*/
|
||||
sealed class ContentState(open val items: Flow<PagingData<TxHistoryItemState>>) : TxHistoryState
|
||||
sealed class ContentState(private val contentItems: Flow<PagingData<TxHistoryItemState>>) : TxHistoryState {
|
||||
|
||||
/** Lambda be invoke when explore button was clicked */
|
||||
abstract val onExploreClick: () -> Unit
|
||||
|
||||
/** Content items with [TxHistoryItemState.Title] */
|
||||
val items: Flow<PagingData<TxHistoryItemState>>
|
||||
get() {
|
||||
return contentItems.map {
|
||||
it.insertHeaderItem(
|
||||
terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE,
|
||||
item = TxHistoryItemState.Title(onExploreClick = onExploreClick),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loading state
|
||||
*
|
||||
* @property onExploreClick lambda be invoke when explore button was clicked
|
||||
* @property transactions loading transactions
|
||||
*/
|
||||
data class Loading(val onExploreClick: () -> Unit) : ContentState(
|
||||
items = flowOf(
|
||||
PagingData.from(
|
||||
listOf(
|
||||
TxHistoryItemState.Title(onExploreClick = onExploreClick),
|
||||
TxHistoryItemState.Transaction(state = TransactionState.Loading(txHash = LOADING_TX_HASH)),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
/**
|
||||
* Wallet transaction history state with loading transactions
|
||||
*
|
||||
* @property itemsCount count of loading transactions
|
||||
*/
|
||||
data class ContentWithLoadingItems(val itemsCount: Int) : ContentState(
|
||||
items = flowOf(
|
||||
value = PagingData.from(
|
||||
data = buildList(capacity = itemsCount) {
|
||||
add(TxHistoryItemState.Transaction(state = TransactionState.Loading(txHash = LOADING_TX_HASH)))
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
data class Loading(
|
||||
override val onExploreClick: () -> Unit,
|
||||
val transactions: Flow<PagingData<TxHistoryItemState>> = getDefaultLoadingTransactions(),
|
||||
) : ContentState(transactions)
|
||||
|
||||
/**
|
||||
* Wallet transaction history state with content
|
||||
*
|
||||
* @property items content items
|
||||
* @property onExploreClick lambda be invoke when explore button was clicked
|
||||
* @property contentItems content items
|
||||
*/
|
||||
data class Content(override val items: Flow<PagingData<TxHistoryItemState>>) : ContentState(items)
|
||||
data class Content(
|
||||
override val onExploreClick: () -> Unit,
|
||||
val contentItems: Flow<PagingData<TxHistoryItemState>>,
|
||||
) : ContentState(contentItems)
|
||||
|
||||
/**
|
||||
* Locked state
|
||||
*
|
||||
* @property onExploreClick lambda be invoke when explore button was clicked
|
||||
*/
|
||||
data class Locked(val onExploreClick: () -> Unit) :
|
||||
ContentState(
|
||||
items = flowOf(
|
||||
PagingData.from(
|
||||
listOf(
|
||||
TxHistoryItemState.Title(onExploreClick = onExploreClick),
|
||||
TxHistoryItemState.Transaction(state = TransactionState.Loading(txHash = LOADING_TX_HASH)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
data class Locked(override val onExploreClick: () -> Unit) :
|
||||
ContentState(contentItems = getDefaultLoadingTransactions()),
|
||||
WalletLockedContentState
|
||||
|
||||
/**
|
||||
|
|
@ -121,5 +112,17 @@ sealed interface TxHistoryState {
|
|||
|
||||
private companion object {
|
||||
const val LOADING_TX_HASH = "LOADING_TX_HASH"
|
||||
|
||||
private fun getDefaultLoadingTransactions(): Flow<PagingData<TxHistoryItemState>> {
|
||||
return flowOf(
|
||||
value = PagingData.from(
|
||||
data = listOf(
|
||||
element = TxHistoryItemState.Transaction(
|
||||
state = TransactionState.Loading(txHash = LOADING_TX_HASH),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -20,12 +20,19 @@ sealed interface TextReference {
|
|||
* Text resource id
|
||||
*
|
||||
* @property id resource id
|
||||
* @property formatArgs arguments
|
||||
*
|
||||
* Impossible to use [kotlinx.collections.immutable.ImmutableList] because [Any] is unstable.
|
||||
* @property formatArgs arguments. Impossible to use [kotlinx.collections.immutable.ImmutableList] because [Any] is
|
||||
* unstable.
|
||||
*/
|
||||
data class Res(@StringRes val id: Int, val formatArgs: WrappedList<Any> = WrappedList(emptyList())) : TextReference
|
||||
|
||||
/**
|
||||
* Plural resource id
|
||||
*
|
||||
* @property id resource id
|
||||
* @property count count
|
||||
* @property formatArgs arguments. Impossible to use [kotlinx.collections.immutable.ImmutableList] because [Any] is
|
||||
* unstable.
|
||||
*/
|
||||
data class PluralRes(@PluralsRes val id: Int, val count: Int, val formatArgs: WrappedList<Any>) : TextReference
|
||||
|
||||
/**
|
||||
|
|
@ -34,9 +41,16 @@ sealed interface TextReference {
|
|||
* @property value value
|
||||
*/
|
||||
data class Str(val value: String) : TextReference
|
||||
|
||||
/**
|
||||
* Combined reference. It concatenates all [refs].
|
||||
*
|
||||
* @see [TextReference.plus] method
|
||||
*/
|
||||
data class Combined(val refs: WrappedList<TextReference>) : TextReference
|
||||
}
|
||||
|
||||
/** Get text */
|
||||
/** Resolve [TextReference] as [String] */
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
fun TextReference.resolveReference(): String {
|
||||
|
|
@ -44,5 +58,23 @@ fun TextReference.resolveReference(): String {
|
|||
is TextReference.Res -> stringResource(id, *formatArgs.toTypedArray())
|
||||
is TextReference.PluralRes -> pluralStringResource(id, count, *formatArgs.toTypedArray())
|
||||
is TextReference.Str -> value
|
||||
is TextReference.Combined -> {
|
||||
buildString {
|
||||
refs.forEach {
|
||||
append(it.resolveReference())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Concatenate [this] reference with [ref] */
|
||||
operator fun TextReference.plus(ref: TextReference): TextReference {
|
||||
return when (this) {
|
||||
is TextReference.Combined -> copy(refs = (refs.data + ref).toWrappedList())
|
||||
is TextReference.PluralRes,
|
||||
is TextReference.Res,
|
||||
is TextReference.Str,
|
||||
-> TextReference.Combined(refs = wrappedList(this, ref))
|
||||
}
|
||||
}
|
||||
|
|
@ -7,4 +7,8 @@ import androidx.compose.runtime.Immutable
|
|||
*/
|
||||
@JvmInline
|
||||
@Immutable
|
||||
value class WrappedList<T>(val data: List<T>) : List<T> by data
|
||||
value class WrappedList<T>(val data: List<T>) : List<T> by data
|
||||
|
||||
fun <T> List<T>.toWrappedList(): WrappedList<T> = WrappedList(data = this)
|
||||
|
||||
fun <T> wrappedList(vararg elements: T): WrappedList<T> = WrappedList(data = listOf(*elements))
|
||||
|
|
@ -12,22 +12,14 @@ object BigDecimalFormatter {
|
|||
|
||||
private const val TEMP_CURRENCY_CODE = "USD"
|
||||
|
||||
fun formatCryptoAmount(
|
||||
cryptoAmount: BigDecimal,
|
||||
cryptoCurrency: String,
|
||||
decimals: Int,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
val formatterCurrency = getCurrency(cryptoCurrency)
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = formatterCurrency
|
||||
fun formatCryptoAmount(cryptoAmount: BigDecimal, cryptoCurrency: String, decimals: Int): String {
|
||||
val formatter = NumberFormat.getNumberInstance().apply {
|
||||
maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8)
|
||||
minimumFractionDigits = 2
|
||||
roundingMode = RoundingMode.DOWN
|
||||
}
|
||||
|
||||
return formatter.format(cryptoAmount)
|
||||
.replace(formatterCurrency.getSymbol(locale), cryptoCurrency)
|
||||
return formatter.format(cryptoAmount) + "\u2009$cryptoCurrency"
|
||||
}
|
||||
|
||||
fun formatFiatAmount(
|
||||
|
|
|
|||
|
|
@ -26,21 +26,34 @@ dependencies {
|
|||
implementation(deps.compose.accompanist.systemUiController)
|
||||
implementation(deps.compose.coil)
|
||||
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.tangem.blockchain)
|
||||
implementation(deps.tangem.card.core)
|
||||
implementation(deps.timber)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.common)
|
||||
implementation(projects.core.featuretoggles)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
/** Domain modules */
|
||||
implementation(projects.domain.appCurrency)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.txhistory)
|
||||
implementation(projects.domain.txhistory.models)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
/** Feature Apis */
|
||||
implementation(projects.features.tokendetails.api)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.tangem.core.ui.components.SystemBarsEffect
|
||||
|
|
@ -38,6 +39,7 @@ internal class TokenDetailsFragment : Fragment() {
|
|||
|
||||
val viewModel = hiltViewModel<TokenDetailsViewModel>()
|
||||
viewModel.router = this@TokenDetailsFragment.internalTokenDetailsRouter
|
||||
LocalLifecycleOwner.current.lifecycle.addObserver(viewModel)
|
||||
TokenDetailsScreen(state = viewModel.uiState)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ 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.marketprice.PriceChangeConfig
|
||||
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
|
||||
|
|
@ -41,7 +40,8 @@ internal object TokenDetailsPreviewData {
|
|||
),
|
||||
)
|
||||
|
||||
private val actionButtons = persistentListOf(
|
||||
// TODO: [REDACTED_JIRA]
|
||||
val actionButtons = persistentListOf(
|
||||
ActionButtonConfig(
|
||||
text = TextReference.Str(value = "Buy"),
|
||||
iconResId = R.drawable.ic_plus_24,
|
||||
|
|
@ -64,7 +64,8 @@ internal object TokenDetailsPreviewData {
|
|||
),
|
||||
)
|
||||
|
||||
private val disabledActionButtons = actionButtons.map { it.copy(enabled = false) }.toPersistentList()
|
||||
// TODO: [REDACTED_JIRA]
|
||||
val disabledActionButtons = actionButtons.map { it.copy(enabled = false) }.toPersistentList()
|
||||
|
||||
val balanceLoading = TokenDetailsBalanceBlockState.Loading(actionButtons = disabledActionButtons)
|
||||
val balanceContent = TokenDetailsBalanceBlockState.Content(
|
||||
|
|
@ -74,15 +75,6 @@ internal object TokenDetailsPreviewData {
|
|||
)
|
||||
val balanceError = TokenDetailsBalanceBlockState.Error(actionButtons = disabledActionButtons)
|
||||
|
||||
val marketPriceContent = MarketPriceBlockState.Content(
|
||||
currencyName = "USDT",
|
||||
price = "98900 $",
|
||||
priceChangeConfig = PriceChangeConfig(
|
||||
valueInPercent = "10.89%",
|
||||
type = PriceChangeConfig.Type.UP,
|
||||
),
|
||||
)
|
||||
|
||||
private val marketPriceLoading = MarketPriceBlockState.Loading(currencyName = "USDT")
|
||||
|
||||
val tokenDetailsState = TokenDetailsState(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,130 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeConfig
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.error.CurrencyError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.math.BigDecimal
|
||||
|
||||
class TokenDetailsLoadedBalanceConverter(
|
||||
private val currentStateProvider: Provider<TokenDetailsState>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
) : Converter<Either<CurrencyError, CryptoCurrencyStatus>, TokenDetailsState> {
|
||||
|
||||
override fun convert(value: Either<CurrencyError, CryptoCurrencyStatus>): TokenDetailsState {
|
||||
return value.fold(ifLeft = { convertError() }, ifRight = ::convert)
|
||||
}
|
||||
|
||||
private fun convertError(): TokenDetailsState {
|
||||
// TODO: [REDACTED_JIRA]
|
||||
return currentStateProvider()
|
||||
}
|
||||
|
||||
private fun convert(status: CryptoCurrencyStatus): TokenDetailsState {
|
||||
val state = currentStateProvider()
|
||||
val currencyName = state.marketPriceBlockState.currencyName
|
||||
return state.copy(
|
||||
tokenBalanceBlockState = getBalanceState(status),
|
||||
marketPriceBlockState = getMarketPriceState(status = status.value, currencyName = currencyName),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getBalanceState(status: CryptoCurrencyStatus): TokenDetailsBalanceBlockState {
|
||||
return when (status.value) {
|
||||
is CryptoCurrencyStatus.NoQuote,
|
||||
is CryptoCurrencyStatus.Loaded,
|
||||
-> {
|
||||
TokenDetailsBalanceBlockState.Content(
|
||||
actionButtons = TokenDetailsPreviewData.actionButtons,
|
||||
fiatBalance = formatFiatAmount(status.value, appCurrencyProvider()),
|
||||
cryptoBalance = formatCryptoAmount(status),
|
||||
)
|
||||
}
|
||||
is CryptoCurrencyStatus.Loading -> {
|
||||
TokenDetailsBalanceBlockState.Loading(TokenDetailsPreviewData.disabledActionButtons)
|
||||
}
|
||||
is CryptoCurrencyStatus.MissedDerivation,
|
||||
is CryptoCurrencyStatus.NoAccount,
|
||||
is CryptoCurrencyStatus.Custom,
|
||||
// TODO: [REDACTED_JIRA]
|
||||
is CryptoCurrencyStatus.Unreachable,
|
||||
-> {
|
||||
TokenDetailsBalanceBlockState.Error(TokenDetailsPreviewData.actionButtons)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMarketPriceState(status: CryptoCurrencyStatus.Status, currencyName: String): MarketPriceBlockState {
|
||||
return when (status) {
|
||||
is CryptoCurrencyStatus.NoQuote,
|
||||
is CryptoCurrencyStatus.Loaded,
|
||||
-> MarketPriceBlockState.Content(
|
||||
currencyName = currencyName,
|
||||
price = formatPrice(status, appCurrencyProvider()),
|
||||
priceChangeConfig = PriceChangeConfig(
|
||||
valueInPercent = formatPriceChange(status),
|
||||
type = getPriceChangeType(status),
|
||||
),
|
||||
)
|
||||
is CryptoCurrencyStatus.Loading -> MarketPriceBlockState.Loading(currencyName)
|
||||
is CryptoCurrencyStatus.Custom,
|
||||
is CryptoCurrencyStatus.MissedDerivation,
|
||||
is CryptoCurrencyStatus.NoAccount,
|
||||
is CryptoCurrencyStatus.Unreachable,
|
||||
-> MarketPriceBlockState.Error(currencyName)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPriceChangeType(status: CryptoCurrencyStatus.Status): PriceChangeConfig.Type {
|
||||
val priceChange = status.priceChange ?: return PriceChangeConfig.Type.DOWN
|
||||
|
||||
return if (priceChange > BigDecimal.ZERO) {
|
||||
PriceChangeConfig.Type.UP
|
||||
} else {
|
||||
PriceChangeConfig.Type.DOWN
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatPriceChange(status: CryptoCurrencyStatus.Status): String {
|
||||
val priceChange = status.priceChange ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN
|
||||
|
||||
return BigDecimalFormatter.formatPercent(
|
||||
percent = priceChange,
|
||||
useAbsoluteValue = true,
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatPrice(status: CryptoCurrencyStatus.Status, appCurrency: AppCurrency): String {
|
||||
val fiatRate = status.fiatRate ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN
|
||||
|
||||
return BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = fiatRate,
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatFiatAmount(status: CryptoCurrencyStatus.Status, appCurrency: AppCurrency): String {
|
||||
val fiatAmount = status.fiatAmount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN
|
||||
|
||||
return BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = fiatAmount,
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatCryptoAmount(status: CryptoCurrencyStatus): String {
|
||||
val amount = status.value.amount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN
|
||||
|
||||
return BigDecimalFormatter.formatCryptoAmount(amount, status.currency.symbol, status.currency.decimals)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
|
||||
|
||||
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
|
||||
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.factory.TokenDetailsSkeletonStateConverter.SkeletonModel
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
|
||||
import com.tangem.features.tokendetails.impl.R
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class TokenDetailsSkeletonStateConverter(
|
||||
private val clickIntents: TokenDetailsClickIntents,
|
||||
) : Converter<SkeletonModel, TokenDetailsState> {
|
||||
|
||||
override fun convert(value: SkeletonModel): TokenDetailsState {
|
||||
return TokenDetailsState(
|
||||
topAppBarConfig = TokenDetailsTopAppBarConfig(
|
||||
onBackClick = clickIntents::onBackClick,
|
||||
onMoreClick = clickIntents::onMoreClick,
|
||||
),
|
||||
tokenInfoBlockState = TokenInfoBlockState(
|
||||
name = value.cryptoCurrency.name,
|
||||
iconUrl = requireNotNull(value.cryptoCurrency.iconUrl),
|
||||
currency = when (value.cryptoCurrency) {
|
||||
is CryptoCurrency.Coin -> TokenInfoBlockState.Currency.Native
|
||||
is CryptoCurrency.Token -> TokenInfoBlockState.Currency.Token(
|
||||
networkName = value.cryptoCurrency.standardType.name,
|
||||
blockchainName = value.cryptoCurrency.blockchainName,
|
||||
// TODO: [REDACTED_JIRA]
|
||||
networkIcon = R.drawable.img_eth_22,
|
||||
)
|
||||
},
|
||||
),
|
||||
tokenBalanceBlockState = TokenDetailsBalanceBlockState.Loading(
|
||||
TokenDetailsPreviewData.disabledActionButtons,
|
||||
),
|
||||
marketPriceBlockState = MarketPriceBlockState.Loading(value.cryptoCurrency.name),
|
||||
)
|
||||
}
|
||||
|
||||
data class SkeletonModel(val cryptoCurrency: CryptoCurrency)
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
|
||||
|
||||
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.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
|
||||
|
||||
internal class TokenDetailsStateFactory(
|
||||
private val currentStateProvider: Provider<TokenDetailsState>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val clickIntents: TokenDetailsClickIntents,
|
||||
) {
|
||||
|
||||
private val skeletonStateConverter by lazy {
|
||||
TokenDetailsSkeletonStateConverter(clickIntents = clickIntents)
|
||||
}
|
||||
|
||||
private val tokenDetailsLoadedBalanceConverter by lazy {
|
||||
TokenDetailsLoadedBalanceConverter(
|
||||
currentStateProvider = currentStateProvider,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
)
|
||||
}
|
||||
|
||||
fun getInitialState(cryptoCurrency: CryptoCurrency): TokenDetailsState {
|
||||
return skeletonStateConverter.convert(
|
||||
TokenDetailsSkeletonStateConverter.SkeletonModel(cryptoCurrency = cryptoCurrency),
|
||||
)
|
||||
}
|
||||
|
||||
fun getCurrencyLoadedBalanceState(
|
||||
cryptoCurrencyEither: Either<CurrencyError, CryptoCurrencyStatus>,
|
||||
): TokenDetailsState {
|
||||
return tokenDetailsLoadedBalanceConverter.convert(cryptoCurrencyEither)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels
|
||||
|
||||
interface TokenDetailsClickIntents {
|
||||
|
||||
fun onBackClick()
|
||||
|
||||
fun onMoreClick()
|
||||
}
|
||||
|
|
@ -3,67 +3,94 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.*
|
||||
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.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||
import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState
|
||||
import com.tangem.features.tokendetails.impl.R
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory
|
||||
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.flow.*
|
||||
import javax.inject.Inject
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
private const val LOADING_DELAY = 4_000L
|
||||
|
||||
@HiltViewModel
|
||||
internal class TokenDetailsViewModel @Inject constructor(
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
|
||||
private val getCurrencyUseCase: GetCurrencyUseCase,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel() {
|
||||
) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents {
|
||||
|
||||
private val cryptoCurrency: CryptoCurrency = savedStateHandle[TokenDetailsRouter.SELECTED_CURRENCY_KEY]
|
||||
?: error("no expected parameter CryptoCurrency found")
|
||||
|
||||
var router by Delegates.notNull<InnerTokenDetailsRouter>()
|
||||
|
||||
var uiState by mutableStateOf(getInitialState())
|
||||
private val marketPriceJobHolder = JobHolder()
|
||||
|
||||
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
|
||||
private val stateFactory = TokenDetailsStateFactory(
|
||||
currentStateProvider = Provider { uiState },
|
||||
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
|
||||
clickIntents = this,
|
||||
)
|
||||
var uiState: TokenDetailsState by mutableStateOf(stateFactory.getInitialState(cryptoCurrency))
|
||||
private set
|
||||
|
||||
init {
|
||||
// simulate loading state
|
||||
viewModelScope.launch {
|
||||
delay(LOADING_DELAY)
|
||||
uiState = uiState.copy(
|
||||
tokenBalanceBlockState = TokenDetailsPreviewData.balanceContent,
|
||||
marketPriceBlockState = TokenDetailsPreviewData.marketPriceContent,
|
||||
)
|
||||
}
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
updateContent(selectedWallet = getWallet(), refresh = false)
|
||||
}
|
||||
|
||||
private fun getInitialState() = TokenDetailsPreviewData.tokenDetailsState.copy(
|
||||
topAppBarConfig = TokenDetailsPreviewData.tokenDetailsTopAppBarConfig.copy(
|
||||
onBackClick = ::onBackClick,
|
||||
),
|
||||
tokenInfoBlockState = TokenInfoBlockState(
|
||||
name = cryptoCurrency.name,
|
||||
iconUrl = requireNotNull(cryptoCurrency.iconUrl),
|
||||
currency = when (cryptoCurrency) {
|
||||
is CryptoCurrency.Coin -> TokenInfoBlockState.Currency.Native
|
||||
is CryptoCurrency.Token -> TokenInfoBlockState.Currency.Token(
|
||||
networkName = cryptoCurrency.standardType.name,
|
||||
blockchainName = cryptoCurrency.blockchainName,
|
||||
// TODO: [REDACTED_JIRA]
|
||||
networkIcon = R.drawable.img_eth_22,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
private fun getWallet(): UserWallet {
|
||||
return getSelectedWalletUseCase()
|
||||
.fold(
|
||||
ifLeft = { error("Can not get selected wallet $it") },
|
||||
ifRight = { it },
|
||||
)
|
||||
}
|
||||
|
||||
private fun onBackClick() {
|
||||
private fun updateContent(selectedWallet: UserWallet, refresh: Boolean) {
|
||||
updateMarketPrice(selectedWallet = selectedWallet, refresh = refresh)
|
||||
}
|
||||
|
||||
private fun updateMarketPrice(selectedWallet: UserWallet, refresh: Boolean) {
|
||||
getCurrencyUseCase(userWalletId = selectedWallet.walletId, currencyId = cryptoCurrency.id, refresh = refresh)
|
||||
.distinctUntilChanged()
|
||||
.onEach { uiState = stateFactory.getCurrencyLoadedBalanceState(it) }
|
||||
.flowOn(dispatchers.io)
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(marketPriceJobHolder)
|
||||
}
|
||||
|
||||
private fun createSelectedAppCurrencyFlow(): StateFlow<AppCurrency> {
|
||||
return getSelectedAppCurrencyUseCase()
|
||||
.map { maybeAppCurrency ->
|
||||
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = AppCurrency.Default,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBackClick() {
|
||||
router.popBackStack()
|
||||
}
|
||||
|
||||
override fun onMoreClick() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
|
@ -28,10 +28,10 @@ internal object WalletPreviewData {
|
|||
|
||||
val walletCardContentState by lazy {
|
||||
WalletCardState.Content(
|
||||
id = UserWalletId("123"),
|
||||
id = UserWalletId(stringValue = "123"),
|
||||
title = "Wallet 1",
|
||||
balance = "8923,05 $",
|
||||
additionalInfo = "3 cards • Seed enabled",
|
||||
additionalInfo = TextReference.Str("3 cards • Seed phrase"),
|
||||
imageResId = R.drawable.ill_businessman_3d,
|
||||
onClick = null,
|
||||
)
|
||||
|
|
@ -41,7 +41,6 @@ internal object WalletPreviewData {
|
|||
WalletCardState.Loading(
|
||||
id = UserWalletId("321"),
|
||||
title = "Wallet 1",
|
||||
additionalInfo = "3 cards • Seed enabled",
|
||||
imageResId = R.drawable.ill_businessman_3d,
|
||||
onClick = null,
|
||||
)
|
||||
|
|
@ -51,7 +50,6 @@ internal object WalletPreviewData {
|
|||
WalletCardState.HiddenContent(
|
||||
id = UserWalletId("42"),
|
||||
title = "Wallet 1",
|
||||
additionalInfo = "3 cards • Seed enabled",
|
||||
imageResId = R.drawable.ill_businessman_3d,
|
||||
onClick = null,
|
||||
)
|
||||
|
|
@ -61,7 +59,6 @@ internal object WalletPreviewData {
|
|||
WalletCardState.Error(
|
||||
id = UserWalletId("24"),
|
||||
title = "Wallet 1",
|
||||
additionalInfo = "3 cards • Seed enabled",
|
||||
imageResId = R.drawable.ill_businessman_3d,
|
||||
onClick = null,
|
||||
)
|
||||
|
|
@ -351,10 +348,10 @@ internal object WalletPreviewData {
|
|||
),
|
||||
),
|
||||
txHistoryState = TxHistoryState.Content(
|
||||
flowOf(
|
||||
onExploreClick = {},
|
||||
contentItems = flowOf(
|
||||
PagingData.from(
|
||||
listOf(
|
||||
TxHistoryState.TxHistoryItemState.Title(onExploreClick = {}),
|
||||
TxHistoryState.TxHistoryItemState.GroupTitle("Today"),
|
||||
TxHistoryState.TxHistoryItemState.Transaction(
|
||||
TransactionState.Sending(
|
||||
|
|
|
|||
|
|
@ -75,12 +75,13 @@ internal sealed interface TokenItemState {
|
|||
) : TokenItemState
|
||||
|
||||
/** Token options state */
|
||||
@Immutable
|
||||
sealed interface TokenOptionsState {
|
||||
|
||||
/**
|
||||
* Visible token options state
|
||||
*
|
||||
* @property fiatAmount fiat amount of token
|
||||
* @property fiatAmount fiat amount of token
|
||||
* @property priceChange value of price changing
|
||||
*/
|
||||
data class Visible(val fiatAmount: String, val priceChange: PriceChangeConfig) : TokenOptionsState
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.domain
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.plus
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.common.CardTypesResolver
|
||||
import com.tangem.utils.toFormattedCurrencyString
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
|
|
@ -9,35 +13,69 @@ import java.math.BigDecimal
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
// TODO: Finalize strings [REDACTED_JIRA]
|
||||
internal object WalletAdditionalInfoFactory {
|
||||
|
||||
private val DIVIDER_RES by lazy { TextReference.Str(value = " • ") }
|
||||
|
||||
/**
|
||||
* Get additional info
|
||||
*
|
||||
* @param cardTypesResolver card types resolver
|
||||
* @param cardTypesResolver card type resolver
|
||||
* @param isLocked check if wallet is locked
|
||||
* @param currencyAmount amount of currency
|
||||
*/
|
||||
fun resolve(cardTypesResolver: CardTypesResolver, isLocked: Boolean, currencyAmount: BigDecimal? = null): String {
|
||||
fun resolve(
|
||||
cardTypesResolver: CardTypesResolver,
|
||||
isLocked: Boolean,
|
||||
currencyAmount: BigDecimal? = null,
|
||||
): TextReference {
|
||||
return if (cardTypesResolver.isMultiwalletAllowed()) {
|
||||
val backupInfo = "${cardTypesResolver.getBackupCardsCount()} cards"
|
||||
when {
|
||||
cardTypesResolver.isWallet2() && !isLocked -> "$backupInfo • Seed phrase"
|
||||
cardTypesResolver.isTangemWallet() && !isLocked -> backupInfo
|
||||
isLocked -> "$backupInfo • Locked"
|
||||
else -> ""
|
||||
}
|
||||
resolveMultiCurrencyInfo(cardTypesResolver, isLocked)
|
||||
} else {
|
||||
if (isLocked) {
|
||||
"Locked"
|
||||
} else {
|
||||
val blockchain = cardTypesResolver.getBlockchain()
|
||||
currencyAmount?.toFormattedCurrencyString(
|
||||
decimals = blockchain.decimals(),
|
||||
currency = blockchain.currency,
|
||||
).orEmpty()
|
||||
resolveSingleCurrencyInfo(cardTypesResolver, isLocked, currencyAmount)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveMultiCurrencyInfo(cardTypeResolver: CardTypesResolver, isLocked: Boolean): TextReference {
|
||||
val backupCardsCount = cardTypeResolver.getBackupCardsCount()
|
||||
val backupInfoRes = TextReference.PluralRes(
|
||||
id = R.plurals.card_label_card_count,
|
||||
count = backupCardsCount,
|
||||
formatArgs = wrappedList(backupCardsCount),
|
||||
)
|
||||
|
||||
return when {
|
||||
cardTypeResolver.isWallet2() && !isLocked -> {
|
||||
backupInfoRes + DIVIDER_RES + TextReference.Res(id = R.string.common_seed_phrase)
|
||||
}
|
||||
cardTypeResolver.isTangemWallet() && !isLocked -> {
|
||||
backupInfoRes
|
||||
}
|
||||
isLocked -> {
|
||||
backupInfoRes + TextReference.Res(R.string.common_locked)
|
||||
}
|
||||
else -> error("It isn't exist additional info for this case")
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveSingleCurrencyInfo(
|
||||
cardTypeResolver: CardTypesResolver,
|
||||
isLocked: Boolean,
|
||||
currencyAmount: BigDecimal?,
|
||||
): TextReference {
|
||||
return if (isLocked) {
|
||||
TextReference.Res(R.string.common_locked)
|
||||
} else {
|
||||
val blockchain = cardTypeResolver.getBlockchain()
|
||||
val amount = currencyAmount?.let {
|
||||
BigDecimalFormatter.formatCryptoAmount(
|
||||
cryptoAmount = it,
|
||||
cryptoCurrency = blockchain.currency,
|
||||
decimals = blockchain.decimals(),
|
||||
)
|
||||
}
|
||||
|
||||
TextReference.Str(value = amount.orEmpty())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.*
|
||||
|
|
@ -11,14 +12,12 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Immutable
|
||||
internal sealed class WalletSingleCurrencyState : WalletState.ContentState() {
|
||||
|
||||
/** Manage buttons */
|
||||
abstract val buttons: ImmutableList<WalletManageButton>
|
||||
|
||||
/** Market price block state */
|
||||
abstract val marketPriceBlockState: MarketPriceBlockState?
|
||||
|
||||
/** Transactions history state */
|
||||
abstract val txHistoryState: TxHistoryState
|
||||
|
||||
|
|
@ -30,8 +29,8 @@ internal sealed class WalletSingleCurrencyState : WalletState.ContentState() {
|
|||
override val notifications: ImmutableList<WalletNotification>,
|
||||
override val bottomSheetConfig: WalletBottomSheetConfig?,
|
||||
override val buttons: ImmutableList<WalletManageButton>,
|
||||
override val marketPriceBlockState: MarketPriceBlockState,
|
||||
override val txHistoryState: TxHistoryState,
|
||||
val marketPriceBlockState: MarketPriceBlockState,
|
||||
) : WalletSingleCurrencyState()
|
||||
|
||||
data class Locked(
|
||||
|
|
@ -61,8 +60,6 @@ internal sealed class WalletSingleCurrencyState : WalletState.ContentState() {
|
|||
),
|
||||
)
|
||||
|
||||
override val marketPriceBlockState = null
|
||||
|
||||
override val txHistoryState: TxHistoryState = TxHistoryState.Locked(onExploreClick)
|
||||
}
|
||||
}
|
||||
|
|
@ -34,14 +34,26 @@ internal sealed class WalletState {
|
|||
/**
|
||||
* Util function that allow to make a copy
|
||||
*
|
||||
* @param walletsListConfig wallets list config
|
||||
* @param walletsListConfig wallets list config
|
||||
* @param pullToRefreshConfig pull to refresh config
|
||||
*/
|
||||
fun copySealed(walletsListConfig: WalletsListConfig = this.walletsListConfig): ContentState {
|
||||
fun copySealed(
|
||||
walletsListConfig: WalletsListConfig = this.walletsListConfig,
|
||||
pullToRefreshConfig: WalletPullToRefreshConfig = this.pullToRefreshConfig,
|
||||
): ContentState {
|
||||
return when (this) {
|
||||
is WalletMultiCurrencyState.Content -> copy(walletsListConfig = walletsListConfig)
|
||||
is WalletMultiCurrencyState.Locked -> copy(walletsListConfig = walletsListConfig)
|
||||
is WalletSingleCurrencyState.Content -> copy(walletsListConfig = walletsListConfig)
|
||||
is WalletSingleCurrencyState.Locked -> copy(walletsListConfig = walletsListConfig)
|
||||
is WalletMultiCurrencyState.Content -> {
|
||||
copy(walletsListConfig = walletsListConfig, pullToRefreshConfig = pullToRefreshConfig)
|
||||
}
|
||||
is WalletMultiCurrencyState.Locked -> {
|
||||
copy(walletsListConfig = walletsListConfig, pullToRefreshConfig = pullToRefreshConfig)
|
||||
}
|
||||
is WalletSingleCurrencyState.Content -> {
|
||||
copy(walletsListConfig = walletsListConfig, pullToRefreshConfig = pullToRefreshConfig)
|
||||
}
|
||||
is WalletSingleCurrencyState.Locked -> {
|
||||
copy(walletsListConfig = walletsListConfig, pullToRefreshConfig = pullToRefreshConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.components
|
|||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
/** Wallet card state */
|
||||
|
|
@ -14,9 +15,6 @@ internal sealed interface WalletCardState {
|
|||
/** Title */
|
||||
val title: String
|
||||
|
||||
/** Additional wallet information */
|
||||
val additionalInfo: String
|
||||
|
||||
/** Wallet image resource id */
|
||||
@get:DrawableRes
|
||||
val imageResId: Int?
|
||||
|
|
@ -24,38 +22,43 @@ internal sealed interface WalletCardState {
|
|||
/** Lambda be invoked when card is clicked */
|
||||
val onClick: (() -> Unit)?
|
||||
|
||||
/** Additional text availability */
|
||||
sealed interface AdditionalTextAvailability {
|
||||
|
||||
/** Additional wallet information */
|
||||
val additionalInfo: TextReference
|
||||
}
|
||||
|
||||
/**
|
||||
* Wallet card content state
|
||||
*
|
||||
* @property id wallet id
|
||||
* @property title wallet name
|
||||
* @property additionalInfo wallet additional info
|
||||
* @property imageResId wallet image resource id
|
||||
* @property onClick lambda be invoked when wallet card is clicked
|
||||
* @property additionalInfo wallet additional info
|
||||
* @property balance wallet balance
|
||||
*/
|
||||
data class Content(
|
||||
override val id: UserWalletId,
|
||||
override val title: String,
|
||||
override val additionalInfo: String,
|
||||
override val imageResId: Int?,
|
||||
override val onClick: (() -> Unit)? = null,
|
||||
override val additionalInfo: TextReference,
|
||||
val balance: String,
|
||||
) : WalletCardState
|
||||
) : WalletCardState, AdditionalTextAvailability
|
||||
|
||||
/**
|
||||
* Wallet card loading state
|
||||
*
|
||||
* @property id wallet id
|
||||
* @property title wallet name
|
||||
* @property additionalInfo wallet additional info
|
||||
* @property imageResId wallet image resource id
|
||||
* @property onClick lambda be invoked when wallet card is clicked
|
||||
* @property id wallet id
|
||||
* @property title wallet name
|
||||
* @property imageResId wallet image resource id
|
||||
* @property onClick lambda be invoked when wallet card is clicked
|
||||
*/
|
||||
data class Loading(
|
||||
override val id: UserWalletId,
|
||||
override val title: String,
|
||||
override val additionalInfo: String,
|
||||
override val imageResId: Int?,
|
||||
override val onClick: (() -> Unit)? = null,
|
||||
) : WalletCardState
|
||||
|
|
@ -63,34 +66,40 @@ internal sealed interface WalletCardState {
|
|||
/**
|
||||
* Wallet card hidden content state
|
||||
*
|
||||
* @property id wallet id
|
||||
* @property title wallet name
|
||||
* @property additionalInfo wallet additional info
|
||||
* @property imageResId wallet image resource id
|
||||
* @property onClick lambda be invoked when wallet card is clicked
|
||||
* @property id wallet id
|
||||
* @property title wallet name
|
||||
* @property imageResId wallet image resource id
|
||||
* @property onClick lambda be invoked when wallet card is clicked
|
||||
*/
|
||||
data class HiddenContent(
|
||||
override val id: UserWalletId,
|
||||
override val title: String,
|
||||
override val additionalInfo: String,
|
||||
override val imageResId: Int?,
|
||||
override val onClick: (() -> Unit)?,
|
||||
) : WalletCardState
|
||||
) : WalletCardState, AdditionalTextAvailability {
|
||||
|
||||
override val additionalInfo: TextReference = HIDDEN_BALANCE_TEXT
|
||||
}
|
||||
|
||||
/**
|
||||
* Wallet card error state
|
||||
*
|
||||
* @property id wallet id
|
||||
* @property title wallet name
|
||||
* @property additionalInfo wallet additional info
|
||||
* @property imageResId wallet image resource id
|
||||
* @property onClick lambda be invoked when wallet card is clicked
|
||||
* @property additionalInfo wallet additional info
|
||||
*/
|
||||
data class Error(
|
||||
override val id: UserWalletId,
|
||||
override val title: String,
|
||||
override val additionalInfo: String,
|
||||
override val imageResId: Int?,
|
||||
override val onClick: (() -> Unit)?,
|
||||
) : WalletCardState
|
||||
override val additionalInfo: TextReference = EMPTY_BALANCE_TEXT,
|
||||
) : WalletCardState, AdditionalTextAvailability
|
||||
|
||||
companion object {
|
||||
val HIDDEN_BALANCE_TEXT by lazy { TextReference.Str(value = "•••") }
|
||||
val EMPTY_BALANCE_TEXT by lazy { TextReference.Str(value = "—") }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.components
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
|
|
@ -11,6 +12,7 @@ import com.tangem.feature.wallet.impl.R
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Immutable
|
||||
sealed class WalletManageButton(val config: ActionButtonConfig) {
|
||||
|
||||
/** Lambda be invoked when manage button is clicked */
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.components
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.notifications.NotificationState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.WrappedList
|
||||
|
|
@ -14,6 +15,7 @@ import com.tangem.feature.wallet.impl.R
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
// TODO: Finalize notification strings [REDACTED_JIRA]
|
||||
@Immutable
|
||||
sealed class WalletNotification(open val state: NotificationState) {
|
||||
|
||||
/** Clickable notification */
|
||||
|
|
|
|||
|
|
@ -28,14 +28,17 @@ internal sealed class WalletTokensListState {
|
|||
open val onOrganizeTokensClick: (() -> Unit)?,
|
||||
) : WalletTokensListState()
|
||||
|
||||
/** Loading content state */
|
||||
object Loading : ContentState(
|
||||
items = persistentListOf(
|
||||
/**
|
||||
* Loading content state
|
||||
*
|
||||
* @property items content items
|
||||
*/
|
||||
data class Loading(
|
||||
override val items: ImmutableList<TokensListItemState.Token> = persistentListOf(
|
||||
TokensListItemState.Token(state = TokenItemState.Loading(id = FIRST_LOADING_TOKEN_ID)),
|
||||
TokensListItemState.Token(state = TokenItemState.Loading(id = SECOND_LOADING_TOKEN_ID)),
|
||||
),
|
||||
onOrganizeTokensClick = null,
|
||||
)
|
||||
) : ContentState(items = items, onOrganizeTokensClick = null)
|
||||
|
||||
/**
|
||||
* Content state
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.factory
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.map
|
||||
import com.tangem.common.Provider
|
||||
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.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState
|
||||
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletPullToRefreshConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.TokensListItemState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
internal class WalletRefreshStateConverter(
|
||||
private val currentStateProvider: Provider<WalletState>,
|
||||
private val clickIntents: WalletClickIntents,
|
||||
) : Converter<Unit, WalletState> {
|
||||
|
||||
override fun convert(value: Unit): WalletState {
|
||||
return when (val state = currentStateProvider()) {
|
||||
is WalletMultiCurrencyState.Content -> state.getRefreshState()
|
||||
is WalletSingleCurrencyState.Content -> state.getRefreshState()
|
||||
else -> state
|
||||
}
|
||||
}
|
||||
|
||||
private fun WalletMultiCurrencyState.Content.getRefreshState(): WalletMultiCurrencyState.Content {
|
||||
return copy(
|
||||
walletsListConfig = getWalletsListConfig(),
|
||||
pullToRefreshConfig = getPullToRefreshConfig(),
|
||||
tokensListState = getTokenListState(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun WalletSingleCurrencyState.Content.getRefreshState(): WalletSingleCurrencyState.Content {
|
||||
return copy(
|
||||
// TODO: [REDACTED_JIRA]
|
||||
walletsListConfig = getWalletsListConfig(),
|
||||
pullToRefreshConfig = getPullToRefreshConfig(),
|
||||
txHistoryState = getTxHistoryState(),
|
||||
marketPriceBlockState = MarketPriceBlockState.Loading(currencyName = marketPriceBlockState.currencyName),
|
||||
)
|
||||
}
|
||||
|
||||
private fun WalletState.ContentState.getWalletsListConfig(): WalletsListConfig {
|
||||
val selectedWallet = walletsListConfig.wallets[walletsListConfig.selectedWalletIndex]
|
||||
|
||||
return walletsListConfig.copy(
|
||||
wallets = walletsListConfig.wallets
|
||||
.toPersistentList()
|
||||
.set(
|
||||
index = walletsListConfig.selectedWalletIndex,
|
||||
element = WalletCardState.Loading(
|
||||
id = selectedWallet.id,
|
||||
title = selectedWallet.title,
|
||||
imageResId = selectedWallet.imageResId,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun WalletState.ContentState.getPullToRefreshConfig(): WalletPullToRefreshConfig {
|
||||
return pullToRefreshConfig.copy(isRefreshing = true)
|
||||
}
|
||||
|
||||
private fun WalletMultiCurrencyState.Content.getTokenListState(): WalletTokensListState {
|
||||
return when (tokensListState) {
|
||||
is WalletTokensListState.Content -> {
|
||||
WalletTokensListState.Loading(
|
||||
items = tokensListState.items
|
||||
.filterIsInstance<TokensListItemState.Token>()
|
||||
.map {
|
||||
TokensListItemState.Token(state = TokenItemState.Loading(id = it.state.id))
|
||||
}
|
||||
.toImmutableList(),
|
||||
)
|
||||
}
|
||||
is WalletTokensListState.Empty -> WalletTokensListState.Loading()
|
||||
is WalletTokensListState.Loading,
|
||||
is WalletTokensListState.Locked,
|
||||
-> tokensListState
|
||||
}
|
||||
}
|
||||
|
||||
private fun WalletSingleCurrencyState.Content.getTxHistoryState(): TxHistoryState {
|
||||
return when (txHistoryState) {
|
||||
is TxHistoryState.Content -> {
|
||||
TxHistoryState.Loading(
|
||||
onExploreClick = clickIntents::onExploreClick,
|
||||
transactions = txHistoryState.contentItems
|
||||
.filterIsInstance<PagingData<TxHistoryItemState.Transaction>>()
|
||||
.mapPagingData { transaction ->
|
||||
transaction.copy(
|
||||
state = TransactionState.Loading(txHash = transaction.state.txHash),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
is TxHistoryState.Empty,
|
||||
is TxHistoryState.Error,
|
||||
is TxHistoryState.NotSupported,
|
||||
-> TxHistoryState.Loading(onExploreClick = clickIntents::onExploreClick)
|
||||
is TxHistoryState.Locked,
|
||||
is TxHistoryState.Loading,
|
||||
-> txHistoryState
|
||||
}
|
||||
}
|
||||
|
||||
private fun Flow<PagingData<TxHistoryItemState.Transaction>>.mapPagingData(
|
||||
transform: (TxHistoryItemState.Transaction) -> TxHistoryItemState,
|
||||
): Flow<PagingData<TxHistoryItemState>> {
|
||||
return map { it.map(transform) }
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyS
|
|||
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletSingleCurrencyLoadedBalanceConverter.SingleCurrencyLoadedBalanceModel
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -22,21 +23,32 @@ internal class WalletSingleCurrencyLoadedBalanceConverter(
|
|||
private val currentStateProvider: Provider<WalletState>,
|
||||
private val cardTypeResolverProvider: Provider<CardTypesResolver>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
) : Converter<Either<CurrencyError, CryptoCurrencyStatus>, WalletSingleCurrencyState.Content> {
|
||||
) : Converter<SingleCurrencyLoadedBalanceModel, WalletSingleCurrencyState.Content> {
|
||||
|
||||
override fun convert(value: Either<CurrencyError, CryptoCurrencyStatus>): WalletSingleCurrencyState.Content {
|
||||
return value.fold(ifLeft = { convertError() }, ifRight = ::convert)
|
||||
override fun convert(value: SingleCurrencyLoadedBalanceModel): WalletSingleCurrencyState.Content {
|
||||
return value.cryptoCurrencyEither.fold(
|
||||
ifLeft = { convertError() },
|
||||
ifRight = { convertContent(it, value.isRefreshing) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertError(): WalletSingleCurrencyState.Content {
|
||||
return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content)
|
||||
}
|
||||
|
||||
private fun convert(status: CryptoCurrencyStatus): WalletSingleCurrencyState.Content {
|
||||
private fun convertContent(
|
||||
status: CryptoCurrencyStatus,
|
||||
isRefreshing: Boolean,
|
||||
): WalletSingleCurrencyState.Content {
|
||||
val state = requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content)
|
||||
val currencyName = state.marketPriceBlockState.currencyName
|
||||
return state.copy(
|
||||
walletsListConfig = getUpdatedSelectedWallet(status = status.value, state = state),
|
||||
pullToRefreshConfig = if (isRefreshing) {
|
||||
state.pullToRefreshConfig.copy(isRefreshing = status.value is CryptoCurrencyStatus.Loading)
|
||||
} else {
|
||||
state.pullToRefreshConfig
|
||||
},
|
||||
marketPriceBlockState = getMarketPriceState(status = status.value, currencyName = currencyName),
|
||||
)
|
||||
}
|
||||
|
|
@ -82,14 +94,13 @@ internal class WalletSingleCurrencyLoadedBalanceConverter(
|
|||
),
|
||||
imageResId = selectedWallet.imageResId,
|
||||
onClick = selectedWallet.onClick,
|
||||
balance = formatFiatAmount(status, appCurrencyProvider()),
|
||||
balance = formatFiatAmount(status = status, appCurrency = appCurrencyProvider()),
|
||||
)
|
||||
}
|
||||
is CryptoCurrencyStatus.Loading -> {
|
||||
WalletCardState.Loading(
|
||||
id = selectedWallet.id,
|
||||
title = selectedWallet.title,
|
||||
additionalInfo = selectedWallet.additionalInfo,
|
||||
imageResId = selectedWallet.imageResId,
|
||||
onClick = selectedWallet.onClick,
|
||||
)
|
||||
|
|
@ -102,7 +113,6 @@ internal class WalletSingleCurrencyLoadedBalanceConverter(
|
|||
WalletCardState.Error(
|
||||
id = selectedWallet.id,
|
||||
title = selectedWallet.title,
|
||||
additionalInfo = selectedWallet.additionalInfo,
|
||||
imageResId = selectedWallet.imageResId,
|
||||
onClick = selectedWallet.onClick,
|
||||
)
|
||||
|
|
@ -153,4 +163,9 @@ internal class WalletSingleCurrencyLoadedBalanceConverter(
|
|||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
}
|
||||
|
||||
data class SingleCurrencyLoadedBalanceModel(
|
||||
val cryptoCurrencyEither: Either<CurrencyError, CryptoCurrencyStatus>,
|
||||
val isRefreshing: Boolean,
|
||||
)
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
|
|||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
|
||||
|
|
@ -47,7 +46,7 @@ internal class WalletSkeletonStateConverter(
|
|||
topBarConfig = createTopBarConfig(),
|
||||
walletsListConfig = createWalletsListConfig(value),
|
||||
pullToRefreshConfig = createPullToRefreshConfig(),
|
||||
tokensListState = WalletTokensListState.Loading,
|
||||
tokensListState = WalletTokensListState.Loading(),
|
||||
notifications = persistentListOf(),
|
||||
bottomSheetConfig = null,
|
||||
)
|
||||
|
|
@ -106,10 +105,6 @@ internal class WalletSkeletonStateConverter(
|
|||
return WalletCardState.Loading(
|
||||
id = wallet.walletId,
|
||||
title = wallet.name,
|
||||
additionalInfo = WalletAdditionalInfoFactory.resolve(
|
||||
cardTypesResolver = cardTypeResolver,
|
||||
isLocked = wallet.isLocked,
|
||||
),
|
||||
imageResId = WalletImageResolver.resolve(cardTypesResolver = cardTypeResolver),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,6 +77,10 @@ internal class WalletStateFactory(
|
|||
)
|
||||
}
|
||||
|
||||
private val refreshStateConverter by lazy {
|
||||
WalletRefreshStateConverter(currentStateProvider = currentStateProvider, clickIntents = clickIntents)
|
||||
}
|
||||
|
||||
fun getInitialState(): WalletState = WalletState.Initial(onBackClick = clickIntents::onBackClick)
|
||||
|
||||
fun getSkeletonState(wallets: List<UserWallet>, selectedWalletIndex: Int): WalletState {
|
||||
|
|
@ -105,9 +109,7 @@ internal class WalletStateFactory(
|
|||
}
|
||||
}
|
||||
|
||||
fun getStateAfterContentRefreshing(): WalletState {
|
||||
return currentStateProvider()
|
||||
}
|
||||
fun getStateAfterContentRefreshing(): WalletState = refreshStateConverter.convert(Unit)
|
||||
|
||||
fun getStateWithOpenBottomSheet(content: WalletBottomSheetConfig.BottomSheetContentConfig): WalletState {
|
||||
return when (val state = currentStateProvider() as WalletState.ContentState) {
|
||||
|
|
@ -205,7 +207,13 @@ internal class WalletStateFactory(
|
|||
|
||||
fun getSingleCurrencyLoadedBalanceState(
|
||||
cryptoCurrencyEither: Either<CurrencyError, CryptoCurrencyStatus>,
|
||||
isRefreshing: Boolean,
|
||||
): WalletState {
|
||||
return singleCurrencyLoadedBalanceConverter.convert(cryptoCurrencyEither)
|
||||
return singleCurrencyLoadedBalanceConverter.convert(
|
||||
value = WalletSingleCurrencyLoadedBalanceConverter.SingleCurrencyLoadedBalanceModel(
|
||||
cryptoCurrencyEither = cryptoCurrencyEither,
|
||||
isRefreshing = isRefreshing,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,16 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import arrow.core.Either
|
||||
import com.tangem.common.Provider
|
||||
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.wallet.presentation.wallet.state.WalletSingleCurrencyState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
/**
|
||||
* Converter from loading tx history state to [WalletSingleCurrencyState.Content]
|
||||
|
|
@ -44,7 +47,17 @@ internal class WalletLoadingTxHistoryConverter(
|
|||
|
||||
private fun convert(value: Int): WalletSingleCurrencyState.Content {
|
||||
return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content).copy(
|
||||
txHistoryState = TxHistoryState.ContentWithLoadingItems(itemsCount = value),
|
||||
txHistoryState = TxHistoryState.Loading(
|
||||
onExploreClick = clickIntents::onExploreClick,
|
||||
transactions = flow {
|
||||
PagingData.from(
|
||||
data = MutableList(
|
||||
size = value,
|
||||
init = { TransactionState.Loading(it.toString()) },
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +1,21 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory
|
||||
|
||||
import android.text.format.DateUtils
|
||||
import androidx.paging.*
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.TerminalSeparatorType
|
||||
import androidx.paging.insertSeparators
|
||||
import androidx.paging.map
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
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.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
|
||||
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.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.joda.time.DateTime
|
||||
|
|
@ -58,17 +61,14 @@ internal class WalletTxHistoryItemFlowConverter(
|
|||
|
||||
override fun convert(value: Flow<PagingData<TxHistoryItem>>): TxHistoryState {
|
||||
return TxHistoryState.Content(
|
||||
items = value
|
||||
onExploreClick = clickIntents::onExploreClick,
|
||||
contentItems = value
|
||||
.map { pagingData ->
|
||||
pagingData
|
||||
.map<TxHistoryItem, TxHistoryItemState> { item ->
|
||||
// [createTransactionState] returns timestamp without formatting
|
||||
TxHistoryItemState.Transaction(state = createTransactionState(item))
|
||||
}
|
||||
.insertHeaderItem(
|
||||
terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE,
|
||||
item = TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick),
|
||||
)
|
||||
.insertGroupTitle() // method uses the raw timestamp
|
||||
.formatTransactionsTimestamp() // method formats the timestamp
|
||||
},
|
||||
|
|
@ -133,7 +133,11 @@ internal class WalletTxHistoryItemFlowConverter(
|
|||
}
|
||||
|
||||
private fun BigDecimal.toCryptoCurrencyFormat(blockchain: Blockchain): String {
|
||||
return toFormattedCurrencyString(currency = blockchain.currency, decimals = blockchain.decimals())
|
||||
return BigDecimalFormatter.formatCryptoAmount(
|
||||
cryptoAmount = this,
|
||||
cryptoCurrency = blockchain.currency,
|
||||
decimals = blockchain.decimals(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun PagingData<TxHistoryItemState>.insertGroupTitle(): PagingData<TxHistoryItemState> {
|
||||
|
|
|
|||
|
|
@ -18,18 +18,17 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import androidx.constraintlayout.compose.ConstraintLayoutScope
|
||||
import androidx.constraintlayout.compose.Dimension
|
||||
import com.tangem.core.ui.components.FontSizeRange
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.ResizableText
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState
|
||||
|
||||
private const val DOTS = "•••"
|
||||
|
||||
/**
|
||||
* Wallet card
|
||||
*
|
||||
|
|
@ -40,61 +39,87 @@ private const val DOTS = "•••"
|
|||
*/
|
||||
@Composable
|
||||
internal fun WalletCard(state: WalletCardState, modifier: Modifier = Modifier) {
|
||||
@Suppress("DestructuringDeclarationWithTooManyEntries")
|
||||
CardContainer(onClick = state.onClick, modifier = modifier) {
|
||||
val (title, balance, additionalText, image) = createRefs()
|
||||
|
||||
val contentVerticalMargin = TangemTheme.dimens.spacing12
|
||||
Title(
|
||||
state = state,
|
||||
modifier = Modifier.constrainAs(title) {
|
||||
start.linkTo(parent.start)
|
||||
top.linkTo(anchor = parent.top, margin = contentVerticalMargin)
|
||||
end.linkTo(image.start)
|
||||
width = Dimension.fillToConstraints
|
||||
},
|
||||
)
|
||||
|
||||
val betweenContentMargin = TangemTheme.dimens.spacing8
|
||||
Balance(
|
||||
state = state,
|
||||
modifier = Modifier.constrainAs(balance) {
|
||||
start.linkTo(parent.start)
|
||||
top.linkTo(anchor = title.bottom, margin = betweenContentMargin)
|
||||
bottom.linkTo(anchor = additionalText.top, margin = betweenContentMargin)
|
||||
},
|
||||
)
|
||||
|
||||
AdditionalInfo(
|
||||
state = state,
|
||||
modifier = Modifier.constrainAs(additionalText) {
|
||||
start.linkTo(parent.start)
|
||||
bottom.linkTo(anchor = parent.bottom, margin = contentVerticalMargin)
|
||||
},
|
||||
)
|
||||
|
||||
val imageWidth = TangemTheme.dimens.size120
|
||||
Image(
|
||||
id = state.imageResId,
|
||||
modifier = Modifier.constrainAs(image) {
|
||||
centerVerticallyTo(parent)
|
||||
top.linkTo(parent.top)
|
||||
end.linkTo(parent.end)
|
||||
height = Dimension.fillToConstraints
|
||||
width = Dimension.value(imageWidth)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CardContainer(
|
||||
onClick: (() -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable ConstraintLayoutScope.() -> Unit,
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier.defaultMinSize(minHeight = TangemTheme.dimens.size108),
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
color = TangemTheme.colors.background.primary,
|
||||
onClick = state.onClick ?: {},
|
||||
enabled = state.onClick != null,
|
||||
onClick = onClick ?: {},
|
||||
enabled = onClick != null,
|
||||
) {
|
||||
ConstraintLayout(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing14),
|
||||
) {
|
||||
val (balanceBlock, imageItem) = createRefs()
|
||||
Column(
|
||||
modifier = Modifier.constrainAs(balanceBlock) {
|
||||
centerVerticallyTo(parent)
|
||||
start.linkTo(parent.start)
|
||||
end.linkTo(imageItem.start)
|
||||
width = Dimension.fillToConstraints
|
||||
},
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
Title(state)
|
||||
Balance(state)
|
||||
AdditionalInfo(description = state.additionalInfo)
|
||||
}
|
||||
|
||||
val imageWidth = TangemTheme.dimens.size120
|
||||
WalletImage(
|
||||
id = state.imageResId,
|
||||
modifier = Modifier.constrainAs(imageItem) {
|
||||
centerVerticallyTo(parent)
|
||||
top.linkTo(parent.top)
|
||||
end.linkTo(parent.end)
|
||||
height = Dimension.fillToConstraints
|
||||
width = Dimension.value(imageWidth)
|
||||
},
|
||||
)
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
private fun Title(state: WalletCardState) {
|
||||
AnimatedContent(targetState = state, label = "Update the title") {
|
||||
when (it) {
|
||||
is WalletCardState.HiddenContent -> {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) {
|
||||
Text(
|
||||
text = it.title,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body2,
|
||||
maxLines = 1,
|
||||
)
|
||||
private fun Title(state: WalletCardState, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
|
||||
) {
|
||||
TitleText(title = state.title)
|
||||
|
||||
AnimatedVisibility(visible = state is WalletCardState.HiddenContent, label = "Update the hidden icon") {
|
||||
when (state) {
|
||||
is WalletCardState.HiddenContent -> {
|
||||
Icon(
|
||||
modifier = Modifier.size(size = TangemTheme.dimens.size20),
|
||||
painter = painterResource(id = R.drawable.ic_eye_off_24),
|
||||
|
|
@ -102,16 +127,63 @@ private fun Title(state: WalletCardState) {
|
|||
tint = TangemTheme.colors.icon.informative,
|
||||
)
|
||||
}
|
||||
is WalletCardState.Content,
|
||||
is WalletCardState.Error,
|
||||
is WalletCardState.Loading,
|
||||
-> Unit
|
||||
}
|
||||
is WalletCardState.Content,
|
||||
is WalletCardState.Error,
|
||||
is WalletCardState.Loading,
|
||||
-> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TitleText(title: String) {
|
||||
Text(
|
||||
text = title,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body2,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
private fun Balance(state: WalletCardState, modifier: Modifier = Modifier) {
|
||||
AnimatedContent(
|
||||
targetState = state,
|
||||
label = "Update the balance",
|
||||
modifier = modifier,
|
||||
) { walletCardState ->
|
||||
when (walletCardState) {
|
||||
is WalletCardState.Content -> {
|
||||
ResizableText(
|
||||
text = walletCardState.balance,
|
||||
fontSizeRange = FontSizeRange(min = 16.sp, max = TangemTheme.typography.h2.fontSize),
|
||||
modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size32),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.h2,
|
||||
)
|
||||
}
|
||||
is WalletCardState.Loading -> {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.size(
|
||||
width = TangemTheme.dimens.size102,
|
||||
height = TangemTheme.dimens.size32,
|
||||
),
|
||||
)
|
||||
}
|
||||
is WalletCardState.HiddenContent -> {
|
||||
Text(
|
||||
text = it.title,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body2,
|
||||
maxLines = 1,
|
||||
text = WalletCardState.HIDDEN_BALANCE_TEXT.resolveReference(),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.h2,
|
||||
)
|
||||
}
|
||||
is WalletCardState.Error -> {
|
||||
Text(
|
||||
text = WalletCardState.EMPTY_BALANCE_TEXT.resolveReference(),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.h2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -120,55 +192,34 @@ private fun Title(state: WalletCardState) {
|
|||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
private fun Balance(state: WalletCardState) {
|
||||
AnimatedContent(targetState = state, label = "Update the balance") {
|
||||
when (it) {
|
||||
is WalletCardState.Content -> {
|
||||
ResizableText(
|
||||
text = it.balance,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.h2,
|
||||
fontSizeRange = FontSizeRange(min = 16.sp, max = TangemTheme.typography.h2.fontSize),
|
||||
modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size32),
|
||||
private fun AdditionalInfo(state: WalletCardState, modifier: Modifier = Modifier) {
|
||||
AnimatedContent(
|
||||
targetState = state,
|
||||
label = "Update the additional text",
|
||||
modifier = modifier,
|
||||
) { walletCardState ->
|
||||
when (walletCardState) {
|
||||
is WalletCardState.AdditionalTextAvailability -> {
|
||||
Text(
|
||||
text = walletCardState.additionalInfo.resolveReference(),
|
||||
color = TangemTheme.colors.text.disabled,
|
||||
style = TangemTheme.typography.caption,
|
||||
)
|
||||
}
|
||||
is WalletCardState.Loading -> {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.size(
|
||||
width = TangemTheme.dimens.size102,
|
||||
height = TangemTheme.dimens.size24,
|
||||
width = TangemTheme.dimens.size84,
|
||||
height = TangemTheme.dimens.size16,
|
||||
),
|
||||
)
|
||||
}
|
||||
is WalletCardState.HiddenContent -> {
|
||||
Text(
|
||||
text = DOTS,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.h2,
|
||||
)
|
||||
}
|
||||
is WalletCardState.Error -> {
|
||||
Text(
|
||||
text = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.h2,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AdditionalInfo(description: String) {
|
||||
Text(
|
||||
text = description,
|
||||
color = TangemTheme.colors.text.disabled,
|
||||
style = TangemTheme.typography.caption,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WalletImage(@DrawableRes id: Int?, modifier: Modifier = Modifier) {
|
||||
private fun Image(@DrawableRes id: Int?, modifier: Modifier = Modifier) {
|
||||
AnimatedVisibility(visible = id != null, modifier = modifier) {
|
||||
Image(
|
||||
painter = painterResource(id = requireNotNull(id)),
|
||||
|
|
@ -180,11 +231,14 @@ private fun WalletImage(@DrawableRes id: Int?, modifier: Modifier = Modifier) {
|
|||
|
||||
// region Preview
|
||||
|
||||
@Preview(widthDp = 360, heightDp = 360)
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_WalletCard_LightTheme(@PreviewParameter(WalletCardStateProvider::class) state: WalletCardState) {
|
||||
private fun Preview_WalletCard_LightTheme(
|
||||
@PreviewParameter(WalletCardStateProvider::class)
|
||||
state: WalletCardState,
|
||||
) {
|
||||
TangemTheme(isDark = false) {
|
||||
WalletCard(state = state, modifier = Modifier.fillMaxWidth())
|
||||
WalletCard(state = state)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import com.tangem.common.Provider
|
|||
import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.common.CardTypesResolver
|
||||
import com.tangem.domain.tokens.model.TokenList
|
||||
import com.tangem.domain.tokens.model.TokenList.FiatBalance
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
|
@ -15,36 +15,59 @@ internal class FiatBalanceToWalletCardConverter(
|
|||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val isLockedState: Boolean,
|
||||
private val isWalletContentHidden: Boolean,
|
||||
) : Converter<TokenList.FiatBalance, WalletCardState> {
|
||||
) : Converter<FiatBalance, WalletCardState> {
|
||||
|
||||
override fun convert(value: TokenList.FiatBalance): WalletCardState {
|
||||
val additionalInfo = WalletAdditionalInfoFactory.resolve(
|
||||
cardTypesResolver = cardTypeResolverProvider(),
|
||||
isLocked = isLockedState,
|
||||
)
|
||||
override fun convert(value: FiatBalance): WalletCardState {
|
||||
return when (value) {
|
||||
is TokenList.FiatBalance.Loading -> with(currentState) {
|
||||
WalletCardState.Loading(id, title, additionalInfo, imageResId, onClick)
|
||||
}
|
||||
is TokenList.FiatBalance.Failed -> with(currentState) {
|
||||
WalletCardState.Error(id, title, additionalInfo, imageResId, onClick)
|
||||
}
|
||||
is TokenList.FiatBalance.Loaded -> with(currentState) {
|
||||
if (isWalletContentHidden) {
|
||||
WalletCardState.HiddenContent(id, title, additionalInfo, imageResId, onClick)
|
||||
} else {
|
||||
val appCurrency = appCurrencyProvider()
|
||||
is FiatBalance.Loading -> currentState.toLoadingWalletCardState()
|
||||
is FiatBalance.Failed -> currentState.toErrorWalletCardState()
|
||||
is FiatBalance.Loaded -> value.convertToWalletCardState()
|
||||
}
|
||||
}
|
||||
|
||||
WalletCardState.Content(
|
||||
id = id,
|
||||
title = title,
|
||||
additionalInfo = additionalInfo,
|
||||
imageResId = imageResId,
|
||||
onClick = onClick,
|
||||
balance = formatFiatAmount(value.amount, appCurrency.code, appCurrency.symbol),
|
||||
)
|
||||
}
|
||||
}
|
||||
private fun WalletCardState.toLoadingWalletCardState(): WalletCardState {
|
||||
return WalletCardState.Loading(id, title, imageResId, onClick)
|
||||
}
|
||||
|
||||
private fun WalletCardState.toErrorWalletCardState(): WalletCardState {
|
||||
return WalletCardState.Error(
|
||||
id = id,
|
||||
title = title,
|
||||
imageResId = imageResId,
|
||||
onClick = onClick,
|
||||
additionalInfo = WalletAdditionalInfoFactory.resolve(
|
||||
cardTypesResolver = cardTypeResolverProvider(),
|
||||
isLocked = isLockedState,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun FiatBalance.Loaded.convertToWalletCardState(): WalletCardState {
|
||||
return if (isWalletContentHidden) {
|
||||
WalletCardState.HiddenContent(
|
||||
id = currentState.id,
|
||||
title = currentState.title,
|
||||
imageResId = currentState.imageResId,
|
||||
onClick = currentState.onClick,
|
||||
)
|
||||
} else {
|
||||
val appCurrency = appCurrencyProvider()
|
||||
|
||||
WalletCardState.Content(
|
||||
id = currentState.id,
|
||||
title = currentState.title,
|
||||
additionalInfo = WalletAdditionalInfoFactory.resolve(
|
||||
cardTypesResolver = cardTypeResolverProvider(),
|
||||
isLocked = isLockedState,
|
||||
),
|
||||
imageResId = currentState.imageResId,
|
||||
onClick = currentState.onClick,
|
||||
balance = formatFiatAmount(
|
||||
fiatAmount = this.amount,
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,10 +4,8 @@ import com.tangem.common.Provider
|
|||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.common.CardTypesResolver
|
||||
import com.tangem.domain.tokens.model.TokenList
|
||||
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.utils.TokenListToWalletStateConverter.TokensListModel
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
|
||||
|
|
@ -35,7 +33,7 @@ internal class TokenListToWalletStateConverter(
|
|||
return state.copy(
|
||||
walletsListConfig = state.updateSelectedWallet(fiatBalance = value.tokenList.totalFiatBalance),
|
||||
pullToRefreshConfig = if (value.isRefreshing) {
|
||||
state.pullToRefreshConfig.copy(isRefreshing = state.getRefreshingStatus())
|
||||
state.pullToRefreshConfig.copy(isRefreshing = getRefreshingStatus(tokenList = value.tokenList))
|
||||
} else {
|
||||
state.pullToRefreshConfig
|
||||
},
|
||||
|
|
@ -60,17 +58,8 @@ internal class TokenListToWalletStateConverter(
|
|||
)
|
||||
}
|
||||
|
||||
private fun WalletState.getRefreshingStatus(): Boolean {
|
||||
return if (this is WalletMultiCurrencyState.Content &&
|
||||
this.tokensListState is WalletTokensListState.ContentState
|
||||
) {
|
||||
tokensListState.items.any { tokensListItemState ->
|
||||
tokensListItemState is WalletTokensListState.TokensListItemState.Token &&
|
||||
tokensListItemState.state is TokenItemState.Loading
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
private fun getRefreshingStatus(tokenList: TokenList): Boolean {
|
||||
return tokenList.totalFiatBalance is TokenList.FiatBalance.Loading
|
||||
}
|
||||
|
||||
data class TokensListModel(val tokenList: TokenList, val isRefreshing: Boolean)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,10 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.viewmodels
|
||||
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.domain.common.CardTypesResolver
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.NetworkGroup
|
||||
import com.tangem.domain.tokens.model.TokenList
|
||||
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
|
@ -26,7 +21,6 @@ import kotlinx.coroutines.flow.flow
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class WalletNotificationsListFactory(
|
||||
private val currentStateProvider: Provider<WalletState>,
|
||||
private val wasCardScannedCallback: suspend (String) -> Boolean,
|
||||
private val isUserAlreadyRateAppCallback: suspend () -> Boolean,
|
||||
private val isDemoCardCallback: (String) -> Boolean,
|
||||
|
|
@ -56,7 +50,7 @@ internal class WalletNotificationsListFactory(
|
|||
add(element = WalletNotification.DemoCard)
|
||||
}
|
||||
|
||||
if (hasUnreachableNetworks()) {
|
||||
if (hasUnreachableNetworks(tokenList)) {
|
||||
add(element = WalletNotification.UnreachableNetworks)
|
||||
}
|
||||
|
||||
|
|
@ -112,15 +106,22 @@ internal class WalletNotificationsListFactory(
|
|||
}
|
||||
}
|
||||
|
||||
private fun hasUnreachableNetworks(): Boolean {
|
||||
val isUnreachableState = { item: WalletTokensListState.TokensListItemState ->
|
||||
(item as? WalletTokensListState.TokensListItemState.Token)?.state is TokenItemState.Unreachable
|
||||
}
|
||||
|
||||
return currentStateProvider().let { state ->
|
||||
state is WalletMultiCurrencyState.Content &&
|
||||
state.tokensListState is WalletTokensListState.ContentState &&
|
||||
state.tokensListState.items.any(isUnreachableState)
|
||||
private fun hasUnreachableNetworks(tokenList: TokenList?): Boolean {
|
||||
return when (tokenList) {
|
||||
is TokenList.GroupedByNetwork -> {
|
||||
tokenList.groups
|
||||
.flatMap(NetworkGroup::currencies)
|
||||
.map(CryptoCurrencyStatus::value)
|
||||
.any { it is CryptoCurrencyStatus.Unreachable }
|
||||
}
|
||||
is TokenList.Ungrouped -> {
|
||||
tokenList.currencies
|
||||
.map(CryptoCurrencyStatus::value)
|
||||
.any { it is CryptoCurrencyStatus.Unreachable }
|
||||
}
|
||||
is TokenList.NotInitialized,
|
||||
null,
|
||||
-> false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
|
@ -83,7 +84,6 @@ internal class WalletViewModel @Inject constructor(
|
|||
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
|
||||
|
||||
private val notificationsListFactory = WalletNotificationsListFactory(
|
||||
currentStateProvider = Provider { uiState },
|
||||
wasCardScannedCallback = getCardWasScannedUseCase::invoke,
|
||||
isUserAlreadyRateAppCallback = isUserAlreadyRateAppUseCase::invoke,
|
||||
isDemoCardCallback = isDemoCardUseCase::invoke,
|
||||
|
|
@ -148,7 +148,7 @@ internal class WalletViewModel @Inject constructor(
|
|||
when {
|
||||
getWallet(index).isLocked -> uiState = stateFactory.getLockedState()
|
||||
cardTypeResolver.isMultiwalletAllowed() -> updateMultiCurrencyContent(index, isRefreshing)
|
||||
!cardTypeResolver.isMultiwalletAllowed() -> updateSingleCurrencyContent(index)
|
||||
!cardTypeResolver.isMultiwalletAllowed() -> updateSingleCurrencyContent(index, isRefreshing)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -157,7 +157,7 @@ internal class WalletViewModel @Inject constructor(
|
|||
"Impossible to update tokens list if state isn't WalletMultiCurrencyState"
|
||||
}
|
||||
|
||||
getTokenListUseCase(userWalletId = state.walletsListConfig.wallets[index].id)
|
||||
getTokenListUseCase(userWalletId = state.walletsListConfig.wallets[index].id, refresh = isRefreshing)
|
||||
.distinctUntilChanged()
|
||||
.onEach { tokenListEither ->
|
||||
uiState = stateFactory.getStateByTokensList(
|
||||
|
|
@ -175,13 +175,13 @@ internal class WalletViewModel @Inject constructor(
|
|||
.saveIn(tokensJobHolder)
|
||||
}
|
||||
|
||||
private fun updateSingleCurrencyContent(index: Int) {
|
||||
private fun updateSingleCurrencyContent(index: Int, isRefreshing: Boolean) {
|
||||
val wallet = getWallet(index)
|
||||
updateTxHistory(
|
||||
blockchain = getCardTypeResolver(index).getBlockchain(),
|
||||
derivationStyle = wallet.scanResponse.derivationStyleProvider.getDerivationStyle(),
|
||||
)
|
||||
updateMarketPrice(userWalletId = wallet.walletId)
|
||||
updateMarketPrice(userWalletId = wallet.walletId, isRefreshing = isRefreshing)
|
||||
updateNotifications(index)
|
||||
}
|
||||
|
||||
|
|
@ -209,10 +209,16 @@ internal class WalletViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun updateMarketPrice(userWalletId: UserWalletId) {
|
||||
// It also update wallet balance
|
||||
private fun updateMarketPrice(userWalletId: UserWalletId, isRefreshing: Boolean) {
|
||||
getPrimaryCurrencyUseCase(userWalletId = userWalletId)
|
||||
.distinctUntilChanged()
|
||||
.onEach { uiState = stateFactory.getSingleCurrencyLoadedBalanceState(cryptoCurrencyEither = it) }
|
||||
.onEach {
|
||||
uiState = stateFactory.getSingleCurrencyLoadedBalanceState(
|
||||
cryptoCurrencyEither = it,
|
||||
isRefreshing = isRefreshing,
|
||||
)
|
||||
}
|
||||
.flowOn(dispatchers.io)
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(marketPriceJobHolder)
|
||||
|
|
@ -336,7 +342,10 @@ internal class WalletViewModel @Inject constructor(
|
|||
val cacheState = WalletStateCache.getState(userWalletId = state.walletsListConfig.wallets[index].id)
|
||||
if (cacheState != null) {
|
||||
uiState = if (cacheState is WalletState.ContentState) {
|
||||
cacheState.copySealed(walletsListConfig = state.walletsListConfig.copy(selectedWalletIndex = index))
|
||||
cacheState.copySealed(
|
||||
walletsListConfig = state.walletsListConfig.copy(selectedWalletIndex = index),
|
||||
pullToRefreshConfig = state.pullToRefreshConfig.copy(isRefreshing = false),
|
||||
)
|
||||
} else {
|
||||
cacheState
|
||||
}
|
||||
|
|
@ -366,19 +375,27 @@ internal class WalletViewModel @Inject constructor(
|
|||
tokensListState is WalletTokensListState.Loading || hasLoadingTokens
|
||||
}
|
||||
is WalletSingleCurrencyState -> {
|
||||
txHistoryState is TxHistoryState.Loading || marketPriceBlockState is MarketPriceBlockState.Loading
|
||||
this is WalletSingleCurrencyState.Content && marketPriceBlockState is MarketPriceBlockState.Loading ||
|
||||
txHistoryState is TxHistoryState.Loading
|
||||
}
|
||||
is WalletState.Initial -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRefreshSwipe() {
|
||||
uiState = stateFactory.getStateAfterContentRefreshing()
|
||||
if (uiState is WalletState.Initial || uiState is WalletLockedState) return
|
||||
|
||||
updateContentItems(
|
||||
index = requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex,
|
||||
isRefreshing = true,
|
||||
)
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
uiState = stateFactory.getStateAfterContentRefreshing()
|
||||
|
||||
// TODO: [REDACTED_JIRA]
|
||||
delay(timeMillis = 500)
|
||||
|
||||
updateContentItems(
|
||||
index = requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex,
|
||||
isRefreshing = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOrganizeTokensClick() {
|
||||
|
|
@ -397,6 +414,7 @@ internal class WalletViewModel @Inject constructor(
|
|||
uiState = stateFactory.getStateAfterContentRefreshing()
|
||||
updateSingleCurrencyContent(
|
||||
index = requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex,
|
||||
isRefreshing = true,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue