Updated on 2026-08-14

This commit is contained in:
Tangem 2023-08-18 15:33:13 +03:00
commit f2e19e40be
35 changed files with 1034 additions and 370 deletions

View file

@ -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?) {

View file

@ -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
}
}

View file

@ -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"

View file

@ -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),
),
),
),
)
}
}
}

View file

@ -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))
}
}

View file

@ -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))

View file

@ -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(

View file

@ -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)

View file

@ -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)
}
}

View file

@ -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(

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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)
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels
interface TokenDetailsClickIntents {
fun onBackClick()
fun onMoreClick()
}

View file

@ -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")
}
}

View file

@ -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(

View file

@ -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

View file

@ -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())
}
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}
}
}

View file

@ -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 = "") }
}
}

View file

@ -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 */

View file

@ -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 */

View file

@ -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

View file

@ -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) }
}
}

View file

@ -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,
)
}

View file

@ -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),
)
}

View file

@ -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,
),
)
}
}

View file

@ -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()) },
),
)
},
),
)
}
}

View file

@ -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> {

View file

@ -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)
}
}

View file

@ -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,
),
)
}
}
}

View file

@ -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)

View file

@ -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
}
}

View file

@ -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,
)
}