diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 28cd0f57de..e44d8366ab 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -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?) { diff --git a/app/src/main/java/com/tangem/tap/common/log/TimberFormatStrategy.kt b/app/src/main/java/com/tangem/tap/common/log/TimberFormatStrategy.kt new file mode 100644 index 0000000000..0a43f525d4 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/log/TimberFormatStrategy.kt @@ -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 + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/Retrofit.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/Retrofit.kt index 3d8b6f3930..830cc43c53 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/Retrofit.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/Retrofit.kt @@ -37,5 +37,8 @@ fun createNetworkLoggingInterceptor(): Interceptor { return LoggingInterceptor.Builder() .setLevel(Level.BODY) .log(Log.VERBOSE) + .tag(NETWORK_LOGS_TAG) .build() -} \ No newline at end of file +} + +private const val NETWORK_LOGS_TAG = "NetworkLogs" \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TxHistoryState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TxHistoryState.kt index 85c907187a..a35545faea 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TxHistoryState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TxHistoryState.kt @@ -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>) : TxHistoryState + sealed class ContentState(private val contentItems: Flow>) : TxHistoryState { + + /** Lambda be invoke when explore button was clicked */ + abstract val onExploreClick: () -> Unit + + /** Content items with [TxHistoryItemState.Title] */ + val items: Flow> + 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> = 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>) : ContentState(items) + data class Content( + override val onExploreClick: () -> Unit, + val contentItems: Flow>, + ) : 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> { + return flowOf( + value = PagingData.from( + data = listOf( + element = TxHistoryItemState.Transaction( + state = TransactionState.Loading(txHash = LOADING_TX_HASH), + ), + ), + ), + ) + } } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt index 92df3daeb9..bc8107d5f5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt @@ -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 = 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) : 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 } -/** 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)) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/WrappedList.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/WrappedList.kt index 6f529c9574..47be5e543d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/WrappedList.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/WrappedList.kt @@ -7,4 +7,8 @@ import androidx.compose.runtime.Immutable */ @JvmInline @Immutable -value class WrappedList(val data: List) : List by data \ No newline at end of file +value class WrappedList(val data: List) : List by data + +fun List.toWrappedList(): WrappedList = WrappedList(data = this) + +fun wrappedList(vararg elements: T): WrappedList = WrappedList(data = listOf(*elements)) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index 2a78274eb5..db128105fb 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -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( diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index a7c6440862..6ce4bed811 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -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) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt index d9e0a1bcf7..6a3e663b58 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt @@ -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() viewModel.router = this@TokenDetailsFragment.internalTokenDetailsRouter + LocalLifecycleOwner.current.lifecycle.addObserver(viewModel) TokenDetailsScreen(state = viewModel.uiState) } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index 47cef6133f..f5e6b62437 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -2,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( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt new file mode 100644 index 0000000000..5c3be27d6a --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -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, + private val appCurrencyProvider: Provider, +) : Converter, TokenDetailsState> { + + override fun convert(value: Either): 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) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt new file mode 100644 index 0000000000..3c685d3e9e --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -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 { + + 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) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt new file mode 100644 index 0000000000..b43c200507 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -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, + private val appCurrencyProvider: Provider, + 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, + ): TokenDetailsState { + return tokenDetailsLoadedBalanceConverter.convert(cryptoCurrencyEither) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt new file mode 100644 index 0000000000..e2735c652c --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -0,0 +1,8 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels + +interface TokenDetailsClickIntents { + + fun onBackClick() + + fun onMoreClick() +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 5dfeaf2b82..4f3b42bdda 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -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() - var uiState by mutableStateOf(getInitialState()) + private val marketPriceJobHolder = JobHolder() + + private val selectedAppCurrencyFlow: StateFlow = 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 { + 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") + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index d0bb5cc8e7..080d54bbc4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -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( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt index bb2883ed5f..214c651d7f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt @@ -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 diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt index affbdde057..699a8bea7d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt @@ -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()) } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletSingleCurrencyState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletSingleCurrencyState.kt index 8433b8d9f7..ab23400cca 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletSingleCurrencyState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletSingleCurrencyState.kt @@ -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 - /** 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, override val bottomSheetConfig: WalletBottomSheetConfig?, override val buttons: ImmutableList, - 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) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletState.kt index 9cc1f3fa27..c9c327f540 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletState.kt @@ -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) + } } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt index 2da80de62f..01134378a5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt @@ -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 = "—") } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletManageButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletManageButton.kt index 6d6055c045..e8745338ed 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletManageButton.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletManageButton.kt @@ -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 */ diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt index 99a59df14d..60abf781ae 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt @@ -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 */ diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTokensListState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTokensListState.kt index 1209a2da37..4df7b1d04d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTokensListState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTokensListState.kt @@ -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 = 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 diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRefreshStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRefreshStateConverter.kt new file mode 100644 index 0000000000..f442b7edd2 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRefreshStateConverter.kt @@ -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, + private val clickIntents: WalletClickIntents, +) : Converter { + + 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() + .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>() + .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>.mapPagingData( + transform: (TxHistoryItemState.Transaction) -> TxHistoryItemState, + ): Flow> { + return map { it.map(transform) } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt index c6d3e7fae9..a1f491073b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt @@ -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, private val cardTypeResolverProvider: Provider, private val appCurrencyProvider: Provider, -) : Converter, WalletSingleCurrencyState.Content> { +) : Converter { - override fun convert(value: Either): 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, + val isRefreshing: Boolean, + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt index f40a3b577a..db8c705554 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt @@ -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), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt index 9e274774b5..5b53bed190 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt @@ -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, 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, + isRefreshing: Boolean, ): WalletState { - return singleCurrencyLoadedBalanceConverter.convert(cryptoCurrencyEither) + return singleCurrencyLoadedBalanceConverter.convert( + value = WalletSingleCurrencyLoadedBalanceConverter.SingleCurrencyLoadedBalanceModel( + cryptoCurrencyEither = cryptoCurrencyEither, + isRefreshing = isRefreshing, + ), + ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt index ef8131d180..b9600ca87e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt @@ -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()) }, + ), + ) + }, + ), ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt index 1c4af33fac..8feafcee7c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt @@ -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>): TxHistoryState { return TxHistoryState.Content( - items = value + onExploreClick = clickIntents::onExploreClick, + contentItems = value .map { pagingData -> pagingData .map { 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.insertGroupTitle(): PagingData { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index ccff84d714..57d1ca7765 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -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) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt index 8d48f9b5ec..52542675be 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt @@ -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, private val isLockedState: Boolean, private val isWalletContentHidden: Boolean, -) : Converter { +) : Converter { - 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, + ), + ) } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt index fb21502fe6..7d29a08e9e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt @@ -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) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt index 11beecade5..fb70c489e4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt @@ -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, 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 } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index f81cd1f969..b6bca4b04a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -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 = 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, ) }