From c444f76c76728027baf7a39145926489b1ed8a68 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 15 Aug 2023 22:02:20 +0500 Subject: [PATCH 01/44] Updated on 2026-08-14 --- .../java/com/tangem/tap/TapApplication.kt | 3 +- .../tap/common/log/TimberFormatStrategy.kt | 63 +++++++++++++++++++ .../tangem/datasource/api/common/Retrofit.kt | 5 +- 3 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/common/log/TimberFormatStrategy.kt 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 From 4ad8c5bfc1abc312318044a89e2a029f858700eb Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 16 Aug 2023 21:37:49 +0800 Subject: [PATCH 02/44] Updated on 2026-08-14 --- .../transactions/state/TxHistoryState.kt | 91 +++++++------ .../presentation/common/WalletPreviewData.kt | 4 +- .../common/state/TokenItemState.kt | 3 +- .../wallet/state/WalletSingleCurrencyState.kt | 9 +- .../presentation/wallet/state/WalletState.kt | 24 +++- .../state/components/WalletManageButton.kt | 2 + .../state/components/WalletNotification.kt | 2 + .../state/components/WalletTokensListState.kt | 13 +- .../factory/WalletRefreshStateConverter.kt | 128 ++++++++++++++++++ ...letSingleCurrencyLoadedBalanceConverter.kt | 25 +++- .../factory/WalletSkeletonStateConverter.kt | 2 +- .../state/factory/WalletStateFactory.kt | 16 ++- .../WalletLoadingTxHistoryConverter.kt | 15 +- .../WalletTxHistoryItemFlowConverter.kt | 12 +- .../utils/TokenListToWalletStateConverter.kt | 17 +-- .../WalletNotificationsListFactory.kt | 33 ++--- .../wallet/viewmodels/WalletViewModel.kt | 46 +++++-- 17 files changed, 318 insertions(+), 124 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRefreshStateConverter.kt 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/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..62c0b8ed22 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 @@ -351,10 +351,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/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/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..0fe9e475c9 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRefreshStateConverter.kt @@ -0,0 +1,128 @@ +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(additionalInfo = ""), + pullToRefreshConfig = getPullToRefreshConfig(), + txHistoryState = getTxHistoryState(), + marketPriceBlockState = MarketPriceBlockState.Loading(currencyName = marketPriceBlockState.currencyName), + ) + } + + private fun WalletState.ContentState.getWalletsListConfig(additionalInfo: String? = null): 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, + additionalInfo = additionalInfo ?: selectedWallet.additionalInfo, + 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..7e84f3f652 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), ) } @@ -153,4 +165,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..f452ed705f 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 @@ -47,7 +47,7 @@ internal class WalletSkeletonStateConverter( topBarConfig = createTopBarConfig(), walletsListConfig = createWalletsListConfig(value), pullToRefreshConfig = createPullToRefreshConfig(), - tokensListState = WalletTokensListState.Loading, + tokensListState = WalletTokensListState.Loading(), notifications = persistentListOf(), bottomSheetConfig = null, ) 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..b3fbee3db3 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,7 +1,10 @@ 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 @@ -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 }, 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, ) } From 33909502ed79022ee38ea93e20b19db9d825bbc9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 16 Aug 2023 14:39:38 +0800 Subject: [PATCH 03/44] Updated on 2026-08-14 --- .../core/ui/extensions/TextReference.kt | 40 ++- .../tangem/core/ui/extensions/WrappedList.kt | 6 +- .../core/ui/utils/BigDecimalFormatter.kt | 14 +- .../presentation/common/WalletPreviewData.kt | 7 +- .../domain/WalletAdditionalInfoFactory.kt | 76 ++++-- .../state/components/WalletCardState.kt | 53 ++-- .../factory/WalletRefreshStateConverter.kt | 5 +- ...letSingleCurrencyLoadedBalanceConverter.kt | 4 +- .../factory/WalletSkeletonStateConverter.kt | 5 - .../WalletTxHistoryItemFlowConverter.kt | 8 +- .../wallet/ui/components/common/WalletCard.kt | 234 +++++++++++------- .../utils/FiatBalanceToWalletCardConverter.kt | 79 +++--- 12 files changed, 338 insertions(+), 193 deletions(-) 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/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 62c0b8ed22..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, ) 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/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/factory/WalletRefreshStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRefreshStateConverter.kt index 0fe9e475c9..f442b7edd2 100644 --- 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 @@ -48,14 +48,14 @@ internal class WalletRefreshStateConverter( private fun WalletSingleCurrencyState.Content.getRefreshState(): WalletSingleCurrencyState.Content { return copy( // TODO: [REDACTED_JIRA] - walletsListConfig = getWalletsListConfig(additionalInfo = ""), + walletsListConfig = getWalletsListConfig(), pullToRefreshConfig = getPullToRefreshConfig(), txHistoryState = getTxHistoryState(), marketPriceBlockState = MarketPriceBlockState.Loading(currencyName = marketPriceBlockState.currencyName), ) } - private fun WalletState.ContentState.getWalletsListConfig(additionalInfo: String? = null): WalletsListConfig { + private fun WalletState.ContentState.getWalletsListConfig(): WalletsListConfig { val selectedWallet = walletsListConfig.wallets[walletsListConfig.selectedWalletIndex] return walletsListConfig.copy( @@ -66,7 +66,6 @@ internal class WalletRefreshStateConverter( element = WalletCardState.Loading( id = selectedWallet.id, title = selectedWallet.title, - additionalInfo = additionalInfo ?: selectedWallet.additionalInfo, imageResId = selectedWallet.imageResId, ), ), 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 7e84f3f652..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 @@ -94,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, ) @@ -114,7 +113,6 @@ internal class WalletSingleCurrencyLoadedBalanceConverter( WalletCardState.Error( id = selectedWallet.id, title = selectedWallet.title, - additionalInfo = selectedWallet.additionalInfo, imageResId = selectedWallet.imageResId, onClick = selectedWallet.onClick, ) 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 f452ed705f..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 @@ -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/txhistory/WalletTxHistoryItemFlowConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt index b3fbee3db3..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 @@ -9,13 +9,13 @@ 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 @@ -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 From 72a136515794d0b7b21ccecdc619d1290c9e0d7d Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 17 Aug 2023 17:26:28 +0500 Subject: [PATCH 04/44] Updated on 2026-08-14 --- features/tokendetails/impl/build.gradle.kts | 17 ++- .../presentation/TokenDetailsFragment.kt | 2 + .../tokendetails/TokenDetailsPreviewData.kt | 16 +-- .../TokenDetailsLoadedBalanceConverter.kt | 130 ++++++++++++++++++ .../TokenDetailsSkeletonStateConverter.kt | 46 +++++++ .../state/factory/TokenDetailsStateFactory.kt | 40 ++++++ .../viewmodels/TokenDetailsClickIntents.kt | 8 ++ .../viewmodels/TokenDetailsViewModel.kt | 107 ++++++++------ 8 files changed, 312 insertions(+), 54 deletions(-) create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt 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 From ec24e9e4eb78ab805377702700aad0081289ae72 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 18 Aug 2023 15:34:08 +0300 Subject: [PATCH 05/44] Updated on 2026-08-14 --- gradle/dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 3d58165539..0588397643 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -80,9 +80,9 @@ okHttp-prettyLogging = "3.1.0" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_4.10-321" +tangemBlockchainSdk = "develop-322" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_4.10-290" +tangemCardSdk = "develop-289" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds # endregion Tangem From f4ab3116fe6bfdeff4b1e35fbd0540ba34360fa1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 16 Aug 2023 16:53:42 +0300 Subject: [PATCH 06/44] Updated on 2026-08-14 --- .../MockSelectedAppCurrencyStore.kt | 26 ------------------- .../appcurrency/SelectedAppCurrencyStore.kt | 2 ++ .../DefaultSelectedAppCurrencyStore.kt | 7 ++++- .../core/KeylessDataStoreDecorator.kt | 6 ++--- .../DefaultAppCurrencyRepository.kt | 21 ++++++++++----- .../repository/DefaultCurrenciesRepository.kt | 4 +-- 6 files changed, 27 insertions(+), 39 deletions(-) delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/MockSelectedAppCurrencyStore.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/MockSelectedAppCurrencyStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/MockSelectedAppCurrencyStore.kt deleted file mode 100644 index 58f48aacc8..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/MockSelectedAppCurrencyStore.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.tangem.datasource.local.appcurrency - -import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf - -// TODO: Will be implemented in [REDACTED_TASK_KEY] task -internal class MockSelectedAppCurrencyStore : SelectedAppCurrencyStore { - - override fun get(): Flow { - return flowOf( - CurrenciesResponse.Currency( - id = "usd", - code = "USD", - name = "US Dollar", - unit = "$", - type = "fiat", - rateBTC = "", - ), - ) - } - - override suspend fun store(item: CurrenciesResponse.Currency) { - /* no-op */ - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/SelectedAppCurrencyStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/SelectedAppCurrencyStore.kt index e1fbad8329..80b95f3fb2 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/SelectedAppCurrencyStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/SelectedAppCurrencyStore.kt @@ -8,4 +8,6 @@ interface SelectedAppCurrencyStore { fun get(): Flow suspend fun store(item: CurrenciesResponse.Currency) + + suspend fun isEmpty(): Boolean } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/DefaultSelectedAppCurrencyStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/DefaultSelectedAppCurrencyStore.kt index c79bf3045b..7cdf82b1d7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/DefaultSelectedAppCurrencyStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/DefaultSelectedAppCurrencyStore.kt @@ -7,4 +7,9 @@ import com.tangem.datasource.local.datastore.core.StringKeyDataStore internal class DefaultSelectedAppCurrencyStore( dataStore: StringKeyDataStore, -) : SelectedAppCurrencyStore, KeylessDataStoreDecorator(dataStore) \ No newline at end of file +) : SelectedAppCurrencyStore, KeylessDataStoreDecorator(dataStore) { + + override suspend fun isEmpty(): Boolean { + return getSyncOrNull() == null + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/KeylessDataStoreDecorator.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/KeylessDataStoreDecorator.kt index 3d567967cf..5e74e2f386 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/KeylessDataStoreDecorator.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/KeylessDataStoreDecorator.kt @@ -10,15 +10,15 @@ internal abstract class KeylessDataStoreDecorator( return STRING_KEY } - fun get(): Flow { + open fun get(): Flow { return get(Unit) } - suspend fun getSyncOrNull(): Value? { + open suspend fun getSyncOrNull(): Value? { return getSyncOrNull(Unit) } - suspend fun store(item: Value) { + open suspend fun store(item: Value) { store(Unit, item) } diff --git a/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt b/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt index 8a780ba151..c2fe02a2b5 100644 --- a/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt +++ b/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt @@ -10,9 +10,9 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEmpty +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.joda.time.Duration import timber.log.Timber @@ -27,11 +27,18 @@ internal class DefaultAppCurrencyRepository( private val appCurrencyConverter = AppCurrencyConverter() - override fun getSelectedAppCurrency(): Flow { - return selectedAppCurrencyStore.get() - .onEmpty { fetchDefaultAppCurrency() } - .map(appCurrencyConverter::convert) - .flowOn(dispatchers.io) + override fun getSelectedAppCurrency(): Flow = channelFlow { + launch(dispatchers.io) { + selectedAppCurrencyStore.get() + .map(appCurrencyConverter::convert) + .collect(::send) + } + + launch(dispatchers.io) { + if (selectedAppCurrencyStore.isEmpty()) { + fetchDefaultAppCurrency() + } + } } override suspend fun getAvailableAppCurrencies(): List { diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 36abfd1d54..b2ff23a3bc 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -8,8 +8,8 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.core.error.DataError import com.tangem.domain.common.util.derivationStyleProvider +import com.tangem.domain.core.error.DataError import com.tangem.domain.demo.DemoConfig import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -164,7 +164,7 @@ internal class DefaultCurrenciesRepository( tangemTechApi.saveUserTokens(userWallet.walletId.stringValue, response) } else { - throw error + Timber.e(error, "Unable to fetch currencies for: ${userWallet.walletId}") } } From c7b11501e7ea81e9cc0ed2f61a7637a044cbcb72 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 21 Aug 2023 10:45:29 +0300 Subject: [PATCH 07/44] Updated on 2026-08-14 --- .../tap/common/extensions/WalletManager.kt | 2 +- .../domain/tasks/product/ScanProductTask.kt | 139 ++++++++++++------ .../common/configs/Wallet2CardConfig.kt | 5 + .../common/extensions/WalletManagerFactory.kt | 62 ++++++-- gradle/dependencies.toml | 2 +- 5 files changed, 151 insertions(+), 59 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt index 6665cab485..1533313216 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt @@ -82,7 +82,7 @@ fun WalletManager?.getAddressData(): WalletDataModel.AddressData? { } fun WalletManager.Companion.stub(): T { - val wallet = Wallet(Blockchain.Unknown, setOf(), Wallet.PublicKey(byteArrayOf(), null, null), setOf()) + val wallet = Wallet(Blockchain.Unknown, setOf(), Wallet.PublicKey(byteArrayOf(), null), setOf()) return object : WalletManager(wallet) { override val currentHost: String = "" override suspend fun update() {} diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt index d04cb9a62e..c45f4e3490 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt @@ -1,5 +1,6 @@ package com.tangem.tap.domain.tasks.product +import com.tangem.blockchain.blockchains.cardano.CardanoUtils import com.tangem.blockchain.common.Blockchain import com.tangem.common.CompletionResult import com.tangem.common.card.Card @@ -217,7 +218,8 @@ private class ScanWalletProcessor( walletData = session.environment.walletData, primaryCard = primaryCard, ) - val derivations = collectDerivations(card, config, scanResponse.derivationStyleProvider) + val derivations = + collectDerivations(card, config, scanResponse.derivationStyleProvider) if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) { callback(CompletionResult.Success(scanResponse)) return@launch @@ -242,60 +244,101 @@ private class ScanWalletProcessor( val userTokensRepository = userTokensRepository ?: return emptyList() val blockchainsToDerive = userTokensRepository.loadBlockchainsToDerive(card) .toMutableList() - .ifEmpty { - mutableListOf( - BlockchainNetwork( - blockchain = Blockchain.Bitcoin, - derivationStyleProvider = derivationStyleProvider, - ), - BlockchainNetwork( - blockchain = Blockchain.Ethereum, - derivationStyleProvider = derivationStyleProvider, - ), - ) - } + .ifEmpty { getDefaultBlockchains(derivationStyleProvider) } if (card.settings.isHDWalletAllowed) { - blockchainsToDerive.addAll( - listOf( - BlockchainNetwork( - blockchain = Blockchain.Ethereum, - derivationStyleProvider = derivationStyleProvider, - ), - BlockchainNetwork( - blockchain = Blockchain.EthereumTestnet, - derivationStyleProvider = derivationStyleProvider, - ), - ), - ) + blockchainsToDerive += getEthereumBlockchains(derivationStyleProvider) } - if (additionalBlockchainsToDerive != null) { - blockchainsToDerive.addAll( - additionalBlockchainsToDerive.map { - BlockchainNetwork( - blockchain = it, - derivationStyleProvider = derivationStyleProvider, - ) - }, - ) + + additionalBlockchainsToDerive?.let { + blockchainsToDerive += getAdditionalBlockchainToDerive(derivationStyleProvider, it) } + + // we should generate second key for cardano + // because cardano address generation for wallet2 requires keys from 2 derivations + // https://developers.cardano.org/docs/get-started/cardano-serialization-lib/generating-keys/ + val secondCardanoNetwork = blockchainsToDerive + .find { it.blockchain == Blockchain.Cardano } + ?.let { getCardanoSecondNetwork(it) } + secondCardanoNetwork?.let { blockchainsToDerive.add(it) } + + // pay attention to this if (!card.useOldStyleDerivation) { - blockchainsToDerive.removeAll( - listOf( - Blockchain.BSC, Blockchain.BSCTestnet, - Blockchain.Polygon, Blockchain.PolygonTestnet, - Blockchain.RSK, - Blockchain.Fantom, Blockchain.FantomTestnet, - Blockchain.Avalanche, Blockchain.AvalancheTestnet, - ).map { - BlockchainNetwork( - blockchain = it, - derivationStyleProvider = derivationStyleProvider, - ) - }, + removeUnnecessaryBlockchains(blockchainsToDerive, derivationStyleProvider) + } + + return blockchainsToDerive.distinct() + } + + private fun getDefaultBlockchains( + derivationStyleProvider: DerivationStyleProvider, + ): MutableList { + return mutableListOf( + BlockchainNetwork( + blockchain = Blockchain.Bitcoin, + derivationStyleProvider = derivationStyleProvider, + ), + BlockchainNetwork( + blockchain = Blockchain.Ethereum, + derivationStyleProvider = derivationStyleProvider, + ), + ) + } + + private fun getEthereumBlockchains(derivationStyleProvider: DerivationStyleProvider): List { + return listOf( + BlockchainNetwork( + blockchain = Blockchain.Ethereum, + derivationStyleProvider = derivationStyleProvider, + ), + BlockchainNetwork( + blockchain = Blockchain.EthereumTestnet, + derivationStyleProvider = derivationStyleProvider, + ), + ) + } + + private fun getAdditionalBlockchainToDerive( + derivationStyleProvider: DerivationStyleProvider, + collection: Collection, + ): List { + return collection.map { + BlockchainNetwork( + blockchain = it, + derivationStyleProvider = derivationStyleProvider, ) } - return blockchainsToDerive.distinct() + } + + private fun getCardanoSecondNetwork(cardanoBlockchainNetwork: BlockchainNetwork): BlockchainNetwork? { + val cardanoStandardDerivation = cardanoBlockchainNetwork.derivationPath?.let { DerivationPath(it) } + ?: return null + val cardanoPatchedDerivation = CardanoUtils.extendedDerivationPath(cardanoStandardDerivation) + return BlockchainNetwork( + blockchain = Blockchain.Cardano, + derivationPath = cardanoPatchedDerivation.rawPath, + tokens = emptyList(), + ) + } + + private fun removeUnnecessaryBlockchains( + blockchainsToDerive: MutableList, + derivationStyleProvider: DerivationStyleProvider, + ) { + blockchainsToDerive.removeAll( + listOf( + Blockchain.BSC, Blockchain.BSCTestnet, + Blockchain.Polygon, Blockchain.PolygonTestnet, + Blockchain.RSK, + Blockchain.Fantom, Blockchain.FantomTestnet, + Blockchain.Avalanche, Blockchain.AvalancheTestnet, + ).map { + BlockchainNetwork( + blockchain = it, + derivationStyleProvider = derivationStyleProvider, + ) + }, + ) } private suspend fun collectDerivations( diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/configs/Wallet2CardConfig.kt b/domain/legacy/src/main/java/com/tangem/domain/common/configs/Wallet2CardConfig.kt index 12f3f24dd3..231e7d7bed 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/configs/Wallet2CardConfig.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/configs/Wallet2CardConfig.kt @@ -16,6 +16,7 @@ object Wallet2CardConfig : CardConfig { /** * Logic to determine primary curve for blockchain in TangemWallet 2.0 + * Order is important here */ override fun primaryCurve(blockchain: Blockchain): EllipticCurve? { // order is important, new curve is preferred for wallet 2 @@ -29,6 +30,10 @@ object Wallet2CardConfig : CardConfig { blockchain.getSupportedCurves().contains(EllipticCurve.Bls12381G2Aug) -> { EllipticCurve.Bls12381G2Aug } + // only for support cardano on Wallet2 + blockchain.getSupportedCurves().contains(EllipticCurve.Ed25519) -> { + EllipticCurve.Ed25519 + } else -> { Timber.e("Unsupported blockchain, curve not found") null diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/WalletManagerFactory.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/WalletManagerFactory.kt index f65ca67b3e..551f75119b 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/WalletManagerFactory.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/WalletManagerFactory.kt @@ -1,10 +1,13 @@ package com.tangem.domain.common.extensions +import com.tangem.blockchain.blockchains.cardano.CardanoUtils import com.tangem.blockchain.common.* import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toMapKey +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation import com.tangem.domain.common.configs.CardConfig @@ -12,6 +15,7 @@ import com.tangem.domain.common.configs.Wallet2CardConfig import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse +import com.tangem.blockchain.common.CardanoAddressConfig fun WalletManagerFactory.makeWalletManagerForApp( scanResponse: ScanResponse, @@ -45,18 +49,19 @@ fun WalletManagerFactory.makeWalletManagerForApp( } scanResponse.card.settings.isHDWalletAllowed && seedKey != null && derivationParams != null -> { val derivedKeys = scanResponse.derivedKeys[wallet.publicKey.toMapKey()] - val derivationPath = when (derivationParams) { - is DerivationParams.Default -> blockchain.derivationPath(derivationParams.style) - is DerivationParams.Custom -> derivationParams.path - } - val derivedKey = derivedKeys?.get(derivationPath) - ?: return null + val derivationPath = derivationParams.getPath(blockchain) + + val publicKey = makePublicKey( + seedKey = wallet.publicKey, + blockchain = blockchain, + derivationPath = derivationPath ?: return null, + derivedWalletKeys = derivedKeys ?: return null, + isWallet2 = scanResponse.cardTypesResolver.isWallet2(), + ) createWalletManager( blockchain = environmentBlockchain, - seedKey = wallet.publicKey, - derivedKey = derivedKey, - derivation = derivationParams, + publicKey = publicKey, ) } else -> { @@ -69,6 +74,45 @@ fun WalletManagerFactory.makeWalletManagerForApp( } } +private fun makePublicKey( + seedKey: ByteArray, + blockchain: Blockchain, + derivationPath: DerivationPath, + derivedWalletKeys: Map, + isWallet2: Boolean, +): Wallet.PublicKey { + val derivedKey = derivedWalletKeys[derivationPath] ?: error("No derivation found") + + val derivationKey = Wallet.HDKey( + path = derivationPath, + extendedPublicKey = derivedKey, + ) + + // we should generate second key for cardano + // because cardano address generation for wallet2 requires keys from 2 derivations + // https://developers.cardano.org/docs/get-started/cardano-serialization-lib/generating-keys/ + if (blockchain == Blockchain.Cardano) { + CardanoAddressConfig.useExtendedAddressing = isWallet2 + + if (isWallet2) { + val extendedDerivationPath = CardanoUtils.extendedDerivationPath(derivationPath) + val secondDerivedKey = derivedWalletKeys[extendedDerivationPath] ?: error("No derivation found") + + val secondDerivationKey = Wallet.HDKey(secondDerivedKey, extendedDerivationPath) + + return Wallet.PublicKey( + seedKey = seedKey, + derivationType = Wallet.PublicKey.DerivationType.Double(derivationKey, secondDerivationKey), + ) + } + } + + return Wallet.PublicKey( + seedKey = seedKey, + derivationType = Wallet.PublicKey.DerivationType.Plain(derivationKey), + ) +} + private fun getDerivationParams(card: CardDTO): DerivationParams? { return if (!card.settings.isHDWalletAllowed) { null diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 0588397643..e2d2bf63ad 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -80,7 +80,7 @@ okHttp-prettyLogging = "3.1.0" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-322" +tangemBlockchainSdk = "develop-325" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-289" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds From f089d311060e1778affbabd0cef4816a81c05b4b Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 21 Aug 2023 11:39:42 +0300 Subject: [PATCH 08/44] Updated on 2026-08-14 --- .../com/tangem/core/ui/event/EventEffect.kt | 23 +++ .../com/tangem/core/ui/event/StateEvent.kt | 51 +++++ .../presentation/common/WalletPreviewData.kt | 6 +- .../presentation/organizetokens/Intents.kt | 28 +++ .../organizetokens/OrganizeTokensIntents.kt | 14 -- .../organizetokens/OrganizeTokensScreen.kt | 38 ++-- .../OrganizeTokensStateHolder.kt | 34 +++- .../organizetokens/OrganizeTokensViewModel.kt | 23 ++- .../model/OrganizeTokensState.kt | 2 + .../utils/common/DraggableItemsOperations.kt | 177 +----------------- .../utils/common/TokenListOperations.kt | 8 +- .../items/TokenListToListStateConverter.kt | 5 +- .../utils/dnd/DragAndDropAdapter.kt | 169 +++++++++++++++++ .../utils/dnd/DraggableGroupsOperations.kt | 106 +++++++++++ 14 files changed, 475 insertions(+), 209 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/event/EventEffect.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/event/StateEvent.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/Intents.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensIntents.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/event/EventEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/event/EventEffect.kt new file mode 100644 index 0000000000..49abda9299 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/event/EventEffect.kt @@ -0,0 +1,23 @@ +package com.tangem.core.ui.event + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.NonRestartableComposable + +/** + * A Composable function that reacts to a given [StateEvent], executing the provided action only once when the event + * is triggered. + * + * @param event The [StateEvent] to listen to. + * @param onTrigger The action to execute when the event is triggered. + */ +@Composable +@NonRestartableComposable +fun EventEffect(event: StateEvent, onTrigger: suspend () -> Unit) { + LaunchedEffect(event) { + if (event is StateEvent.Triggered) { + onTrigger() + event.consume() + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/event/StateEvent.kt b/core/ui/src/main/java/com/tangem/core/ui/event/StateEvent.kt new file mode 100644 index 0000000000..d0f495b944 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/event/StateEvent.kt @@ -0,0 +1,51 @@ +package com.tangem.core.ui.event + +import androidx.compose.runtime.Immutable + +/** + * Represents compose state event, which can be consumed or triggered. + * + * This is especially useful for handling one-off UI events like showing snack bars or navigation which should not be + * re-triggered on recompositions or state changes. + */ +@Immutable +sealed class StateEvent { + + /** Defines the action to be executed when the event is consumed. */ + protected abstract val onConsume: () -> Unit + + /** + * Represents an already consumed state event. + * Events of this type will not trigger any further actions. + */ + object Consumed : StateEvent() { + override val onConsume: () -> Unit = {} + } + + /** + * Represents a state event that has been triggered but not yet consumed. + * + * @property onConsume The action to be executed when the event is consumed. + */ + data class Triggered(override val onConsume: () -> Unit) : StateEvent() + + /** + * Consumes the event, triggering any associated action. + */ + fun consume() { + onConsume() + } +} + +/** + * Creates a [StateEvent.Triggered] instance. + * + * @param onConsume The action to be executed when the event is consumed. + * @return A triggered state event. + */ +fun triggered(onConsume: () -> Unit): StateEvent.Triggered = StateEvent.Triggered(onConsume) + +/** + * Represents a statically defined [StateEvent.Consumed] event. + */ +val consumed: StateEvent.Consumed = StateEvent.Consumed \ 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 080d54bbc4..68df79d297 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 @@ -6,6 +6,7 @@ import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.marketprice.PriceChangeConfig import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.event.consumed import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.common.state.TokenItemState @@ -145,7 +146,7 @@ internal object WalletPreviewData { private const val networksSize = 10 private const val tokensSize = 3 - val draggableItems by lazy { + private val draggableItems by lazy { List(networksSize) { it } .flatMap { index -> val lastNetworkIndex = networksSize - 1 @@ -194,7 +195,7 @@ internal object WalletPreviewData { .toPersistentList() } - val draggableTokens by lazy { + private val draggableTokens by lazy { draggableItems .filterIsInstance() .toMutableList() @@ -224,6 +225,7 @@ internal object WalletPreviewData { onApplyClick = {}, onCancelClick = {}, ), + scrollListToTop = consumed, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/Intents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/Intents.kt new file mode 100644 index 0000000000..e0f4868347 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/Intents.kt @@ -0,0 +1,28 @@ +package com.tangem.feature.wallet.presentation.organizetokens + +import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem +import org.burnoutcrew.reorderable.ItemPosition + +internal interface OrganizeTokensIntents { + + fun onBackClick() + + fun onSortClick() + + fun onGroupClick() + + fun onApplyClick() + + fun onCancelClick() +} + +internal interface DragAndDropIntents { + + fun onItemDragged(from: ItemPosition, to: ItemPosition) + + fun canDragItemOver(dragOver: ItemPosition, dragging: ItemPosition): Boolean + + fun onItemDraggingStart(item: DraggableItem) + + fun onItemDraggingEnd() +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensIntents.kt deleted file mode 100644 index 84ce246245..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensIntents.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens - -internal interface OrganizeTokensIntents { - - fun onBackClick() - - fun onSortClick() - - fun onGroupClick() - - fun onApplyClick() - - fun onCancelClick() -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt index beb7709ffb..cd55c2f577 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.organizetokens import androidx.activity.compose.BackHandler import androidx.compose.animation.core.* +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* @@ -26,6 +27,7 @@ import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.components.buttons.actions.RoundedActionButton +import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.impl.R @@ -64,6 +66,10 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier }, containerColor = TangemTheme.colors.background.secondary, ) + + EventEffect(state.scrollListToTop) { + tokensListState.animateScrollToItem(index = 0) + } } @Composable @@ -74,11 +80,16 @@ private fun TokenList( modifier: Modifier = Modifier, ) { Box(modifier = modifier) { + val onDragEnd: (Int, Int) -> Unit = remember { + { _, _ -> + dndConfig.onItemDragEnd() + } + } val reorderableListState = rememberReorderableLazyListState( onMove = dndConfig.onItemDragged, listState = listState, canDragOver = dndConfig.canDragItemOver, - onDragEnd = { _, _ -> dndConfig.onItemDragEnd() }, + onDragEnd = onDragEnd, ) val items = state.items @@ -109,11 +120,6 @@ private fun TokenList( reorderableState = reorderableListState, onDragStart = onDragStart, ) - - if (item is DraggableItem.GroupPlaceholder) { - // This item should be displayed in the list but remain invisible - Box(modifier = Modifier.fillMaxWidth()) - } } } @@ -121,6 +127,7 @@ private fun TokenList( } } +@OptIn(ExperimentalFoundationApi::class) @Composable private fun LazyItemScope.DraggableItem( index: Int, @@ -129,15 +136,13 @@ private fun LazyItemScope.DraggableItem( onDragStart: () -> Unit, ) { ReorderableItem( - reorderableState = reorderableState, + defaultDraggingModifier = Modifier.animateItemPlacement( + animationSpec = tween(easing = LinearOutSlowInEasing), + ), + state = reorderableState, index = index, key = item.id, ) { isDragging -> - - if (isDragging) { - onDragStart() - } - val itemModifier = Modifier.applyShapeAndShadow(item.roundingMode, item.showShadow) when (item) { @@ -151,7 +156,14 @@ private fun LazyItemScope.DraggableItem( state = item.tokenItemState, reorderableTokenListState = reorderableState, ) - is DraggableItem.GroupPlaceholder -> Unit + // Should be presented in the list but remain invisible + is DraggableItem.GroupPlaceholder -> Box(modifier = Modifier.fillMaxWidth()) + } + + LaunchedEffect(isDragging) { + if (isDragging) { + onDragStart() + } } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt index 14fecec78d..ec8c8655c2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt @@ -1,6 +1,8 @@ package com.tangem.feature.wallet.presentation.organizetokens import com.tangem.common.Provider +import com.tangem.core.ui.event.consumed +import com.tangem.core.ui.event.triggered import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.TokenListSortingError @@ -19,6 +21,7 @@ import kotlinx.coroutines.flow.* internal class OrganizeTokensStateHolder( private val intents: OrganizeTokensIntents, + private val dragAndDropIntents: DragAndDropIntents, private val appCurrencyProvider: Provider, private val onSubscription: () -> Unit, stateFlowScope: CoroutineScope, @@ -60,6 +63,14 @@ internal class OrganizeTokensStateHolder( updateState { tokenListConverter.convert(tokenList) } } + fun updateStateAfterTokenListSorting(tokenList: TokenList) { + updateState { + tokenListConverter.convert(tokenList).copy( + scrollListToTop = triggered(::consumeScrollListToTopEvent), + ) + } + } + fun updateStateToDisplayProgress() { updateState { inProgressStateConverter.convert(value = this) } } @@ -68,6 +79,15 @@ internal class OrganizeTokensStateHolder( updateState { inProgressStateConverter.convertBack(value = this) } } + fun updateStateWithManualSorting(itemsState: OrganizeTokensListState) { + updateState { + copy( + header = header.copy(isSortedByBalance = false), + itemsState = itemsState, + ) + } + } + fun updateStateWithError(error: TokenListError) { updateState { tokenListErrorConverter.convert(error) } } @@ -88,17 +108,21 @@ internal class OrganizeTokensStateHolder( onApplyClick = intents::onApplyClick, onCancelClick = intents::onCancelClick, ), - // TODO: Will be added in next MR dndConfig = OrganizeTokensState.DragAndDropConfig( - onItemDragged = { _, _ -> }, - onDragStart = { }, - onItemDragEnd = { }, - canDragItemOver = { _, _ -> false }, + onItemDragged = dragAndDropIntents::onItemDragged, + onDragStart = dragAndDropIntents::onItemDraggingStart, + onItemDragEnd = dragAndDropIntents::onItemDraggingEnd, + canDragItemOver = dragAndDropIntents::canDragItemOver, ), + scrollListToTop = consumed, ) } private fun updateState(block: OrganizeTokensState.() -> OrganizeTokensState) { stateFlowInternal.update(block) } + + private fun consumeScrollListToTopEvent() { + updateState { copy(scrollListToTop = consumed) } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt index 0313bf1e7a..5c24a976b3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt @@ -16,6 +16,8 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState import com.tangem.feature.wallet.presentation.organizetokens.utils.CryptoCurrenciesIdsResolver +import com.tangem.feature.wallet.presentation.organizetokens.utils.common.disableSortingByBalance +import com.tangem.feature.wallet.presentation.organizetokens.utils.dnd.DragAndDropAdapter import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.router.WalletRoute import dagger.hilt.android.lifecycle.HiltViewModel @@ -39,12 +41,19 @@ internal class OrganizeTokensViewModel @Inject constructor( private val selectedAppCurrencyFlow = createSelectedAppCurrencyFlow() + private val dragAndDropAdapter = DragAndDropAdapter( + listStateProvider = Provider { uiState.value.itemsState }, + scope = viewModelScope, + ) + private val stateHolder = OrganizeTokensStateHolder( stateFlowScope = viewModelScope, intents = this, + dragAndDropIntents = dragAndDropAdapter, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), onSubscription = { bootstrapTokenList() + bootstrapDragAndDropUpdates() }, ) @@ -69,7 +78,7 @@ internal class OrganizeTokensViewModel @Inject constructor( toggleTokenListSortingUseCase(list).fold( ifLeft = stateHolder::updateStateWithError, ifRight = { - stateHolder.updateStateWithTokenList(it) + stateHolder.updateStateAfterTokenListSorting(it) tokenList = it }, ) @@ -83,7 +92,7 @@ internal class OrganizeTokensViewModel @Inject constructor( toggleTokenListGroupingUseCase(list).fold( ifLeft = stateHolder::updateStateWithError, ifRight = { - stateHolder.updateStateWithTokenList(it) + stateHolder.updateStateAfterTokenListSorting(it) tokenList = it }, ) @@ -133,6 +142,16 @@ internal class OrganizeTokensViewModel @Inject constructor( } } + private fun bootstrapDragAndDropUpdates() { + dragAndDropAdapter.stateFlow + .distinctUntilChanged() + .onEach { + stateHolder.updateStateWithManualSorting(it) + tokenList = tokenList?.disableSortingByBalance() + } + .launchIn(viewModelScope) + } + private fun createSelectedAppCurrencyFlow(): StateFlow { return getSelectedAppCurrencyUseCase() .map { maybeAppCurrency -> diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt index 30d8e846c1..860c4c5b24 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.organizetokens.model import androidx.compose.runtime.Immutable +import com.tangem.core.ui.event.StateEvent import org.burnoutcrew.reorderable.ItemPosition @Immutable @@ -10,6 +11,7 @@ internal data class OrganizeTokensState( val header: HeaderConfig, val actions: ActionsConfig, val dndConfig: DragAndDropConfig, + val scrollListToTop: StateEvent, ) { data class HeaderConfig( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt index 74ada001e8..fee7cb1033 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt @@ -1,184 +1,27 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.common import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import kotlinx.collections.immutable.PersistentList -import org.burnoutcrew.reorderable.ItemPosition -internal fun List.findItemsToMove( - moveOverItemKey: Any?, - movedItemKey: Any?, -): Pair { - var moveOverItem: DraggableItem? = null - var movedItem: DraggableItem? = null - - for (item in this) { - if (item.id == moveOverItemKey) { - moveOverItem = item - } - if (item.id == movedItemKey) { - movedItem = item - } - if (moveOverItem != null && movedItem != null) { - break - } - } - - return Pair(moveOverItem, movedItem) -} - -internal fun checkCanMoveHeaderOver( - moveOverItemPosition: ItemPosition, - moveOverItem: DraggableItem, - lastItemIndex: Int, -): Boolean { - // Group item can be moved only to group divider or to ages of the items list - return when { - moveOverItemPosition.index == 0 -> true - moveOverItemPosition.index == lastItemIndex -> true - moveOverItem is DraggableItem.GroupPlaceholder -> true - else -> false - } -} - -internal fun checkCanMoveTokenOver(item: DraggableItem.Token, moveOverItem: DraggableItem): Boolean { - // Token item can be moved only in its group - return when (moveOverItem) { - is DraggableItem.GroupHeader -> false // Token item can not be moved to group item - is DraggableItem.Token -> item.groupId == moveOverItem.groupId // Token item can not be moved over its group - is DraggableItem.GroupPlaceholder -> false - } -} - -internal fun PersistentList.moveItem(fromIndex: Int, toIndex: Int): PersistentList { - val fromItem = this[fromIndex] - return this - .removeAt(fromIndex) - .add(toIndex, fromItem) -} - -internal fun List.divideItems(movingItem: DraggableItem): List { - return this.map { - it - .updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true)) - .updateShadowVisibility(show = it.id == movingItem.id) - } -} - -@Suppress("UNCHECKED_CAST") // Erased type -internal fun List.uniteItems(): List { +internal fun List.uniteItems(): List { val lastItemIndex = this.lastIndex return this.mapIndexed { index, item -> val mode = when (index) { 0 -> DraggableItem.RoundingMode.Top() lastItemIndex -> DraggableItem.RoundingMode.Bottom() - else -> DraggableItem.RoundingMode.None + else -> when (item) { + is DraggableItem.GroupHeader -> DraggableItem.RoundingMode.Top(showGap = true) + is DraggableItem.Token -> if (this[index + 1] is DraggableItem.GroupPlaceholder) { + DraggableItem.RoundingMode.Bottom(showGap = true) + } else { + DraggableItem.RoundingMode.None + } + is DraggableItem.GroupPlaceholder -> DraggableItem.RoundingMode.None + } } item .updateRoundingMode(mode) .updateShadowVisibility(show = false) - } as List -} - -// TODO: Move to domain -@Volatile -private var groupIdToTokens: Map>? = null - -internal fun List.collapseGroup(group: DraggableItem.GroupHeader): List { - if (!groupIdToTokens.isNullOrEmpty()) return this - - groupIdToTokens = this - .asSequence() - .filterIsInstance() - .groupBy { it.groupId } - - return this - .filterNot { it is DraggableItem.Token && it.groupId == group.id } - .divideGroups(group) -} - -internal fun List.expandGroups(): List { - if (groupIdToTokens.isNullOrEmpty()) return this - - val currentGroups = this.filterIsInstance() - val lastGroupIndex = currentGroups.lastIndex - - return currentGroups - .flatMapIndexed { index, group -> - buildList { - add(group) - addAll(groupIdToTokens?.get(group.id).orEmpty()) - if (index != lastGroupIndex) { - add(DraggableItem.GroupPlaceholder(id = "group_divider_$index")) - } - } - } - .uniteItems() - .also { groupIdToTokens = null } -} - -/** - * Applies the correct [DraggableItem.RoundingMode] and shadow status to each item in the list, - * based on the relationship of each item to the [movingItem] and its position in the list. - * - * @param movingItem The item that is being dragged/moved. - * @return A list of [DraggableItem]s with updated rounding modes and shadow statuses. - */ -internal fun List.divideGroups(movingItem: DraggableItem): List { - val lastItemIndex = this.lastIndex - - return this.mapIndexed { index, item -> - when { - // Case when current item is the moving item - item.id == movingItem.id -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true)) - .updateShadowVisibility(show = true) - } - // Case when moving item is a token and current item is the group of the moving token - movingItem is DraggableItem.Token && item.id == movingItem.groupId -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true)) - .updateShadowVisibility(show = true) - } - // Case when both moving item and current item are tokens and belong to the same group - movingItem is DraggableItem.Token && - item is DraggableItem.Token && item.groupId == movingItem.groupId -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true)) - .updateShadowVisibility(show = false) - } - // Case when current item is the first item in the list - index == 0 -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.Top()) - .updateShadowVisibility(show = false) - } - // Case when current item is the last item in the list - index == lastItemIndex -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.Bottom()) - .updateShadowVisibility(show = false) - } - // Case when previous item is a GroupPlaceholder - this[index - 1] is DraggableItem.GroupPlaceholder -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.Top(showGap = true)) - .updateShadowVisibility(show = false) - } - // Case when next item is a GroupPlaceholder - this[index + 1] is DraggableItem.GroupPlaceholder -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.Bottom(showGap = true)) - .updateShadowVisibility(show = false) - } - // Default case when none of the above conditions are met - else -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.None) - .updateShadowVisibility(show = false) - } - } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/TokenListOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/TokenListOperations.kt index 4f1478d2bf..4e4250f5e9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/TokenListOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/TokenListOperations.kt @@ -3,12 +3,10 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.common import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.model.TokenList.SortType -internal fun TokenList.updateSorting(isSortedByBalance: Boolean): TokenList { - val sortType = if (isSortedByBalance) SortType.BALANCE else SortType.NONE - +internal fun TokenList.disableSortingByBalance(): TokenList { return when (this) { - is TokenList.GroupedByNetwork -> this.copy(sortedBy = sortType) - is TokenList.Ungrouped -> this.copy(sortedBy = sortType) + is TokenList.GroupedByNetwork -> this.copy(sortedBy = SortType.NONE) + is TokenList.Ungrouped -> this.copy(sortedBy = SortType.NONE) is TokenList.NotInitialized -> this } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/TokenListToListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/TokenListToListStateConverter.kt index bb1b829d47..dd82c1672b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/TokenListToListStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/TokenListToListStateConverter.kt @@ -1,9 +1,11 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items import com.tangem.domain.tokens.model.TokenList +import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItems import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.toPersistentList internal class TokenListToListStateConverter( @@ -28,11 +30,12 @@ internal class TokenListToListStateConverter( ) } + @Suppress("UNCHECKED_CAST") // Erased type private fun createListState(tokenList: TokenList.Ungrouped): OrganizeTokensListState.Ungrouped { return OrganizeTokensListState.Ungrouped( items = tokensConverter.convertList(tokenList.currencies) .uniteItems() - .toPersistentList(), + .toPersistentList() as PersistentList, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt new file mode 100644 index 0000000000..c73798c77e --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt @@ -0,0 +1,169 @@ +package com.tangem.feature.wallet.presentation.organizetokens.utils.dnd + +import com.tangem.common.Provider +import com.tangem.feature.wallet.presentation.organizetokens.DragAndDropIntents +import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem +import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState +import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItems +import com.tangem.feature.wallet.presentation.organizetokens.utils.common.updateItems +import kotlinx.collections.immutable.mutate +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import org.burnoutcrew.reorderable.ItemPosition + +internal class DragAndDropAdapter( + private val listStateProvider: Provider, + private val scope: CoroutineScope, +) : DragAndDropIntents { + + private val draggableGroupsOperations = DraggableGroupsOperations() + + private val currentListState: OrganizeTokensListState + get() = listStateProvider.invoke() + + private val listStateFlowInternal: MutableSharedFlow = MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + + private var currentDraggingItem: DraggableItem? = null + + val stateFlow: Flow + get() = listStateFlowInternal + + override fun canDragItemOver(dragOver: ItemPosition, dragging: ItemPosition): Boolean { + val items = (currentListState as? OrganizeTokensListState.GroupedByNetwork) + ?.items + ?: return true // If ungrouped then item can be moved anywhere + + val (dragOverItem, draggingItem) = findItemsToMove( + items = items, + moveOverItemKey = dragOver.key, + movedItemKey = dragging.key, + ) + + if (dragOverItem == null || draggingItem == null) { + return false + } + + return when (draggingItem) { + is DraggableItem.GroupHeader -> checkCanMoveHeaderOver(dragOver, dragOverItem, items.lastIndex) + is DraggableItem.Token -> checkCanMoveTokenOver(draggingItem, dragOverItem) + is DraggableItem.GroupPlaceholder -> false + } + } + + override fun onItemDraggingStart(item: DraggableItem) { + if (currentDraggingItem != null) return + currentDraggingItem = item + + updateListState { + when (item) { + is DraggableItem.GroupPlaceholder -> items + is DraggableItem.GroupHeader -> draggableGroupsOperations.collapseGroup(items, item) + is DraggableItem.Token -> when (this) { + is OrganizeTokensListState.GroupedByNetwork -> draggableGroupsOperations.divideGroups(items, item) + is OrganizeTokensListState.Ungrouped -> divideTokens(items, item) + is OrganizeTokensListState.Empty -> items + } + } + } + } + + override fun onItemDraggingEnd() { + scope.launch(Dispatchers.IO) { + val draggingItem = currentDraggingItem ?: return@launch + + delay(FINISH_DRAGGING_DELAY_MILLIS) + + updateListState { + when (draggingItem) { + is DraggableItem.GroupHeader -> draggableGroupsOperations.expandGroups(items) + is DraggableItem.Token -> items.uniteItems() + is DraggableItem.GroupPlaceholder -> items + } + } + + currentDraggingItem = null + } + } + + override fun onItemDragged(from: ItemPosition, to: ItemPosition) = updateListState { + items.mutate { + it.add(to.index, it.removeAt(from.index)) + } + } + + private fun updateListState(block: OrganizeTokensListState.() -> List) { + val updatedState = currentListState.updateItems { block(currentListState) } + + listStateFlowInternal.tryEmit(updatedState) + } + + private fun findItemsToMove( + items: List, + moveOverItemKey: Any?, + movedItemKey: Any?, + ): Pair { + var moveOverItem: DraggableItem? = null + var movedItem: DraggableItem? = null + + for (item in items) { + if (item.id == moveOverItemKey) { + moveOverItem = item + } + if (item.id == movedItemKey) { + movedItem = item + } + if (moveOverItem != null && movedItem != null) { + break + } + } + + return Pair(moveOverItem, movedItem) + } + + private fun checkCanMoveHeaderOver( + moveOverItemPosition: ItemPosition, + moveOverItem: DraggableItem, + lastItemIndex: Int, + ): Boolean { + // Group item can be moved only to group divider or to ages of the items list + return when { + moveOverItemPosition.index == 0 -> true + moveOverItemPosition.index == lastItemIndex -> true + moveOverItem is DraggableItem.GroupPlaceholder -> true + else -> false + } + } + + private fun checkCanMoveTokenOver(item: DraggableItem.Token, moveOverItem: DraggableItem): Boolean { + // Token item can be moved only in its group + return when (moveOverItem) { + is DraggableItem.GroupHeader -> false // Token item can not be moved to group item + is DraggableItem.Token -> item.groupId == moveOverItem.groupId // Token item can not be moved over its group + is DraggableItem.GroupPlaceholder -> false + } + } + + @Suppress("UNCHECKED_CAST") // Erased type + private fun divideTokens( + items: List, + movingItem: DraggableItem.Token, + ): List { + return items.map { token -> + token + .updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true)) + .updateShadowVisibility(show = token.id == movingItem.id) + } as List + } + + private companion object { + const val FINISH_DRAGGING_DELAY_MILLIS = 200L + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt new file mode 100644 index 0000000000..d28133195a --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt @@ -0,0 +1,106 @@ +package com.tangem.feature.wallet.presentation.organizetokens.utils.dnd + +import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem +import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupPlaceholder +import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItems + +internal class DraggableGroupsOperations { + + private var groupIdToTokens: Map>? = null + + fun collapseGroup(items: List, movingGroup: DraggableItem.GroupHeader): List { + if (!groupIdToTokens.isNullOrEmpty()) return items + + groupIdToTokens = items + .asSequence() + .filterIsInstance() + .groupBy { it.groupId } + + val itemsWithoutGroupTokens = items.filterNot { + it is DraggableItem.Token && it.groupId == movingGroup.id + } + + return divideGroups(itemsWithoutGroupTokens, movingGroup) + } + + fun expandGroups(items: List): List { + if (groupIdToTokens.isNullOrEmpty()) return items + + val currentGroups = items.filterIsInstance() + val lastGroupIndex = currentGroups.lastIndex + + val expandedGroups = currentGroups + .flatMapIndexed { index, group -> + buildList { + add(group) + addAll(groupIdToTokens?.get(group.id).orEmpty()) + if (index != lastGroupIndex) { + add(getGroupPlaceholder(index)) + } + } + } + .uniteItems() + + groupIdToTokens = null + + return expandedGroups + } + + fun divideGroups(items: List, movingItem: DraggableItem): List { + val lastItemIndex = items.lastIndex + + return items.mapIndexed { index, item -> + when { + // Case when current item is the moving item + item.id == movingItem.id -> { + item + .updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true)) + .updateShadowVisibility(show = true) + } + // Case when moving item is a token and current item is the group of the moving token + movingItem is DraggableItem.Token && item.id == movingItem.groupId -> { + item + .updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true)) + .updateShadowVisibility(show = true) + } + // Case when both moving item and current item are tokens and belong to the same group + movingItem is DraggableItem.Token && + item is DraggableItem.Token && item.groupId == movingItem.groupId -> { + item + .updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true)) + .updateShadowVisibility(show = false) + } + // Case when current item is the first item in the list + index == 0 -> { + item + .updateRoundingMode(DraggableItem.RoundingMode.Top()) + .updateShadowVisibility(show = false) + } + // Case when current item is the last item in the list + index == lastItemIndex -> { + item + .updateRoundingMode(DraggableItem.RoundingMode.Bottom()) + .updateShadowVisibility(show = false) + } + // Case when previous item is a GroupPlaceholder + items[index - 1] is DraggableItem.GroupPlaceholder -> { + item + .updateRoundingMode(DraggableItem.RoundingMode.Top(showGap = true)) + .updateShadowVisibility(show = false) + } + // Case when next item is a GroupPlaceholder + items[index + 1] is DraggableItem.GroupPlaceholder -> { + item + .updateRoundingMode(DraggableItem.RoundingMode.Bottom(showGap = true)) + .updateShadowVisibility(show = false) + } + // Default case when none of the above conditions are met + else -> { + item + .updateRoundingMode(DraggableItem.RoundingMode.None) + .updateShadowVisibility(show = false) + } + } + } + } +} \ No newline at end of file From eda6a624d00abe9c5a3307b9441ce978e08da228 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 18 Aug 2023 12:48:55 +0300 Subject: [PATCH 09/44] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 3 +- .../converters/CryptoCurrencyConverter.kt | 2 +- .../repository/DefaultNetworksRepository.kt | 2 +- .../tokens/utils/CryptoCurrencyFactory.kt | 10 +-- .../data/tokens/utils/NetworkConverter.kt | 11 +-- .../data/tokens/utils/NetworkOperations.kt | 29 ++++++++ .../tokens/utils/ResponseCurrenciesFactory.kt | 10 ++- .../data/tokens/utils/TokensOperations.kt | 19 +----- .../tokens/utils/UserTokensResponseFactory.kt | 2 +- .../domain/tokens/models/CryptoCurrency.kt | 34 ++-------- .../tangem/domain/tokens/models/Network.kt | 67 ++++++++++++++++--- .../tokens/ToggleTokenListGroupingUseCase.kt | 21 ++---- .../tokens/ToggleTokenListSortingUseCase.kt | 3 +- .../error/mapper/TokenListErrorMappers.kt | 2 - .../mapper/TokenListSortingErrorMappers.kt | 1 - .../CurrenciesStatusesOperations.kt | 6 +- .../tokens/operations/TokenListOperations.kt | 30 +-------- .../operations/TokenListSortingOperations.kt | 29 +++----- .../domain/tokens/GetTokenListUseCaseTest.kt | 58 +--------------- .../tokens/ToggleTokenListGroupingTest.kt | 43 +----------- .../tangem/domain/tokens/mock/MockNetworks.kt | 6 ++ .../domain/tokens/mock/MockNetworksGroups.kt | 8 +-- .../tangem/domain/tokens/mock/MockTokens.kt | 34 +++------- .../domain/tokens/mock/MockTokensStates.kt | 2 +- .../TokenDetailsSkeletonStateConverter.kt | 4 +- .../CryptoCurrencyToDraggableItemConverter.kt | 2 +- 26 files changed, 161 insertions(+), 277 deletions(-) create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index d5ab303555..0acb93c93c 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -51,10 +51,9 @@ internal object TokensDomainModule { @Provides @ViewModelScoped fun provideToggleTokenListGroupingUseCase( - networksRepository: NetworksRepository, dispatchers: CoroutineDispatcherProvider, ): ToggleTokenListGroupingUseCase { - return ToggleTokenListGroupingUseCase(networksRepository, dispatchers) + return ToggleTokenListGroupingUseCase(dispatchers) } @Provides diff --git a/app/src/main/java/com/tangem/tap/features/wallet/converters/CryptoCurrencyConverter.kt b/app/src/main/java/com/tangem/tap/features/wallet/converters/CryptoCurrencyConverter.kt index 1cf1515b01..8b22dd95e6 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/converters/CryptoCurrencyConverter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/converters/CryptoCurrencyConverter.kt @@ -7,7 +7,7 @@ import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.store import com.tangem.utils.converter.Converter -class CryptoCurrencyConverter : Converter { +internal class CryptoCurrencyConverter : Converter { private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory() } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index e9e235c8d1..606fc9e94d 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -88,7 +88,7 @@ internal class DefaultNetworksRepository( private suspend fun fetchNetworkStatus(userWalletId: UserWalletId, networkId: Network.ID) { val currencies = getCurrencies(userWalletId) .asSequence() - .filter { it.networkId == networkId } + .filter { it.network.id == networkId } val result = walletManagersFacade.update( userWalletId = userWalletId, diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt index f4e0148b34..e6fff3176f 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt @@ -1,11 +1,12 @@ package com.tangem.data.tokens.utils import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token as SdkToken import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.tokens.models.CryptoCurrency import timber.log.Timber +import com.tangem.blockchain.common.Token as SdkToken +// FIXME: Make internal class CryptoCurrencyFactory { fun createToken( @@ -19,9 +20,10 @@ class CryptoCurrencyFactory { } val id = getTokenId(blockchain, sdkToken) + return CryptoCurrency.Token( id = id, - networkId = getNetworkId(blockchain), + network = getNetwork(blockchain) ?: return null, name = sdkToken.name, symbol = sdkToken.symbol, iconUrl = getTokenIconUrl(blockchain, sdkToken), @@ -29,8 +31,6 @@ class CryptoCurrencyFactory { isCustom = isCustomToken(id), contractAddress = sdkToken.contractAddress, derivationPath = getDerivationPath(blockchain, derivationStyleProvider), - blockchainName = blockchain.fullName, - standardType = getTokenStandardType(blockchain, sdkToken), ) } @@ -42,7 +42,7 @@ class CryptoCurrencyFactory { return CryptoCurrency.Coin( id = getCoinId(blockchain), - networkId = getNetworkId(blockchain), + network = getNetwork(blockchain) ?: return null, name = blockchain.fullName, symbol = blockchain.currency, iconUrl = getCoinIconUrl(blockchain), diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkConverter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkConverter.kt index 23e27fe466..6c57906a38 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkConverter.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkConverter.kt @@ -3,22 +3,13 @@ package com.tangem.data.tokens.utils import com.tangem.blockchain.common.Blockchain import com.tangem.domain.tokens.models.Network import com.tangem.utils.converter.Converter -import timber.log.Timber internal class NetworkConverter : Converter { override fun convert(value: Network.ID): Network? { val blockchain = Blockchain.fromId(value.value) - if (blockchain == Blockchain.Unknown) { - Timber.e("Unable to convert Unknown blockchain to the domain network model") - return null - } - - return Network( - id = value, - name = blockchain.fullName, - ) + return getNetwork(blockchain) } override fun convertList(input: Collection): List { diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt new file mode 100644 index 0000000000..3a536a46ed --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt @@ -0,0 +1,29 @@ +package com.tangem.data.tokens.utils + +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.tokens.models.Network +import timber.log.Timber + +internal fun getNetwork(blockchain: Blockchain): Network? { + if (blockchain == Blockchain.Unknown) { + Timber.e("Unable to convert Unknown blockchain to the domain network model") + return null + } + + return Network( + id = Network.ID(blockchain.id), + name = blockchain.fullName, + isTestnet = blockchain.isTestnet(), + standardType = getNetworkStandardType(blockchain), + ) +} + +private fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType { + return when (blockchain) { + Blockchain.Ethereum, Blockchain.EthereumTestnet -> Network.StandardType.ERC20 + Blockchain.BSC, Blockchain.BSCTestnet -> Network.StandardType.BEP20 + Blockchain.Binance, Blockchain.BinanceTestnet -> Network.StandardType.BEP2 + Blockchain.Tron, Blockchain.TronTestnet -> Network.StandardType.TRC20 + else -> Network.StandardType.Unspecified(blockchain.name) + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCurrenciesFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCurrenciesFactory.kt index b79f32f515..f3e7832865 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCurrenciesFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCurrenciesFactory.kt @@ -59,10 +59,10 @@ internal class ResponseCurrenciesFactory(private val demoConfig: DemoConfig) { } } - private fun createCoin(blockchain: Blockchain, responseToken: UserTokensResponse.Token): CryptoCurrency.Coin { + private fun createCoin(blockchain: Blockchain, responseToken: UserTokensResponse.Token): CryptoCurrency.Coin? { return CryptoCurrency.Coin( id = getCoinId(blockchain), - networkId = getNetworkId(blockchain), + network = getNetwork(blockchain) ?: return null, name = responseToken.name, symbol = responseToken.symbol, decimals = responseToken.decimals, @@ -71,12 +71,12 @@ internal class ResponseCurrenciesFactory(private val demoConfig: DemoConfig) { ) } - private fun createToken(blockchain: Blockchain, sdkToken: Token, derivationPath: String?): CryptoCurrency.Token { + private fun createToken(blockchain: Blockchain, sdkToken: Token, derivationPath: String?): CryptoCurrency.Token? { val id = getTokenId(blockchain, sdkToken) return CryptoCurrency.Token( id = id, - networkId = getNetworkId(blockchain), + network = getNetwork(blockchain) ?: return null, name = sdkToken.name, symbol = sdkToken.symbol, decimals = sdkToken.decimals, @@ -84,8 +84,6 @@ internal class ResponseCurrenciesFactory(private val demoConfig: DemoConfig) { iconUrl = getTokenIconUrl(blockchain, sdkToken), contractAddress = sdkToken.contractAddress, isCustom = isCustomToken(id), - blockchainName = blockchain.fullName, - standardType = getTokenStandardType(blockchain, sdkToken), ) } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt index 2298a28fb2..a95d6efa08 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt @@ -5,7 +5,6 @@ import com.tangem.blockchain.common.IconsUtil import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.common.extensions.toNetworkId -import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.CryptoCurrency.ID import com.tangem.domain.tokens.models.Network import com.tangem.blockchain.common.Token as SdkToken @@ -31,12 +30,6 @@ internal fun getBlockchain(networkId: Network.ID): Blockchain { return Blockchain.fromId(networkId.value) } -internal fun getNetworkId(blockchain: Blockchain): Network.ID { - val value = blockchain.id - - return Network.ID(value) -} - internal fun getCoinId(blockchain: Blockchain): ID { return getTokenOrCoinId(blockchain, token = null) } @@ -45,16 +38,6 @@ internal fun getTokenId(blockchain: Blockchain, token: SdkToken): ID { return getTokenOrCoinId(blockchain, token) } -internal fun getTokenStandardType(blockchain: Blockchain, token: SdkToken): CryptoCurrency.StandardType { - return when (blockchain) { - Blockchain.Ethereum, Blockchain.EthereumTestnet -> CryptoCurrency.StandardType.ERC20 - Blockchain.BSC, Blockchain.BSCTestnet -> CryptoCurrency.StandardType.BEP20 - Blockchain.Binance, Blockchain.BinanceTestnet -> CryptoCurrency.StandardType.BEP2 - Blockchain.Tron, Blockchain.TronTestnet -> CryptoCurrency.StandardType.TRC20 - else -> CryptoCurrency.StandardType.Unspecified(token.name) - } -} - internal fun getTokenIconUrl(blockchain: Blockchain, token: SdkToken): String? { val tokenId = token.id @@ -83,7 +66,7 @@ private fun getTokenOrCoinId(blockchain: Blockchain, token: SdkToken?): ID { else -> TOKEN_ID_PREFIX to CurrencyIdSuffix(rawId = sdkTokenId) } - return ID(prefix, getNetworkId(blockchain), suffix) + return ID(prefix, Network.ID(blockchain.id), suffix) } private fun getTokenIconUrlFromDefaultHost(tokenId: String): String { diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt index d112ad4476..658522a30e 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt @@ -27,7 +27,7 @@ internal class UserTokensResponseFactory { } private fun createResponseToken(currency: CryptoCurrency): UserTokensResponse.Token { - val blockchain = getBlockchain(currency.networkId) + val blockchain = getBlockchain(currency.network.id) return UserTokensResponse.Token( id = currency.id.rawCurrencyId, diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/CryptoCurrency.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/CryptoCurrency.kt index 6c46276300..4fc784b4a8 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/CryptoCurrency.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/CryptoCurrency.kt @@ -6,7 +6,7 @@ import java.io.Serializable * Represents a generic cryptocurrency. * * @property id Unique identifier for the cryptocurrency. - * @property networkId Identifier for the network to which the cryptocurrency belongs. + * @property network The network to which the cryptocurrency belongs. * @property name Human-readable name of the cryptocurrency. * @property symbol Symbol of the cryptocurrency. * @property decimals Number of decimal places used by the cryptocurrency. @@ -14,11 +14,11 @@ import java.io.Serializable * @property derivationPath Optional path used for key derivation. `null` if the wallet does not support the * [HD Wallet](https://coinsutra.com/hd-wallets-deterministic-wallet/) feature. */ -// TODO: [REDACTED_JIRA] delete serializable +// FIXME: Remove serialization [REDACTED_JIRA] sealed class CryptoCurrency : Serializable { abstract val id: ID - abstract val networkId: Network.ID + abstract val network: Network abstract val name: String abstract val symbol: String abstract val decimals: Int @@ -30,7 +30,7 @@ sealed class CryptoCurrency : Serializable { */ data class Coin( override val id: ID, - override val networkId: Network.ID, + override val network: Network, override val name: String, override val symbol: String, override val decimals: Int, @@ -51,7 +51,7 @@ sealed class CryptoCurrency : Serializable { */ data class Token( override val id: ID, - override val networkId: Network.ID, + override val network: Network, override val name: String, override val symbol: String, override val decimals: Int, @@ -59,8 +59,6 @@ sealed class CryptoCurrency : Serializable { override val derivationPath: String?, val contractAddress: String, val isCustom: Boolean, - val blockchainName: String, // TODO: Move this field to proper entity - val standardType: StandardType, // TODO: Move this field to proper entity ) : CryptoCurrency() { init { @@ -79,6 +77,7 @@ sealed class CryptoCurrency : Serializable { * @property rawCurrencyId Represents not unique currency ID from the blockchain network. `null` if * its ID of the custom token. */ + // FIXME: Remove serialization [REDACTED_JIRA] data class ID( private val prefix: Prefix, private val networkId: Network.ID, @@ -114,6 +113,7 @@ sealed class CryptoCurrency : Serializable { * * The suffix can either be a raw ID or a contract address. */ + // FIXME: Remove serialization [REDACTED_JIRA] sealed class Suffix : Serializable { /** The value of the suffix, which could be either a raw ID or a contract address. */ @@ -135,26 +135,6 @@ sealed class CryptoCurrency : Serializable { } } - sealed class StandardType { - abstract val name: String - - object ERC20 : StandardType() { - override val name: String = "ERC20" - } - object TRC20 : StandardType() { - override val name: String = "TRC20" - } - object BEP20 : StandardType() { - override val name: String = "BEP20" - } - object BEP2 : StandardType() { - override val name: String = "BEP2" - } - class Unspecified(val tokenName: String) : StandardType() { - override val name: String = tokenName - } - } - protected fun checkProperties() { require(name.isNotBlank()) { "Crypto currency name must not be blank" } require(symbol.isNotBlank()) { "Crypto currency symbol must not be blank" } diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt index 48ba69226f..05c043d7f1 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt @@ -1,29 +1,80 @@ package com.tangem.domain.tokens.models +import java.io.Serializable + /** - * Represents a blockchain network, identified by a unique ID and a human-readable name. + * Represents a blockchain network, identified by a unique ID, a human-readable name, and its standard type. * - * @property id The unique identifier of the network, encapsulated as an inline value class. + * This class encapsulates the primary details of a blockchain network, such as its ID, name, + * whether it operates as a test network, and the type of blockchain standard it conforms to + * (e.g., ERC20, BEP20). + * + * @property id The unique identifier of the network. * @property name The human-readable name of the network, such as "Ethereum" or "Bitcoin". - * - * @throws IllegalArgumentException If the name or ID is blank. + * @property isTestnet Indicates whether the network is a test network or a main network. + * @property standardType The type of blockchain standard the network adheres to. */ -data class Network(val id: ID, val name: String) { +// FIXME: Remove serialization [REDACTED_JIRA] +data class Network( + val id: ID, + val name: String, + val isTestnet: Boolean, + val standardType: StandardType, +) : Serializable { init { require(name.isNotBlank()) { "Network name must not be blank" } } /** - * Represents a unique identifier for a network. + * Represents a unique identifier for a blockchain network. * - * @property value The string value of the network ID. + * @property value The string representation of the network ID. */ + // FIXME: Remove serialization [REDACTED_JIRA] @JvmInline - value class ID(val value: String) { + value class ID(val value: String) : Serializable { init { require(value.isNotBlank()) { "Network ID must not be blank" } } } + + /** + * Represents the type of blockchain standard that a network adheres to. + * + * Blockchain networks often follow certain standards that dictate how tokens operate on them. + * These standards can define functionalities such as how transactions are processed, + * how tokens are minted or burned, and more. + * + * @property name The human-readable name of the standard type. + */ + sealed class StandardType { + abstract val name: String + + /** Represents the ERC20 token standard, common on the Ethereum network. */ + object ERC20 : StandardType() { + override val name: String = "ERC20" + } + + /** Represents the TRC20 token standard, common on the TRON network. */ + object TRC20 : StandardType() { + override val name: String = "TRC20" + } + + /** Represents the BEP20 token standard, common on the Binance Smart Chain network. */ + object BEP20 : StandardType() { + override val name: String = "BEP20" + } + + /** Represents the BEP2 token standard, common on the Binance Chain network. */ + object BEP2 : StandardType() { + override val name: String = "BEP2" + } + + /** Represents a network that does not adhere to a predefined standard type. */ + class Unspecified(val networkName: String) : StandardType() { + override val name: String = networkName + } + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt index 07f86f5069..6328868257 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt @@ -1,18 +1,18 @@ package com.tangem.domain.tokens import arrow.core.Either -import arrow.core.raise.* +import arrow.core.raise.Raise +import arrow.core.raise.either +import arrow.core.raise.ensure +import arrow.core.raise.withError import com.tangem.domain.tokens.error.TokenListSortingError import com.tangem.domain.tokens.error.mapper.mapToTokenListSortingError import com.tangem.domain.tokens.model.TokenList -import com.tangem.domain.tokens.models.Network import com.tangem.domain.tokens.operations.TokenListSortingOperations -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext class ToggleTokenListGroupingUseCase( - private val networksRepository: NetworksRepository, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -34,14 +34,10 @@ class ToggleTokenListGroupingUseCase( private fun Raise.groupTokens(tokenList: TokenList.Ungrouped): TokenList.GroupedByNetwork { val sortingOperations = TokenListSortingOperations(tokenList) - val tokens = withError(TokenListSortingOperations.Error::mapToTokenListSortingError) { - sortingOperations.getTokens().bind() - } - val networks = getNetworks(tokens.map { it.currency.networkId }.toSet()) return TokenList.GroupedByNetwork( groups = withError(TokenListSortingOperations.Error::mapToTokenListSortingError) { - sortingOperations.getGroupedTokens(networks).bind() + sortingOperations.getGroupedTokens().bind() }, totalFiatBalance = tokenList.totalFiatBalance, sortedBy = sortingOperations.getSortType(), @@ -61,11 +57,4 @@ class ToggleTokenListGroupingUseCase( sortedBy = sortingOperations.getSortType(), ) } - - private fun Raise.getNetworks(networksIds: Set): Set { - return catch( - block = { networksRepository.getNetworks(networksIds) }, - catch = { raise(TokenListSortingError.DataError(it)) }, - ) - } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt index 483b822928..d1ae3d9b74 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt @@ -36,11 +36,10 @@ class ToggleTokenListSortingUseCase( tokenList: TokenList.GroupedByNetwork, ): TokenList.GroupedByNetwork { val operations = getSortingOperations(tokenList) - val networks = tokenList.groups.map { it.network }.toSet() return tokenList.copy( groups = withError(TokenListSortingOperations.Error::mapToTokenListSortingError) { - operations.getGroupedTokens(networks).bind() + operations.getGroupedTokens().bind() }, sortedBy = operations.getSortType(), ) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt index 4796f50063..27b4e3956f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt @@ -20,7 +20,5 @@ internal fun TokenListOperations.Error.mapToTokenListError(): TokenListError { is TokenListOperations.Error.DataError -> TokenListError.DataError(this.cause) is TokenListOperations.Error.UnableToSortTokenList -> TokenListError.UnableToSortTokenList(this.unsortedTokenList) - is TokenListOperations.Error.UnableToGroupTokenList -> - TokenListError.UnableToSortTokenList(this.ungroupedTokenList) } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListSortingErrorMappers.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListSortingErrorMappers.kt index 2b492ad7ee..eefa5d1417 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListSortingErrorMappers.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListSortingErrorMappers.kt @@ -6,7 +6,6 @@ import com.tangem.domain.tokens.operations.TokenListSortingOperations internal fun TokenListSortingOperations.Error.mapToTokenListSortingError(): TokenListSortingError { return when (this) { is TokenListSortingOperations.Error.EmptyTokens -> TokenListSortingError.TokenListIsEmpty - is TokenListSortingOperations.Error.EmptyNetworks, is TokenListSortingOperations.Error.NetworkNotFound, -> TokenListSortingError.UnableToSortTokenList } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index 463205a41c..48ea420c61 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -105,7 +105,7 @@ internal class CurrenciesStatusesOperations( val statusFlow = getNetworksStatuses(networksIds) .map { maybeStatuses -> maybeStatuses.map { statuses -> - statuses.singleOrNull { it.networkId == currency.networkId } + statuses.singleOrNull { it.networkId == currency.network.id } } } @@ -129,7 +129,7 @@ internal class CurrenciesStatusesOperations( currencies.map { currency -> val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } - val networkStatus = networksStatuses?.firstOrNull { it.networkId == currency.networkId } + val networkStatus = networksStatuses?.firstOrNull { it.networkId == currency.network.id } createStatus(currency, quote, networkStatus, ignoreQuote = quotesRetrievingFailed) } @@ -205,7 +205,7 @@ internal class CurrenciesStatusesOperations( currencies: NonEmptyList, ): Pair, NonEmptySet> { val currencyIdToNetworkId = currencies.associate { currency -> - currency.id to currency.networkId + currency.id to currency.network.id } val currenciesIds = currencyIdToNetworkId.keys.toNonEmptySetOrNull() val networksIds = currencyIdToNetworkId.values.toNonEmptySetOrNull() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt index 627df695af..2fee6a75c0 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt @@ -5,16 +5,13 @@ import arrow.core.raise.* import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList -import com.tangem.domain.tokens.models.Network import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.* @Suppress("LongParameterList") internal class TokenListOperations( private val currenciesRepository: CurrenciesRepository, - private val networksRepository: NetworksRepository, private val userWalletId: UserWalletId, private val tokens: List, ) { @@ -25,7 +22,6 @@ internal class TokenListOperations( useCase: GetTokenListUseCase, ) : this( currenciesRepository = useCase.currenciesRepository, - networksRepository = useCase.networksRepository, userWalletId = userWalletId, tokens = tokens, ) @@ -70,37 +66,21 @@ internal class TokenListOperations( sortByBalance = isSortedByBalance, ) - return createTokenList(currencies, sortingOperations, fiatBalance, isGrouped) + return createTokenList(sortingOperations, fiatBalance, isGrouped) } private fun Raise.createTokenList( - tokens: NonEmptyList, sortingOperations: TokenListSortingOperations, fiatBalance: TokenList.FiatBalance, isGrouped: Boolean, ): TokenList { return if (isGrouped) { - val networks = ensureNotNull(getNetworks(tokens).toNonEmptySetOrNull()) { - Error.UnableToGroupTokenList( - ungroupedTokenList = createUngroupedTokenList(sortingOperations, fiatBalance), - ) - } - - createGroupedTokenList(sortingOperations, fiatBalance, networks) + createGroupedTokenList(sortingOperations, fiatBalance) } else { createUngroupedTokenList(sortingOperations, fiatBalance) } } - private fun Raise.getNetworks(tokensNes: NonEmptyList): Set { - val networksIds = tokensNes.map { it.currency.networkId }.toNonEmptySet() - - return catch( - block = { networksRepository.getNetworks(networksIds) }, - catch = { raise(Error.DataError(it)) }, - ) - } - private fun Raise.createUngroupedTokenList( sortingOperations: TokenListSortingOperations, fiatBalance: TokenList.FiatBalance, @@ -118,7 +98,6 @@ internal class TokenListOperations( private fun Raise.createGroupedTokenList( sortingOperations: TokenListSortingOperations, fiatBalance: TokenList.FiatBalance, - networks: NonEmptySet, ): TokenList.GroupedByNetwork = TokenList.GroupedByNetwork( sortedBy = sortingOperations.getSortType(), totalFiatBalance = fiatBalance, @@ -126,7 +105,7 @@ internal class TokenListOperations( transform = { e -> Error.fromTokenListOperations(e) { createUnsortedUngroupedTokenList(tokens, fiatBalance) } }, - block = { sortingOperations.getGroupedTokens(networks).bind() }, + block = { sortingOperations.getGroupedTokens().bind() }, ), ) @@ -159,8 +138,6 @@ internal class TokenListOperations( data class UnableToSortTokenList(val unsortedTokenList: TokenList.Ungrouped) : Error() - data class UnableToGroupTokenList(val ungroupedTokenList: TokenList.Ungrouped) : Error() - data class DataError(val cause: Throwable) : Error() internal companion object { @@ -169,7 +146,6 @@ internal class TokenListOperations( e: TokenListSortingOperations.Error, createUnsortedUngroupedTokenList: () -> TokenList.Ungrouped, ): Error = when (e) { - is TokenListSortingOperations.Error.EmptyNetworks, is TokenListSortingOperations.Error.EmptyTokens, is TokenListSortingOperations.Error.NetworkNotFound, -> UnableToSortTokenList( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt index 4e609a140f..281d35a8ba 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt @@ -1,10 +1,12 @@ package com.tangem.domain.tokens.operations -import arrow.core.* +import arrow.core.Either +import arrow.core.NonEmptyList import arrow.core.raise.Raise import arrow.core.raise.either import arrow.core.raise.ensure import arrow.core.raise.ensureNotNull +import arrow.core.toNonEmptyListOrNull import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList @@ -31,16 +33,13 @@ internal class TokenListSortingOperations( sortByBalance = sortByBalance, ) - fun getGroupedTokens(networks: Set): Either> = either { + fun getGroupedTokens(): Either> = either { ensure(currencies.isNotEmpty()) { Error.EmptyTokens } - val networksNes = ensureNotNull(networks.toNonEmptySetOrNull()) { - Error.EmptyNetworks - } if (sortByBalance) { - groupAndSortTokensByBalance(networksNes) + groupAndSortTokensByBalance() } else { - groupTokens(networksNes) + groupTokens() } } @@ -54,14 +53,10 @@ internal class TokenListSortingOperations( fun getSortType(): TokenList.SortType = if (sortByBalance) TokenList.SortType.BALANCE else TokenList.SortType.NONE - private fun Raise.groupTokens(networks: NonEmptySet): NonEmptyList { + private fun Raise.groupTokens(): NonEmptyList { val groupedTokens = currencies - .groupBy { it.currency.networkId } - .map { (networkId, tokens) -> - val network = ensureNotNull(networks.firstOrNull { it.id == networkId }) { - Error.NetworkNotFound(networkId) - } - + .groupBy { it.currency.network } + .map { (network, tokens) -> NetworkGroup( network = network, currencies = ensureNotNull(tokens.toNonEmptyListOrNull()) { Error.EmptyTokens }, @@ -72,8 +67,8 @@ internal class TokenListSortingOperations( return ensureNotNull(groupedTokens) { Error.EmptyTokens } } - private fun Raise.groupAndSortTokensByBalance(networks: NonEmptySet): NonEmptyList { - val groupsWithSortedTokens = groupTokens(networks) + private fun Raise.groupAndSortTokensByBalance(): NonEmptyList { + val groupsWithSortedTokens = groupTokens() .map { group -> val tokens = group.currencies as? NonEmptyList ?: error("Tokens can not be empty here") @@ -110,8 +105,6 @@ internal class TokenListSortingOperations( object EmptyTokens : Error() - object EmptyNetworks : Error() - data class NetworkNotFound(val networkId: Network.ID) : Error() } } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt index 8b9eebf8a8..bea0edb006 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt @@ -12,7 +12,6 @@ import com.tangem.domain.tokens.mock.MockTokens import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.models.CryptoCurrency -import com.tangem.domain.tokens.models.Network import com.tangem.domain.tokens.models.Quote import com.tangem.domain.tokens.repository.MockCurrenciesRepository import com.tangem.domain.tokens.repository.MockNetworksRepository @@ -109,23 +108,6 @@ internal class GetTokenListUseCaseTest { assertEquals(expectedResult, result) } - @Test - fun `when networks getting failed and list is groped then error should be received`() = runTest { - // Given - val expectedResult = TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left() - - val useCase = getUseCase( - networks = DataError.NetworkError.NoInternetConnection.left(), - isGrouped = flowOf(true.right()), - ) - - // When - val result = useCase(userWalletId).first() - - // Then - assertEquals(expectedResult, result) - } - @Test fun `when networks statuses getting failed then error should be received`() = runTest { // Given @@ -212,22 +194,6 @@ internal class GetTokenListUseCaseTest { assertEquals(expectedResult, result) } - @Test - fun `when list is grouped and networks getting failed then error should be received`() = runTest { - val expectedResult = TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left() - - val useCase = getUseCase( - networks = DataError.NetworkError.NoInternetConnection.left(), - isGrouped = flowOf(true.right()), - ) - - // When - val result = useCase(userWalletId).first() - - // Then - assertEquals(expectedResult, result) - } - @Test fun `when list is sorted and ungrouped then correct token list should be received`() = runTest { val expectedResult = listOf( @@ -285,27 +251,6 @@ internal class GetTokenListUseCaseTest { assertEquals(expectedResult, result) } - @Test - fun `when networks is empty and list is grouped then ungrouped list should be received`() = runTest { - val expectedResult = listOf( - TokenListError.UnableToSortTokenList(MockTokenLists.loadingUngroupedTokenList).left(), - TokenListError.UnableToSortTokenList(MockTokenLists.failedUngroupedTokenList).left(), - ) - - val useCase = getUseCase( - networks = emptySet().right(), - isGrouped = flowOf(true.right()), - ) - - // When - val result = useCase(userWalletId) - .take(count = 2) - .toList() - - // Then - assertEquals(expectedResult, result) - } - @Test fun `when tokens flow is empty then error should be received`() = runTest { val expectedResult = TokenListError.EmptyTokens.left() @@ -390,7 +335,6 @@ internal class GetTokenListUseCaseTest { private fun getUseCase( tokens: Flow>> = flowOf(MockTokens.tokens.right()), quotes: Flow>> = flowOf(MockQuotes.quotes.right()), - networks: Either> = MockNetworks.networks.right(), statuses: Flow>> = flowOf(MockNetworks.errorNetworksStatuses.right()), isGrouped: Flow> = flowOf(MockTokenLists.isGrouped.right()), isSortedByBalance: Flow> = flowOf(MockTokenLists.isSortedByBalance.right()), @@ -404,6 +348,6 @@ internal class GetTokenListUseCaseTest { isSortedByBalance = isSortedByBalance, ), quotesRepository = MockQuotesRepository(quotes), - networksRepository = MockNetworksRepository(networks, statuses), + networksRepository = MockNetworksRepository(MockNetworks.networks.right(), statuses), ) } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingTest.kt index 8e34224394..299b44a0d0 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingTest.kt @@ -1,17 +1,11 @@ package com.tangem.domain.tokens -import arrow.core.Either import arrow.core.left import arrow.core.right -import com.tangem.domain.core.error.DataError import com.tangem.domain.tokens.error.TokenListSortingError -import com.tangem.domain.tokens.mock.MockNetworks import com.tangem.domain.tokens.mock.MockTokenLists -import com.tangem.domain.tokens.models.Network -import com.tangem.domain.tokens.repository.MockNetworksRepository import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import junit.framework.TestCase.assertEquals -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Test @@ -143,38 +137,7 @@ internal class ToggleTokenListGroupingTest { assertEquals(expectedResult, result) } - @Test - fun `when list is ungrouped but networks is empty then error should be received`() = runTest { - // Given - val expectedResult = TokenListSortingError.UnableToSortTokenList.left() - - val useCase = getUseCase(networks = emptySet().right()) - - // When - val result = useCase(MockTokenLists.unsortedUngroupedTokenList) - - // Then - assertEquals(expectedResult, result) - } - - @Test - fun `when list is ungrouped but networks getting failed then error should be received`() = runTest { - // Given - val error = DataError.NetworkError.NoInternetConnection - val expectedResult = TokenListSortingError.DataError(error).left() - - val useCase = getUseCase(networks = error.left()) - - // When - val result = useCase(MockTokenLists.unsortedUngroupedTokenList) - - // Then - assertEquals(expectedResult, result) - } - - private fun getUseCase(networks: Either> = MockNetworks.networks.right()) = - ToggleTokenListGroupingUseCase( - networksRepository = MockNetworksRepository(networks, statuses = flowOf()), - dispatchers = TestingCoroutineDispatcherProvider(), - ) + private fun getUseCase() = ToggleTokenListGroupingUseCase( + dispatchers = TestingCoroutineDispatcherProvider(), + ) } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt index 4bf559f4c7..5823b20556 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt @@ -14,16 +14,22 @@ internal object MockNetworks { val network1 = Network( id = Network.ID("network1"), name = "Network One", + isTestnet = false, + standardType = Network.StandardType.ERC20, ) val network2 = Network( id = Network.ID("network2"), name = "Network Two", + isTestnet = false, + standardType = Network.StandardType.ERC20, ) val network3 = Network( id = Network.ID("network3"), name = "Network Three", + isTestnet = false, + standardType = Network.StandardType.ERC20, ) val networks = nonEmptySetOf(network1, network2, network3) diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt index ab3078de1d..d69fa589b0 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt @@ -10,21 +10,21 @@ internal object MockNetworksGroups { val networkGroup1 = NetworkGroup( network = MockNetworks.network1, currencies = MockTokensStates.failedTokenStates - .filter { it.currency.networkId == MockNetworks.network1.id } + .filter { it.currency.network.id == MockNetworks.network1.id } .toNonEmptyListOrNull()!!, ) val networkGroup2 = NetworkGroup( network = MockNetworks.network2, currencies = MockTokensStates.failedTokenStates - .filter { it.currency.networkId == MockNetworks.network2.id } + .filter { it.currency.network.id == MockNetworks.network2.id } .toNonEmptyListOrNull()!!, ) val networkGroup3 = NetworkGroup( network = MockNetworks.network3, currencies = MockTokensStates.failedTokenStates - .filter { it.currency.networkId == MockNetworks.network3.id } + .filter { it.currency.network.id == MockNetworks.network3.id } .toNonEmptyListOrNull()!!, ) @@ -33,7 +33,7 @@ internal object MockNetworksGroups { val loadedNetworksGroups = failedNetworksGroups.map { group -> group.copy( currencies = MockTokensStates.loadedTokensStates - .filter { it.currency.networkId == group.network.id } + .filter { it.currency.network.id == group.network.id } .toNonEmptyListOrNull()!!, ) } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt index 6e3749a46f..fda72a1b06 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt @@ -8,7 +8,7 @@ internal object MockTokens { val token1 get() = CryptoCurrency.Coin( id = ID(ID.Prefix.COIN_PREFIX, MockNetworks.network1.id, ID.Suffix.RawID("token1")), - networkId = MockNetworks.network1.id, + network = MockNetworks.network1, name = "Token 1", symbol = "T1", decimals = 8, @@ -18,7 +18,7 @@ internal object MockTokens { val token2 get() = CryptoCurrency.Token( id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network1.id, ID.Suffix.RawID("token2")), - networkId = MockNetworks.network1.id, + network = MockNetworks.network1, name = "Token 2", symbol = "T2", isCustom = false, @@ -26,13 +26,11 @@ internal object MockTokens { iconUrl = null, contractAddress = "address", derivationPath = null, - blockchainName = "Ethereum", - standardType = CryptoCurrency.StandardType.ERC20, ) val token3 get() = CryptoCurrency.Token( id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network1.id, ID.Suffix.RawID("token3")), - networkId = MockNetworks.network1.id, + network = MockNetworks.network1, name = "Token 3", symbol = "T3", isCustom = false, @@ -40,13 +38,11 @@ internal object MockTokens { iconUrl = null, contractAddress = "address", derivationPath = null, - blockchainName = "Ethereum", - standardType = CryptoCurrency.StandardType.ERC20, ) val token4 get() = CryptoCurrency.Coin( id = ID(ID.Prefix.COIN_PREFIX, MockNetworks.network2.id, ID.Suffix.RawID("token4")), - networkId = MockNetworks.network2.id, + network = MockNetworks.network2, name = "Token 4", symbol = "T4", decimals = 8, @@ -56,7 +52,7 @@ internal object MockTokens { val token5 get() = CryptoCurrency.Token( id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network2.id, ID.Suffix.RawID("token5")), - networkId = MockNetworks.network2.id, + network = MockNetworks.network2, name = "Token 5", symbol = "T5", isCustom = false, @@ -64,13 +60,11 @@ internal object MockTokens { iconUrl = null, contractAddress = "address", derivationPath = null, - blockchainName = "Ethereum", - standardType = CryptoCurrency.StandardType.ERC20, ) val token6 get() = CryptoCurrency.Token( id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network2.id, ID.Suffix.RawID("token6")), - networkId = MockNetworks.network2.id, + network = MockNetworks.network2, name = "Token 6", symbol = "T6", isCustom = false, @@ -78,13 +72,11 @@ internal object MockTokens { iconUrl = null, contractAddress = "address", derivationPath = null, - blockchainName = "Ethereum", - standardType = CryptoCurrency.StandardType.ERC20, ) val token7 get() = CryptoCurrency.Coin( id = ID(ID.Prefix.COIN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token7")), - networkId = MockNetworks.network3.id, + network = MockNetworks.network3, name = "Token 7", symbol = "T7", decimals = 8, @@ -94,7 +86,7 @@ internal object MockTokens { val token8 get() = CryptoCurrency.Token( id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token8")), - networkId = MockNetworks.network3.id, + network = MockNetworks.network3, name = "Token 8", symbol = "T8", isCustom = false, @@ -102,13 +94,11 @@ internal object MockTokens { iconUrl = null, contractAddress = "address", derivationPath = null, - blockchainName = "Ethereum", - standardType = CryptoCurrency.StandardType.ERC20, ) val token9 get() = CryptoCurrency.Token( id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token9")), - networkId = MockNetworks.network3.id, + network = MockNetworks.network3, name = "Token 9", symbol = "T9", isCustom = false, @@ -116,13 +106,11 @@ internal object MockTokens { iconUrl = null, contractAddress = "address", derivationPath = null, - blockchainName = "Ethereum", - standardType = CryptoCurrency.StandardType.ERC20, ) val token10 get() = CryptoCurrency.Token( id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token10")), - networkId = MockNetworks.network3.id, + network = MockNetworks.network3, name = "Token 10", symbol = "T10", isCustom = false, @@ -130,8 +118,6 @@ internal object MockTokens { iconUrl = null, contractAddress = "address", derivationPath = null, - blockchainName = "Ethereum", - standardType = CryptoCurrency.StandardType.ERC20, ) val tokens = listOf(token1, token2, token3, token4, token5, token6, token7, token8, token9, token10) diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index ee6b0a6a1e..acd225aa87 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -72,7 +72,7 @@ internal object MockTokensStates { val loadedTokensStates = failedTokenStates.map { status -> val networkStatus = MockNetworks.verifiedNetworksStatuses - .first { it.networkId == status.currency.networkId } + .first { it.networkId == status.currency.network.id } val amount = (networkStatus.value as NetworkStatus.Verified).amounts[status.currency.id]!! val quote = MockQuotes.quotes.first { it.rawCurrencyId == status.currency.id.rawCurrencyId } val fiatAmount = amount * quote.fiatRate diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 3c685d3e9e..5a829656e3 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -28,8 +28,8 @@ internal class TokenDetailsSkeletonStateConverter( 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, + networkName = value.cryptoCurrency.network.standardType.name, + blockchainName = value.cryptoCurrency.network.name, // TODO: [REDACTED_JIRA] networkIcon = R.drawable.img_eth_22, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt index 5284855142..2b1cf0d0bd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -45,7 +45,7 @@ internal class CryptoCurrencyToDraggableItemConverter( ): DraggableItem.Token { return DraggableItem.Token( tokenItemState = createTokenItemState(currencyStatus, appCurrency), - groupId = getGroupHeaderId(currencyStatus.currency.networkId), + groupId = getGroupHeaderId(currencyStatus.currency.network.id), ) } From afb3ce17940b8b6872d09740795d154628d3095f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 21 Aug 2023 16:34:49 +0100 Subject: [PATCH 10/44] Updated on 2026-08-14 --- gradle/dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index e2d2bf63ad..5b188fd26c 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -80,7 +80,7 @@ okHttp-prettyLogging = "3.1.0" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-325" +tangemBlockchainSdk = "develop-327" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-289" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds From f120b184ac54d47924657a66f157c0a36306a1f4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 22 Aug 2023 12:48:12 +0700 Subject: [PATCH 11/44] Updated on 2026-08-14 --- .../impl/presentation/ui/TokensListScreen.kt | 18 ++--- .../transactions/TransactionList.kt | 20 +++-- .../transactions/state/TxHistoryState.kt | 74 +++---------------- .../presentation/common/WalletPreviewData.kt | 5 +- .../wallet/state/WalletSingleCurrencyState.kt | 6 +- .../factory/WalletRefreshStateConverter.kt | 31 ++------ .../factory/WalletSkeletonStateConverter.kt | 7 +- .../WalletLoadedTxHistoryConverter.kt | 1 + .../WalletLoadingTxHistoryConverter.kt | 41 +++++----- .../WalletTxHistoryItemFlowConverter.kt | 45 +++++++---- .../presentation/wallet/ui/WalletScreen.kt | 4 +- .../wallet/viewmodels/WalletViewModel.kt | 4 +- gradle/dependencies.toml | 2 +- 13 files changed, 111 insertions(+), 147 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt index 174563c6be..ea6ab16083 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt @@ -7,17 +7,14 @@ import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.FabPosition import androidx.compose.material.Scaffold import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -32,9 +29,7 @@ import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnitType import androidx.compose.ui.unit.dp import androidx.paging.PagingData -import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.collectAsLazyPagingItems -import androidx.paging.compose.items +import androidx.paging.compose.* import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme @@ -134,8 +129,11 @@ private fun TokensListContent( item { DifferentAddressesWarning() } } - items(items = tokens, key = TokenItemState::composedId) { - it?.let { TokenItem(model = it) } + tokens.itemKey(TokenItemState::composedId) + tokens.itemContentType(TokenItemState::composedId) + + items(items = tokens.itemSnapshotList.items, key = TokenItemState::composedId) { + TokenItem(model = it) } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt index 6be91c66ae..93a6d16058 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt @@ -4,9 +4,11 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.ui.Modifier import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.itemsIndexed +import androidx.paging.compose.itemContentType +import androidx.paging.compose.itemKey import com.tangem.core.ui.components.transactions.empty.EmptyTransactionBlock import com.tangem.core.ui.components.transactions.empty.EmptyTransactionsBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState @@ -26,7 +28,7 @@ fun LazyListScope.txHistoryItems( modifier: Modifier = Modifier, ) { when (state) { - is TxHistoryState.ContentState -> { + is TxHistoryState.Content -> { contentItems( txHistoryItems = requireNotNull(txHistoryItems), modifier = modifier, @@ -58,8 +60,18 @@ private fun LazyListScope.contentItems( txHistoryItems: LazyPagingItems, modifier: Modifier = Modifier, ) { + txHistoryItems.itemKey { item -> + when (item) { + is TxHistoryState.TxHistoryItemState.GroupTitle -> item.title + is TxHistoryState.TxHistoryItemState.Title -> item.onExploreClick.hashCode() + is TxHistoryState.TxHistoryItemState.Transaction -> item.state.txHash + } + } + + txHistoryItems.itemContentType { it::class.java } + itemsIndexed( - items = txHistoryItems, + items = txHistoryItems.itemSnapshotList.items, key = { _, item -> when (item) { is TxHistoryState.TxHistoryItemState.GroupTitle -> item.title @@ -68,8 +80,6 @@ private fun LazyListScope.contentItems( } }, ) { index, item -> - if (item == null) return@itemsIndexed - TxHistoryListItem( state = item, modifier = modifier 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 a35545faea..a6035f48dc 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,68 +1,17 @@ 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 +import kotlinx.coroutines.flow.MutableStateFlow /** Wallet transaction history state */ sealed interface TxHistoryState { - /** - * Wallet transaction history state with content. Items contains a required [TxHistoryItemState.Title]. - * - * @property contentItems content items - */ - 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( - override val onExploreClick: () -> Unit, - val transactions: Flow> = getDefaultLoadingTransactions(), - ) : ContentState(transactions) - /** * Wallet transaction history state with content * - * @property onExploreClick lambda be invoke when explore button was clicked - * @property contentItems content items + * @property contentItems content 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(override val onExploreClick: () -> Unit) : - ContentState(contentItems = getDefaultLoadingTransactions()), - WalletLockedContentState + data class Content(val contentItems: MutableStateFlow>) : TxHistoryState /** * Empty state @@ -110,16 +59,15 @@ sealed interface TxHistoryState { data class Transaction(val state: TransactionState) : TxHistoryItemState } - private companion object { - const val LOADING_TX_HASH = "LOADING_TX_HASH" + companion object { + private 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), - ), + fun getDefaultLoadingTransactions(onExploreClick: () -> Unit): PagingData { + return PagingData.from( + data = listOf( + TxHistoryItemState.Title(onExploreClick = onExploreClick), + TxHistoryItemState.Transaction( + state = TransactionState.Loading(txHash = LOADING_TX_HASH), ), ), ) 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 68df79d297..504297fbf1 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 @@ -19,7 +19,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.components.* import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.TokensListItemState import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.MutableStateFlow import java.util.UUID @Suppress("LargeClass") @@ -350,8 +350,7 @@ internal object WalletPreviewData { ), ), txHistoryState = TxHistoryState.Content( - onExploreClick = {}, - contentItems = flowOf( + contentItems = MutableStateFlow( PagingData.from( listOf( TxHistoryState.TxHistoryItemState.GroupTitle("Today"), 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 ab23400cca..2d4bc11664 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,11 +1,13 @@ package com.tangem.feature.wallet.presentation.wallet.state import androidx.compose.runtime.Immutable +import androidx.paging.PagingData 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.* import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow /** * Single currency wallet content state @@ -60,6 +62,8 @@ internal sealed class WalletSingleCurrencyState : WalletState.ContentState() { ), ) - override val txHistoryState: TxHistoryState = TxHistoryState.Locked(onExploreClick) + override val txHistoryState: TxHistoryState = TxHistoryState.Content( + contentItems = MutableStateFlow(PagingData.empty()), + ) } } \ No newline at end of file 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 index f442b7edd2..715a51e7d4 100644 --- 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 @@ -1,12 +1,8 @@ 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 @@ -20,9 +16,7 @@ import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickInten 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 +import kotlinx.coroutines.flow.update internal class WalletRefreshStateConverter( private val currentStateProvider: Provider, @@ -98,30 +92,15 @@ internal class WalletRefreshStateConverter( 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), - ) - }, - ) + txHistoryState.contentItems.update { + TxHistoryState.getDefaultLoadingTransactions(onExploreClick = clickIntents::onExploreClick) + } + txHistoryState } 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/WalletSkeletonStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt index db8c705554..597647666c 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 @@ -16,6 +16,7 @@ import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.MutableStateFlow /** * Converter from loaded list of [UserWallet] to skeleton state of screen [WalletState.ContentState] @@ -62,7 +63,11 @@ internal class WalletSkeletonStateConverter( bottomSheetConfig = null, buttons = getButtons(), marketPriceBlockState = MarketPriceBlockState.Loading(currencyName = currencyName), - txHistoryState = TxHistoryState.Loading(onExploreClick = clickIntents::onExploreClick), + txHistoryState = TxHistoryState.Content( + contentItems = MutableStateFlow( + value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), + ), + ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt index d5a8f7cdbd..e585ac71f5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt @@ -30,6 +30,7 @@ internal class WalletLoadedTxHistoryConverter( private val walletTxHistoryItemFlowConverter by lazy { WalletTxHistoryItemFlowConverter( + currentStateProvider = currentStateProvider, blockchain = currentCardTypeResolverProvider().getBlockchain(), clickIntents = clickIntents, ) 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 b9600ca87e..ddb1a4ab1c 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 @@ -4,13 +4,13 @@ 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.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 +import kotlinx.coroutines.flow.update /** * Converter from loading tx history state to [WalletSingleCurrencyState.Content] @@ -33,31 +33,36 @@ internal class WalletLoadingTxHistoryConverter( return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content).copy( txHistoryState = when (error) { is TxHistoryStateError.EmptyTxHistories -> { - TxHistoryState.Empty(onBuyClick = clickIntents::onBuyClick) + Empty(onBuyClick = clickIntents::onBuyClick) } is TxHistoryStateError.DataError -> { - TxHistoryState.Error(onReloadClick = clickIntents::onReloadClick) + Error(onReloadClick = clickIntents::onReloadClick) } is TxHistoryStateError.TxHistoryNotImplemented -> { - TxHistoryState.NotSupported(onExploreClick = clickIntents::onExploreClick) + NotSupported(onExploreClick = clickIntents::onExploreClick) } }, ) } private fun convert(value: Int): WalletSingleCurrencyState.Content { - return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content).copy( - txHistoryState = TxHistoryState.Loading( - onExploreClick = clickIntents::onExploreClick, - transactions = flow { - PagingData.from( - data = MutableList( - size = value, - init = { TransactionState.Loading(it.toString()) }, - ), - ) - }, - ), - ) + val state = requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content) + val txHistoryContent = requireNotNull(state.txHistoryState as? Content) + + txHistoryContent.contentItems.update { + PagingData.from( + data = listOf(TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick)) + + MutableList( + size = value, + init = { + TxHistoryItemState.Transaction( + state = TransactionState.Loading(it.toString()), + ) + }, + ), + ) + } + + return state } } \ 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 8feafcee7c..fae55f6b5b 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,23 +1,27 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory import android.text.format.DateUtils -import androidx.paging.PagingData -import androidx.paging.TerminalSeparatorType -import androidx.paging.insertSeparators -import androidx.paging.map +import androidx.paging.* import com.tangem.blockchain.common.Blockchain +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.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.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 com.tangem.utils.extensions.isToday import com.tangem.utils.extensions.isYesterday import com.tangem.utils.toBriefAddressFormat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update import org.joda.time.DateTime import org.joda.time.DateTimeZone import org.joda.time.format.DateTimeFormatterBuilder @@ -27,12 +31,14 @@ import java.util.Locale /** * Convert from [Flow] of [TxHistoryItem] to [TxHistoryState] * - * @property blockchain blockchain of transactions history - * @property clickIntents screen click intents + * @property currentStateProvider current state provider + * @property blockchain blockchain of transactions history + * @property clickIntents screen click intents * [REDACTED_AUTHOR] */ internal class WalletTxHistoryItemFlowConverter( + private val currentStateProvider: Provider, private val blockchain: Blockchain, private val clickIntents: WalletClickIntents, ) : Converter>, TxHistoryState> { @@ -60,19 +66,30 @@ internal class WalletTxHistoryItemFlowConverter( } override fun convert(value: Flow>): TxHistoryState { - return TxHistoryState.Content( - onExploreClick = clickIntents::onExploreClick, - contentItems = value - .map { pagingData -> - pagingData + val state = currentStateProvider() as WalletSingleCurrencyState + val txHistoryContent = state.txHistoryState as TxHistoryState.Content + + // FIXME: TxHistoryRepository should send loading transactions + // [REDACTED_JIRA] + value + .onEach { txHistoryStatePagingData -> + txHistoryContent.contentItems.update { + txHistoryStatePagingData .map { item -> // [createTransactionState] returns timestamp without formatting TxHistoryItemState.Transaction(state = createTransactionState(item)) } + .insertHeaderItem( + terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE, + item = TxHistoryItemState.Title(clickIntents::onExploreClick), + ) .insertGroupTitle() // method uses the raw timestamp .formatTransactionsTimestamp() // method formats the timestamp - }, - ) + } + } + .launchIn(CoroutineScope(Dispatchers.IO)) + + return txHistoryContent } private fun createTransactionState(item: TxHistoryItem): TransactionState { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 4bc618e81d..8c94b30d15 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -69,9 +69,9 @@ private fun WalletContent(state: WalletState.ContentState) { .pullRefresh(pullRefreshState), ) { val txHistoryItems = if (state is WalletSingleCurrencyState && - state.txHistoryState is TxHistoryState.ContentState + state.txHistoryState is TxHistoryState.Content ) { - (state.txHistoryState as? TxHistoryState.ContentState)?.items?.collectAsLazyPagingItems() + (state.txHistoryState as? TxHistoryState.Content)?.contentItems?.collectAsLazyPagingItems() } else { null } 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 b6bca4b04a..5deb677137 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 @@ -9,7 +9,6 @@ import com.tangem.common.Provider import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess import com.tangem.core.ui.components.marketprice.MarketPriceBlockState -import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.card.* @@ -375,8 +374,7 @@ internal class WalletViewModel @Inject constructor( tokensListState is WalletTokensListState.Loading || hasLoadingTokens } is WalletSingleCurrencyState -> { - this is WalletSingleCurrencyState.Content && marketPriceBlockState is MarketPriceBlockState.Loading || - txHistoryState is TxHistoryState.Loading + this is WalletSingleCurrencyState.Content && marketPriceBlockState is MarketPriceBlockState.Loading } is WalletState.Initial -> false } diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 5b188fd26c..04b0bc5086 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -30,7 +30,7 @@ compose-material3 = "1.1.0" compose-constraint = "1.0.1" compose-navigation = "2.5.3" compose-accompanist = "0.30.1" -compose-paging = "1.0.0-alpha18" +compose-paging = "3.2.0" compose-reorderable = "0.9.6" # endregion Compose From 55641cab14ce05ca264c3364db33c24a4cd50acf Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 22 Aug 2023 12:41:19 +0100 Subject: [PATCH 12/44] Updated on 2026-08-14 --- .../tokens/GetCryptoCurrencyActionsUseCase.kt | 32 +++++++++++++++++++ .../domain/tokens/model/TokenActionsState.kt | 25 +++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt new file mode 100644 index 0000000000..ec8f8f39f8 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -0,0 +1,32 @@ +package com.tangem.domain.tokens + +import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.* + +class GetCryptoCurrencyActionsUseCase( + private val dispatchers: CoroutineDispatcherProvider, +) { + + operator fun invoke(userWalletId: UserWalletId, tokenId: String): Flow { + return flow { + emit(getMockState(userWalletId, tokenId)) + }.flowOn(dispatchers.io) + } + + // TODO replace by real data + private fun getMockState(userWalletId: UserWalletId, tokenId: String): TokenActionsState { + return TokenActionsState( + walletId = userWalletId, + tokenId = tokenId, + states = listOf( + TokenActionsState.ActionState.Buy(true), + TokenActionsState.ActionState.Sell(true), + TokenActionsState.ActionState.Receive(true), + TokenActionsState.ActionState.Swap(true), + TokenActionsState.ActionState.Sell(true), + ), + ) + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt new file mode 100644 index 0000000000..b2d6e6e71b --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt @@ -0,0 +1,25 @@ +package com.tangem.domain.tokens.model + +import com.tangem.domain.wallets.models.UserWalletId + +data class TokenActionsState( + val walletId: UserWalletId, + val tokenId: String, + val states: List, +) { + + sealed class ActionState { + + abstract val enabled: Boolean + + data class Buy(override val enabled: Boolean) : ActionState() + + data class Sell(override val enabled: Boolean) : ActionState() + + data class Receive(override val enabled: Boolean) : ActionState() + + data class Swap(override val enabled: Boolean) : ActionState() + + data class Send(override val enabled: Boolean) : ActionState() + } +} \ No newline at end of file From cd34a586bbfa6249042c1076fdd6013c43f85574 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 22 Aug 2023 18:49:27 +0500 Subject: [PATCH 13/44] Updated on 2026-08-14 --- .../intents/TxHistoryClickIntents.kt | 10 + .../tangem/domain/tokens/models/Network.kt | 6 +- features/tokendetails/impl/build.gradle.kts | 9 +- .../router/DefaultTokenDetailsRouter.kt | 4 + .../router/InnerTokenDetailsRouter.kt | 4 + .../tokendetails/TokenDetailsPreviewData.kt | 7 + .../tokendetails/state/TokenDetailsState.kt | 2 + .../TokenDetailsSkeletonStateConverter.kt | 7 + .../state/factory/TokenDetailsStateFactory.kt | 32 +++ .../TokenDetailsLoadedTxHistoryConverter.kt | 49 ++++ .../TokenDetailsLoadingTxHistoryConverter.kt | 59 +++++ .../TokenDetailsTxHistoryItemFlowConverter.kt | 223 ++++++++++++++++++ .../tokendetails/ui/TokenDetailsScreen.kt | 39 ++- .../viewmodels/TokenDetailsClickIntents.kt | 4 +- .../viewmodels/TokenDetailsViewModel.kt | 54 +++++ .../wallet/viewmodels/WalletClickIntents.kt | 9 +- 16 files changed, 496 insertions(+), 22 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/transactions/intents/TxHistoryClickIntents.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadedTxHistoryConverter.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/intents/TxHistoryClickIntents.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/intents/TxHistoryClickIntents.kt new file mode 100644 index 0000000000..4abce062be --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/intents/TxHistoryClickIntents.kt @@ -0,0 +1,10 @@ +package com.tangem.core.ui.components.transactions.intents + +interface TxHistoryClickIntents { + + fun onBuyClick() + + fun onReloadClick() + + fun onExploreClick() +} \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt index 05c043d7f1..4c60938e9c 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt @@ -31,9 +31,8 @@ data class Network( * * @property value The string representation of the network ID. */ - // FIXME: Remove serialization [REDACTED_JIRA] @JvmInline - value class ID(val value: String) : Serializable { + value class ID(val value: String) { init { require(value.isNotBlank()) { "Network ID must not be blank" } @@ -49,7 +48,8 @@ data class Network( * * @property name The human-readable name of the standard type. */ - sealed class StandardType { + // FIXME: Remove serialization [REDACTED_JIRA] + sealed class StandardType : Serializable { abstract val name: String /** Represents the ERC20 token standard, common on the Ethereum network. */ diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 6ce4bed811..55d58498b5 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -14,19 +14,22 @@ android { dependencies { /** AndroidX */ implementation(deps.androidx.activity.compose) + implementation(deps.androidx.paging.runtime) /** Compose */ - implementation(deps.compose.material) + implementation(deps.compose.accompanist.systemUiController) + implementation(deps.compose.coil) implementation(deps.compose.foundation) + implementation(deps.compose.material) implementation(deps.compose.material3) implementation(deps.compose.navigation) implementation(deps.compose.navigation.hilt) + implementation(deps.compose.paging) implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) - implementation(deps.compose.accompanist.systemUiController) - implementation(deps.compose.coil) implementation(deps.arrow.core) + implementation(deps.jodatime) implementation(deps.kotlin.immutable.collections) implementation(deps.tangem.blockchain) implementation(deps.tangem.card.core) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt index 73819028da..797a2d9194 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt @@ -14,4 +14,8 @@ internal class DefaultTokenDetailsRouter( override fun popBackStack() { navigationStateHolder.navigate(NavigationAction.PopBackTo()) } + + override fun openUrl(url: String) { + navigationStateHolder.navigate(NavigationAction.OpenUrl(url = url)) + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt index 89448deb0d..718ceb5821 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt @@ -4,5 +4,9 @@ import com.tangem.features.tokendetails.navigation.TokenDetailsRouter internal interface InnerTokenDetailsRouter : TokenDetailsRouter { + /** Pop back stack */ fun popBackStack() + + /** Open website by [url] */ + fun openUrl(url: String) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index f5e6b62437..9122d3dfbd 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -2,6 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.extensions.TextReference import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState @@ -10,6 +11,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfo import com.tangem.features.tokendetails.impl.R import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.MutableStateFlow internal object TokenDetailsPreviewData { @@ -82,5 +84,10 @@ internal object TokenDetailsPreviewData { tokenInfoBlockState = tokenInfoBlockState, tokenBalanceBlockState = balanceLoading, marketPriceBlockState = marketPriceLoading, + txHistoryState = TxHistoryState.Content( + contentItems = MutableStateFlow( + value = TxHistoryState.getDefaultLoadingTransactions {}, + ), + ), ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index 8c7ba02c14..c3902dc328 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -1,10 +1,12 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.state.TxHistoryState data class TokenDetailsState( val topAppBarConfig: TokenDetailsTopAppBarConfig, val tokenInfoBlockState: TokenInfoBlockState, val tokenBalanceBlockState: TokenDetailsBalanceBlockState, val marketPriceBlockState: MarketPriceBlockState, + val txHistoryState: TxHistoryState, ) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 5a829656e3..880da1991e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -1,6 +1,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState @@ -11,6 +12,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.T import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.features.tokendetails.impl.R import com.tangem.utils.converter.Converter +import kotlinx.coroutines.flow.MutableStateFlow internal class TokenDetailsSkeletonStateConverter( private val clickIntents: TokenDetailsClickIntents, @@ -39,6 +41,11 @@ internal class TokenDetailsSkeletonStateConverter( TokenDetailsPreviewData.disabledActionButtons, ), marketPriceBlockState = MarketPriceBlockState.Loading(value.cryptoCurrency.name), + txHistoryState = TxHistoryState.Content( + contentItems = MutableStateFlow( + value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), + ), + ), ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index b43c200507..09c5094a0e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -1,18 +1,27 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory +import androidx.paging.PagingData import arrow.core.Either import com.tangem.common.Provider import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.error.CurrencyError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.txhistory.models.TxHistoryListError +import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadedTxHistoryConverter +import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents +import kotlinx.coroutines.flow.Flow internal class TokenDetailsStateFactory( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, private val clickIntents: TokenDetailsClickIntents, + symbol: String, + decimals: Int, ) { private val skeletonStateConverter by lazy { @@ -26,6 +35,19 @@ internal class TokenDetailsStateFactory( ) } + private val loadingTransactionsStateConverter by lazy { + TokenDetailsLoadingTxHistoryConverter(currentStateProvider = currentStateProvider, clickIntents = clickIntents) + } + + private val loadedTxHistoryConverter by lazy { + TokenDetailsLoadedTxHistoryConverter( + currentStateProvider = currentStateProvider, + clickIntents = clickIntents, + symbol = symbol, + decimals = decimals, + ) + } + fun getInitialState(cryptoCurrency: CryptoCurrency): TokenDetailsState { return skeletonStateConverter.convert( TokenDetailsSkeletonStateConverter.SkeletonModel(cryptoCurrency = cryptoCurrency), @@ -37,4 +59,14 @@ internal class TokenDetailsStateFactory( ): TokenDetailsState { return tokenDetailsLoadedBalanceConverter.convert(cryptoCurrencyEither) } + + fun getLoadingTxHistoryState(itemsCountEither: Either): TokenDetailsState { + return loadingTransactionsStateConverter.convert(value = itemsCountEither) + } + + fun getLoadedTxHistoryState( + txHistoryEither: Either>>, + ): TokenDetailsState { + return loadedTxHistoryConverter.convert(txHistoryEither) + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadedTxHistoryConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadedTxHistoryConverter.kt new file mode 100644 index 0000000000..f9146ee1d3 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadedTxHistoryConverter.kt @@ -0,0 +1,49 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory + +import androidx.paging.PagingData +import arrow.core.Either +import com.tangem.common.Provider +import com.tangem.core.ui.components.transactions.intents.TxHistoryClickIntents +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.txhistory.models.TxHistoryListError +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.utils.converter.Converter +import kotlinx.coroutines.flow.Flow + +internal class TokenDetailsLoadedTxHistoryConverter( + private val currentStateProvider: Provider, + private val clickIntents: TxHistoryClickIntents, + symbol: String, + decimals: Int, +) : Converter>>, TokenDetailsState> { + + private val txHistoryItemFlowConverter by lazy { + TokenDetailsTxHistoryItemFlowConverter( + currentStateProvider = currentStateProvider, + symbol = symbol, + decimals = decimals, + clickIntents = clickIntents, + ) + } + + override fun convert(value: Either>>): TokenDetailsState { + return value.fold(ifLeft = ::convertError, ifRight = ::convert) + } + + private fun convertError(error: TxHistoryListError): TokenDetailsState { + return currentStateProvider().copy( + txHistoryState = when (error) { + is TxHistoryListError.DataError -> { + TxHistoryState.Error(onReloadClick = clickIntents::onReloadClick) + } + }, + ) + } + + private fun convert(items: Flow>): TokenDetailsState { + return currentStateProvider().copy( + txHistoryState = txHistoryItemFlowConverter.convert(value = items), + ) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt new file mode 100644 index 0000000000..9f98c1ccf3 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt @@ -0,0 +1,59 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory + +import androidx.paging.PagingData +import arrow.core.Either +import com.tangem.common.Provider +import com.tangem.core.ui.components.transactions.intents.TxHistoryClickIntents +import com.tangem.core.ui.components.transactions.state.TransactionState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.domain.txhistory.models.TxHistoryStateError +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.utils.converter.Converter +import kotlinx.coroutines.flow.update + +internal class TokenDetailsLoadingTxHistoryConverter( + private val currentStateProvider: Provider, + private val clickIntents: TxHistoryClickIntents, +) : Converter, TokenDetailsState> { + + override fun convert(value: Either): TokenDetailsState { + return value.fold(ifLeft = ::convertError, ifRight = ::convert) + } + + private fun convertError(error: TxHistoryStateError): TokenDetailsState { + return currentStateProvider().copy( + txHistoryState = when (error) { + is TxHistoryStateError.EmptyTxHistories -> { + TxHistoryState.Empty(onBuyClick = clickIntents::onBuyClick) + } + is TxHistoryStateError.DataError -> { + TxHistoryState.Error(onReloadClick = clickIntents::onReloadClick) + } + is TxHistoryStateError.TxHistoryNotImplemented -> { + TxHistoryState.NotSupported(onExploreClick = clickIntents::onExploreClick) + } + }, + ) + } + + private fun convert(value: Int): TokenDetailsState { + val state = currentStateProvider() + val txHistoryContent = state.txHistoryState as TxHistoryState.Content + + txHistoryContent.contentItems.update { + PagingData.from( + data = listOf(TxHistoryState.TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick)) + + MutableList( + size = value, + init = { + TxHistoryState.TxHistoryItemState.Transaction( + state = TransactionState.Loading(it.toString()), + ) + }, + ), + ) + } + + return state + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt new file mode 100644 index 0000000000..eafd6c9a31 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt @@ -0,0 +1,223 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory + +import android.text.format.DateUtils +import androidx.paging.* +import com.tangem.common.Provider +import com.tangem.core.ui.components.transactions.intents.TxHistoryClickIntents +import com.tangem.core.ui.components.transactions.state.TransactionState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.isToday +import com.tangem.utils.extensions.isYesterday +import com.tangem.utils.toBriefAddressFormat +import com.tangem.utils.toFormattedCurrencyString +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.* +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import org.joda.time.format.DateTimeFormatterBuilder +import java.math.BigDecimal +import java.util.Locale + +internal class TokenDetailsTxHistoryItemFlowConverter( + private val currentStateProvider: Provider, + private val symbol: String, + private val decimals: Int, + private val clickIntents: TxHistoryClickIntents, +) : Converter>, TxHistoryState> { + + /** Example, 2 Aug, 2023 */ + private val dateFormatter by lazy { + DateTimeFormatterBuilder() + .appendDayOfMonth(1) + .appendLiteral(' ') + .appendMonthOfYearShortText() + .appendLiteral(", ") + .appendYear(4, 4) + .toFormatter() + .withLocale(Locale.getDefault()) + } + + /** Example, 13:35 */ + private val timeFormatter by lazy { + DateTimeFormatterBuilder() + .appendHourOfDay(1) + .appendLiteral(':') + .appendMinuteOfHour(2) + .toFormatter() + .withLocale(Locale.getDefault()) + } + + override fun convert(value: Flow>): TxHistoryState { + val txHistoryContent = currentStateProvider().txHistoryState as TxHistoryState.Content + + // FIXME: TxHistoryRepository should send loading transactions + // [REDACTED_JIRA] + value + .onEach { txHistoryStatePagingData -> + txHistoryContent.contentItems.update { + txHistoryStatePagingData + .map { item -> + // [createTransactionState] returns timestamp without formatting + TxHistoryItemState.Transaction(state = createTransactionState(item)) + } + .insertHeaderItem( + terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE, + item = TxHistoryItemState.Title(clickIntents::onExploreClick), + ) + .insertGroupTitle() // method uses the raw timestamp + .formatTransactionsTimestamp() // method formats the timestamp + } + } + .launchIn(CoroutineScope(Dispatchers.IO)) + + return txHistoryContent + } + + private fun createTransactionState(item: TxHistoryItem): TransactionState { + return when (item.type) { + TxHistoryItem.TransactionType.Transfer -> { + when (val direction = item.direction) { + is TxHistoryItem.TransactionDirection.Incoming -> { + createIncomingTransferTransaction(item, direction) + } + is TxHistoryItem.TransactionDirection.Outgoing -> { + createOutgoingTransferTransaction(item, direction) + } + } + } + } + } + + private fun createIncomingTransferTransaction( + item: TxHistoryItem, + direction: TxHistoryItem.TransactionDirection.Incoming, + ): TransactionState { + return when (item.status) { + TxHistoryItem.TxStatus.Confirmed -> TransactionState.Receive( + txHash = item.txHash, + address = direction.from.toBriefAddressFormat(), + amount = item.amount.toCryptoCurrencyFormat(), + timestamp = item.getRawTimestamp(), + ) + TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Receiving( + txHash = item.txHash, + address = direction.from.toBriefAddressFormat(), + amount = item.amount.toCryptoCurrencyFormat(), + timestamp = item.getRawTimestamp(), + ) + } + } + + private fun createOutgoingTransferTransaction( + item: TxHistoryItem, + direction: TxHistoryItem.TransactionDirection.Outgoing, + ): TransactionState { + return when (item.status) { + TxHistoryItem.TxStatus.Confirmed -> TransactionState.Send( + txHash = item.txHash, + address = direction.to.toBriefAddressFormat(), + amount = item.amount.toCryptoCurrencyFormat(), + timestamp = item.getRawTimestamp(), + ) + TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Sending( + txHash = item.txHash, + address = direction.to.toBriefAddressFormat(), + amount = item.amount.toCryptoCurrencyFormat(), + timestamp = item.getRawTimestamp(), + ) + } + } + + private fun BigDecimal.toCryptoCurrencyFormat(): String { + return toFormattedCurrencyString(currency = symbol, decimals = decimals) + } + + private fun PagingData.insertGroupTitle(): PagingData { + return insertSeparators(terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE) { before, after -> + // Use raw timestamp to get date + + // If [afterDate] is the first transaction in the flow, add the group title + val afterDate = after.getTimestamp()?.toDateFormat() ?: return@insertSeparators null + if (before is TxHistoryItemState.Title) { + return@insertSeparators TxHistoryItemState.GroupTitle(afterDate) + } + + /* + * If [beforeDate] is not equals to [afterDate], then [afterDate] is first transaction in + * the new group + */ + val beforeDate = before.getTimestamp()?.toDateFormat() ?: return@insertSeparators null + return@insertSeparators if (beforeDate != afterDate) { + TxHistoryItemState.GroupTitle(afterDate) + } else { + null + } + } + } + + /** + * Map the [PagingData] to format the [TxHistoryItemState] timestamp + */ + private fun PagingData.formatTransactionsTimestamp(): PagingData { + return map { txHistoryItemState -> + if (txHistoryItemState is TxHistoryItemState.Transaction && + txHistoryItemState.state is TransactionState.Content + ) { + val txContent = txHistoryItemState.state as TransactionState.Content + txHistoryItemState.copy( + state = txContent.copySealed( + timestamp = txContent.timestamp.toTimeFormat(), + ), + ) + } else { + txHistoryItemState + } + } + } + + /** + * Get timestamp without formatting. + * It's life hack that help us to add transaction's group title to flow. + * + * @see [convert] + */ + private fun TxHistoryItem.getRawTimestamp() = this.timestampInMillis.toString() + + private fun TxHistoryItemState?.getTimestamp(): Long? { + return if (this is TxHistoryItemState.Transaction && this.state is TransactionState.Content) { + val txContent = this.state as TransactionState.Content + requireNotNull(txContent.timestamp.toLongOrNull()) { "Timestamp must be Long type" } + } else { + null + } + } + + /** + * If [this] timestamp is today or yesterday, returns relative date, + * otherwise returns formatting date by [dateFormatter] + */ + private fun Long.toDateFormat(): String { + val localDate = DateTime(this, DateTimeZone.getDefault()) + return if (localDate.isToday() || localDate.isYesterday()) { + DateUtils.getRelativeTimeSpanString( + this, + DateTime.now().millis, + DateUtils.DAY_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE, + ).toString() + } else { + dateFormatter.print(localDate) + } + } + + private fun String.toTimeFormat(): String { + return timeFormatter.print( + DateTime(this.toLong(), DateTimeZone.getDefault()), + ) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index a8a20dfa34..a54eef8db7 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -1,14 +1,17 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import androidx.paging.compose.collectAsLazyPagingItems import com.tangem.core.ui.components.marketprice.MarketPriceBlock +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.components.transactions.txHistoryItems import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState @@ -22,16 +25,36 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { topBar = { TokenDetailsTopAppBar(config = state.topAppBarConfig) }, containerColor = TangemTheme.colors.background.secondary, ) { scaffoldPaddings -> - Column( + val txHistoryItems = if (state.txHistoryState is TxHistoryState.Content) { + state.txHistoryState.contentItems.collectAsLazyPagingItems() + } else { + null + } + val betweenItemsPadding = TangemTheme.dimens.spacing12 + val horizontalPadding = TangemTheme.dimens.spacing16 + val itemModifier = Modifier + .padding(top = betweenItemsPadding) + .padding(horizontal = horizontalPadding) + LazyColumn( modifier = Modifier .padding(paddingValues = scaffoldPaddings) - .padding(horizontal = TangemTheme.dimens.spacing16) .fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { - TokenInfoBlock(state = state.tokenInfoBlockState) - TokenDetailsBalanceBlock(state = state.tokenBalanceBlockState) - MarketPriceBlock(state = state.marketPriceBlockState) + item { + TokenInfoBlock( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing4) + .padding(horizontal = horizontalPadding), + state = state.tokenInfoBlockState, + ) + } + item { TokenDetailsBalanceBlock(modifier = itemModifier, state = state.tokenBalanceBlockState) } + item( + key = MarketPriceBlockState::class.java, + contentType = MarketPriceBlockState::class.java, + content = { MarketPriceBlock(modifier = itemModifier, state = state.marketPriceBlockState) }, + ) + txHistoryItems(state = state.txHistoryState, txHistoryItems = txHistoryItems) } } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index e2735c652c..4f1008ac8c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -1,6 +1,8 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels -interface TokenDetailsClickIntents { +import com.tangem.core.ui.components.transactions.intents.TxHistoryClickIntents + +interface TokenDetailsClickIntents : TxHistoryClickIntents { fun onBackClick() diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 4f3b42bdda..a417d04cf8 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -4,13 +4,17 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.* +import androidx.paging.cachedIn import arrow.core.getOrElse import com.tangem.common.Provider import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.GetCurrencyUseCase import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState @@ -21,15 +25,20 @@ import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import javax.inject.Inject import kotlin.properties.Delegates +@Suppress("LongParameterList") @HiltViewModel internal class TokenDetailsViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val getSelectedWalletUseCase: GetSelectedWalletUseCase, private val getCurrencyUseCase: GetCurrencyUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, + private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, + private val getExploreUrlUseCase: GetExploreUrlUseCase, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents { @@ -45,6 +54,8 @@ internal class TokenDetailsViewModel @Inject constructor( currentStateProvider = Provider { uiState }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), clickIntents = this, + symbol = cryptoCurrency.symbol, + decimals = cryptoCurrency.decimals, ) var uiState: TokenDetailsState by mutableStateOf(stateFactory.getInitialState(cryptoCurrency)) private set @@ -63,6 +74,7 @@ internal class TokenDetailsViewModel @Inject constructor( private fun updateContent(selectedWallet: UserWallet, refresh: Boolean) { updateMarketPrice(selectedWallet = selectedWallet, refresh = refresh) + updateTxHistory() } private fun updateMarketPrice(selectedWallet: UserWallet, refresh: Boolean) { @@ -74,6 +86,28 @@ internal class TokenDetailsViewModel @Inject constructor( .saveIn(marketPriceJobHolder) } + private fun updateTxHistory() { + viewModelScope.launch(dispatchers.io) { + val txHistoryItemsCountEither = txHistoryItemsCountUseCase( + networkId = cryptoCurrency.network.id, + derivationPath = cryptoCurrency.derivationPath, + ) + + uiState = stateFactory.getLoadingTxHistoryState(itemsCountEither = txHistoryItemsCountEither) + + txHistoryItemsCountEither.onRight { + uiState = stateFactory.getLoadedTxHistoryState( + txHistoryEither = txHistoryItemsUseCase( + networkId = cryptoCurrency.network.id, + derivationPath = cryptoCurrency.derivationPath, + ).map { + it.cachedIn(viewModelScope) + }, + ) + } + } + } + private fun createSelectedAppCurrencyFlow(): StateFlow { return getSelectedAppCurrencyUseCase() .map { maybeAppCurrency -> @@ -93,4 +127,24 @@ internal class TokenDetailsViewModel @Inject constructor( override fun onMoreClick() { TODO("Not yet implemented") } + + override fun onBuyClick() { + // TODO: [REDACTED_JIRA] + } + + override fun onReloadClick() { + updateTxHistory() + } + + override fun onExploreClick() { + viewModelScope.launch { + val wallet = getWallet() + router.openUrl( + url = getExploreUrlUseCase( + userWalletId = wallet.walletId, + networkId = cryptoCurrency.network.id, + ), + ) + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt index 2dcc2ddc06..48d9423033 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt @@ -1,8 +1,9 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels +import com.tangem.core.ui.components.transactions.intents.TxHistoryClickIntents import com.tangem.domain.tokens.models.CryptoCurrency -internal interface WalletClickIntents { +internal interface WalletClickIntents : TxHistoryClickIntents { fun onBackClick() @@ -28,12 +29,6 @@ internal interface WalletClickIntents { fun onOrganizeTokensClick() - fun onBuyClick() - - fun onReloadClick() - - fun onExploreClick() - fun onUnlockWalletClick() fun onUnlockWalletNotificationClick() From f2abb0ee1c742df24dc14344f2bdffe663c383e0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 18 Aug 2023 13:00:01 +0300 Subject: [PATCH 14/44] Updated on 2026-08-14 --- core/ui/build.gradle.kts | 8 ++- .../core/ui/extensions/CryptoCurrency.kt | 45 ++++++++++++++ .../ui/src/main/res/drawable}/ic_qcx.webp | Bin .../ui}/src/main/res/drawable/ic_voyr.webp | Bin .../TokenDetailsSkeletonStateConverter.kt | 11 ++-- .../presentation/common/WalletPreviewData.kt | 30 ++++++---- .../common/component/TokenItem.kt | 56 ++++++++++++++---- .../common/state/TokenItemState.kt | 49 ++++++++------- .../CryptoCurrencyToDraggableItemConverter.kt | 22 ++----- .../domain/WalletAdditionalInfoFactory.kt | 2 +- ...ryptoCurrencyStatusToTokenItemConverter.kt | 26 +++----- 11 files changed, 159 insertions(+), 90 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt rename {app/src/main/res/drawable-ldpi => core/ui/src/main/res/drawable}/ic_qcx.webp (100%) rename {app => core/ui}/src/main/res/drawable/ic_voyr.webp (100%) diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 5cabb03840..6222c42337 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -5,6 +5,12 @@ plugins { } dependencies { + /** Project - Domain */ + implementation(projects.domain.tokens.models) + + /** Project - Core */ + implementation(projects.core.res) + /** AndroidX libraries */ implementation(deps.androidx.fragment.ktx) implementation(deps.androidx.paging.runtime) @@ -22,6 +28,4 @@ dependencies { implementation(deps.material) implementation(deps.compose.shimmer) implementation(deps.kotlin.immutable.collections) - - implementation(project(":core:res")) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt new file mode 100644 index 0000000000..7c32f55014 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt @@ -0,0 +1,45 @@ +package com.tangem.core.ui.extensions + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.R +import com.tangem.domain.tokens.models.CryptoCurrency + +/** + * Retrieves the resource ID for the network badge of a [CryptoCurrency]. + * + * This property provides a way to fetch the appropriate drawable resource ID + * for the network badge of a given cryptocurrency. For coins, this will typically + * return null as they do not have network badges, while tokens will fetch the icon + * based on their associated network ID. + * + * @return Drawable resource ID for the network badge or null if the cryptocurrency is a coin. + */ +@get:DrawableRes +val CryptoCurrency.networkBadgeIconResId: Int? + get() = when (this) { + is CryptoCurrency.Coin -> null + is CryptoCurrency.Token -> getActiveIconRes(network.id.value) + } + +/** + * Retrieves the resource ID for the icon of a [CryptoCurrency]. + * + * This property provides a way to fetch the appropriate drawable resource ID + * for the icon of a given cryptocurrency. + * + * @return Drawable resource ID for the cryptocurrency icon. + */ +@get:DrawableRes +val CryptoCurrency.iconResId: Int + get() = when (this) { + is CryptoCurrency.Coin -> { + val rawCoinId = id.rawCurrencyId + + if (rawCoinId != null) { + getActiveIconResByCoinId(rawCoinId, network.id.value) + } else { + R.drawable.ic_alert_24 + } + } + is CryptoCurrency.Token -> R.drawable.ic_alert_24 + } \ No newline at end of file diff --git a/app/src/main/res/drawable-ldpi/ic_qcx.webp b/core/ui/src/main/res/drawable/ic_qcx.webp similarity index 100% rename from app/src/main/res/drawable-ldpi/ic_qcx.webp rename to core/ui/src/main/res/drawable/ic_qcx.webp diff --git a/app/src/main/res/drawable/ic_voyr.webp b/core/ui/src/main/res/drawable/ic_voyr.webp similarity index 100% rename from app/src/main/res/drawable/ic_voyr.webp rename to core/ui/src/main/res/drawable/ic_voyr.webp diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 880da1991e..34823e4635 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -2,6 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.extensions.iconResId import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState @@ -10,7 +11,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDeta 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 import kotlinx.coroutines.flow.MutableStateFlow @@ -27,13 +27,12 @@ internal class TokenDetailsSkeletonStateConverter( tokenInfoBlockState = TokenInfoBlockState( name = value.cryptoCurrency.name, iconUrl = requireNotNull(value.cryptoCurrency.iconUrl), - currency = when (value.cryptoCurrency) { + currency = when (val currency = value.cryptoCurrency) { is CryptoCurrency.Coin -> TokenInfoBlockState.Currency.Native is CryptoCurrency.Token -> TokenInfoBlockState.Currency.Token( - networkName = value.cryptoCurrency.network.standardType.name, - blockchainName = value.cryptoCurrency.network.name, - // TODO: [REDACTED_JIRA] - networkIcon = R.drawable.img_eth_22, + networkName = currency.network.standardType.name, + blockchainName = currency.network.name, + networkIcon = currency.iconResId, ) }, ), 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 504297fbf1..aaa70af11f 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 @@ -87,7 +87,7 @@ internal object WalletPreviewData { id = UUID.randomUUID().toString(), tokenIconUrl = null, tokenIconResId = R.drawable.img_polygon_22, - networkIconResId = R.drawable.img_polygon_22, + networkBadgeIconResId = R.drawable.img_polygon_22, name = "Polygon", amount = "5,412 MATIC", hasPending = true, @@ -98,16 +98,24 @@ internal object WalletPreviewData { type = PriceChangeConfig.Type.UP, ), ), + isTestnet = false, onClick = {}, ) } + val testnetTokenItemVisibleState by lazy { + tokenItemVisibleState.copy( + name = "Polygon testnet", + isTestnet = true, + ) + } + val tokenItemHiddenState by lazy { TokenItemState.Content( id = UUID.randomUUID().toString(), tokenIconUrl = null, tokenIconResId = R.drawable.img_polygon_22, - networkIconResId = R.drawable.img_polygon_22, + networkBadgeIconResId = R.drawable.img_polygon_22, name = "Polygon", amount = "5,412 MATIC", hasPending = true, @@ -117,6 +125,7 @@ internal object WalletPreviewData { type = PriceChangeConfig.Type.UP, ), ), + isTestnet = false, onClick = {}, ) } @@ -126,8 +135,9 @@ internal object WalletPreviewData { id = UUID.randomUUID().toString(), tokenIconUrl = null, tokenIconResId = R.drawable.img_polygon_22, - networkIconResId = R.drawable.img_polygon_22, + networkBadgeIconResId = R.drawable.img_polygon_22, name = "Polygon", + isTestnet = false, fiatAmount = "3 172,14 $", ) } @@ -137,7 +147,7 @@ internal object WalletPreviewData { id = UUID.randomUUID().toString(), tokenIconUrl = null, tokenIconResId = R.drawable.img_polygon_22, - networkIconResId = R.drawable.img_polygon_22, + networkBadgeIconResId = R.drawable.img_polygon_22, name = "Polygon", ) } @@ -171,7 +181,7 @@ internal object WalletPreviewData { tokenItemState = tokenItemDragState.copy( id = "${group.id}_token_$tokenNumber", name = "Token $tokenNumber from $networkNumber network", - networkIconResId = R.drawable.img_eth_22.takeIf { i != 0 }, + networkBadgeIconResId = R.drawable.img_eth_22.takeIf { i != 0 }, ), groupId = group.id, roundingMode = when { @@ -271,7 +281,7 @@ internal object WalletPreviewData { id = "token_1", name = "Ethereum", tokenIconResId = R.drawable.img_eth_22, - networkIconResId = null, + networkBadgeIconResId = null, amount = "1,89340821 ETH", ), ), @@ -280,7 +290,7 @@ internal object WalletPreviewData { id = "token_2", name = "Ethereum", tokenIconResId = R.drawable.img_eth_22, - networkIconResId = null, + networkBadgeIconResId = null, amount = "1,89340821 ETH", ), ), @@ -289,7 +299,7 @@ internal object WalletPreviewData { id = "token_3", name = "Ethereum", tokenIconResId = R.drawable.img_eth_22, - networkIconResId = null, + networkBadgeIconResId = null, amount = "1,89340821 ETH", ), ), @@ -298,7 +308,7 @@ internal object WalletPreviewData { id = "token_4", name = "Ethereum", tokenIconResId = R.drawable.img_eth_22, - networkIconResId = null, + networkBadgeIconResId = null, amount = "1,89340821 ETH", ), ), @@ -308,7 +318,7 @@ internal object WalletPreviewData { id = "token_5", name = "Ethereum", tokenIconResId = R.drawable.img_eth_22, - networkIconResId = null, + networkBadgeIconResId = null, amount = "1,89340821 ETH", ), ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt index 0e9751d451..677001b7e5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -14,9 +14,12 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.ColorMatrix import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -42,6 +45,8 @@ import org.burnoutcrew.reorderable.ReorderableLazyListState import org.burnoutcrew.reorderable.detectReorder private const val DOTS = "•••" +private const val GRAY_SCALE_SATURATION = 0f + val TOKEN_ITEM_HEIGHT: Dp @Composable @ReadOnlyComposable @@ -49,6 +54,7 @@ val TOKEN_ITEM_HEIGHT: Dp @Composable internal fun TokenItem(state: TokenItemState, modifier: Modifier = Modifier) { + // TODO: Add custom token state: [REDACTED_JIRA] when (state) { is TokenItemState.Content -> ContentTokenItem(state, modifier) is TokenItemState.Loading -> LoadingTokenItem(modifier) @@ -65,7 +71,7 @@ private fun ContentTokenItem(content: TokenItemState.Content, modifier: Modifier name = content.name, tokenIconUrl = content.tokenIconUrl, tokenIconResId = content.tokenIconResId, - networkIconResId = content.networkIconResId, + networkBadgeIconResId = content.networkBadgeIconResId, amount = if (content.tokenOptions is TokenOptionsState.Hidden) DOTS else content.amount, hasPending = content.hasPending, options = { ref -> @@ -74,6 +80,7 @@ private fun ContentTokenItem(content: TokenItemState.Content, modifier: Modifier state = content.tokenOptions, ) }, + isTestnet = content.isTestnet, ) } @@ -88,9 +95,10 @@ internal fun DraggableTokenItem( name = state.name, tokenIconUrl = state.tokenIconUrl, tokenIconResId = state.tokenIconResId, - networkIconResId = state.networkIconResId, + networkBadgeIconResId = state.networkBadgeIconResId, amount = state.fiatAmount, hasPending = false, + isTestnet = state.isTestnet, options = { ref -> Box( modifier = Modifier @@ -122,7 +130,7 @@ internal fun UnreachableTokenItem(state: TokenItemState.Unreachable, modifier: M name = state.name, tokenIconUrl = state.tokenIconUrl, tokenIconResId = state.tokenIconResId, - networkIconResId = state.networkIconResId, + networkBadgeIconResId = state.networkBadgeIconResId, amount = null, hasPending = false, options = { ref -> @@ -213,11 +221,12 @@ private fun InternalTokenItem( name: String, tokenIconUrl: String?, @DrawableRes tokenIconResId: Int, - @DrawableRes networkIconResId: Int?, + @DrawableRes networkBadgeIconResId: Int?, amount: String?, hasPending: Boolean, options: @Composable ConstraintLayoutScope.(ref: ConstrainedLayoutReference) -> Unit, modifier: Modifier = Modifier, + isTestnet: Boolean = false, onClick: (() -> Unit)? = null, ) { BaseSurface( @@ -241,7 +250,8 @@ private fun InternalTokenItem( }, tokenIconUrl = tokenIconUrl, tokenIconResId = tokenIconResId, - networkIconRes = networkIconResId, + networkBadgeIconRes = networkBadgeIconResId, + isTestnet = isTestnet, ) TokenTitleAmountBlock( @@ -379,9 +389,10 @@ private fun TokenFiatPercentageBlock( @Composable private fun TokenIcon( tokenIconUrl: String?, + @DrawableRes tokenIconResId: Int, + isTestnet: Boolean, modifier: Modifier = Modifier, - @DrawableRes tokenIconResId: Int? = null, - @DrawableRes networkIconRes: Int? = null, + @DrawableRes networkBadgeIconRes: Int? = null, ) { Box( modifier = modifier @@ -392,19 +403,36 @@ private fun TokenIcon( .align(Alignment.BottomStart) .size(TangemTheme.dimens.size36) - val data = if (tokenIconUrl.isNullOrEmpty()) tokenIconResId else tokenIconUrl + val iconData: Any = remember(tokenIconUrl) { + if (tokenIconUrl.isNullOrEmpty()) tokenIconResId else tokenIconUrl + } + val colorFilter = remember(isTestnet) { + if (isTestnet) { + val colorMatrix = ColorMatrix().also { + it.setToSaturation(GRAY_SCALE_SATURATION) + } + + ColorFilter.colorMatrix(colorMatrix) + } else { + null + } + } + SubcomposeAsyncImage( modifier = tokenImageModifier, model = ImageRequest.Builder(LocalContext.current) - .data(data) - .crossfade(true) + .data(iconData) + .placeholder(tokenIconResId) + .error(tokenIconResId) + .fallback(tokenIconResId) + .crossfade(enable = true) .build(), - loading = { CircleShimmer(modifier = tokenImageModifier) }, + colorFilter = colorFilter, contentDescription = null, ) AnimatedVisibility( - visible = networkIconRes != null, + visible = networkBadgeIconRes != null, modifier = Modifier .align(Alignment.TopEnd) .size(TangemTheme.dimens.size18) @@ -414,7 +442,8 @@ private fun TokenIcon( modifier = Modifier .padding(all = TangemTheme.dimens.spacing0_5) .align(Alignment.Center), - painter = painterResource(id = requireNotNull(networkIconRes)), + painter = painterResource(id = requireNotNull(networkBadgeIconRes)), + colorFilter = colorFilter, contentDescription = null, ) } @@ -446,6 +475,7 @@ private class TokenConfigProvider : CollectionPreviewParameterProvider Unit, ) : TokenItemState /** * Draggable token state * - * @property id unique id - * @property tokenIconUrl token icon url - * @property tokenIconResId token icon resource id - * @property networkIconResId network icon resource id, may be null if it is a coin - * @property name token name - * @property fiatAmount fiat amount of token + * @property id unique id + * @property tokenIconUrl token icon url + * @property tokenIconResId token icon resource id + * @property networkBadgeIconResId network badge icon resource id, may be null if it is a coin + * @property name token name + * @property fiatAmount fiat amount of token + * @property isTestnet indicates whether the token is from test network or not */ data class Draggable( override val id: String, val tokenIconUrl: String?, @DrawableRes val tokenIconResId: Int, - @DrawableRes val networkIconResId: Int?, + @DrawableRes val networkBadgeIconResId: Int?, val name: String, val fiatAmount: String, + val isTestnet: Boolean, ) : TokenItemState /** * Unreachable token state * - * @property id token id - * @property tokenIconUrl token icon url - * @property tokenIconResId token icon resource id - * @property networkIconResId network icon resource id, may be null if it is a coin - * @property name token name + * @property id token id + * @property tokenIconUrl token icon url + * @property tokenIconResId token icon resource id + * @property networkBadgeIconResId network badge icon resource id, may be null if it is a coin + * @property name token name */ data class Unreachable( override val id: String, val tokenIconUrl: String?, @DrawableRes val tokenIconResId: Int, - @DrawableRes val networkIconResId: Int?, + @DrawableRes val networkBadgeIconResId: Int?, val name: String, ) : TokenItemState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt index 2b1cf0d0bd..11f4747c2b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -1,12 +1,11 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items -import androidx.annotation.DrawableRes import com.tangem.common.Provider +import com.tangem.core.ui.extensions.iconResId +import com.tangem.core.ui.extensions.networkBadgeIconResId import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.models.CryptoCurrency -import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId @@ -17,18 +16,6 @@ internal class CryptoCurrencyToDraggableItemConverter( private val appCurrencyProvider: Provider, ) : Converter { - private val CryptoCurrency.networkIconResId: Int? - @DrawableRes get() { - // TODO: [REDACTED_JIRA] - return if (this is CryptoCurrency.Coin) null else R.drawable.img_eth_22 - } - - private val CryptoCurrency.tokenIconResId: Int - @DrawableRes get() { - // TODO: [REDACTED_JIRA] - return R.drawable.img_eth_22 - } - override fun convert(value: CryptoCurrencyStatus): DraggableItem.Token { return createDraggableToken(value, appCurrencyProvider()) } @@ -58,10 +45,11 @@ internal class CryptoCurrencyToDraggableItemConverter( return TokenItemState.Draggable( id = getTokenItemId(currency.id), tokenIconUrl = currency.iconUrl, - tokenIconResId = currency.tokenIconResId, - networkIconResId = currency.networkIconResId, + tokenIconResId = currencyStatus.currency.iconResId, + networkBadgeIconResId = currencyStatus.currency.networkBadgeIconResId, name = currency.name, fiatAmount = getFormattedFiatAmount(currencyStatus, appCurrency), + isTestnet = currencyStatus.currency.network.isTestnet, ) } 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 699a8bea7d..dc61e70f2c 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 @@ -54,7 +54,7 @@ internal object WalletAdditionalInfoFactory { isLocked -> { backupInfoRes + TextReference.Res(R.string.common_locked) } - else -> error("It isn't exist additional info for this case") + else -> error("It isn't exist additional info for this case") // FIXME: Crashes on dev cards } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt index 4c9c49ed14..19347315dd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt @@ -1,13 +1,12 @@ package com.tangem.feature.wallet.presentation.wallet.utils -import androidx.annotation.DrawableRes import com.tangem.common.Provider import com.tangem.core.ui.components.marketprice.PriceChangeConfig +import com.tangem.core.ui.extensions.iconResId +import com.tangem.core.ui.extensions.networkBadgeIconResId import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.models.CryptoCurrency -import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter @@ -19,18 +18,6 @@ internal class CryptoCurrencyStatusToTokenItemConverter( private val clickIntents: WalletClickIntents, ) : Converter { - private val CryptoCurrencyStatus.networkIconResId: Int? - @DrawableRes get() { - // TODO: [REDACTED_JIRA] - return if (currency is CryptoCurrency.Coin) null else R.drawable.img_eth_22 - } - - private val CryptoCurrencyStatus.tokenIconResId: Int - @DrawableRes get() { - // TODO: [REDACTED_JIRA] - return R.drawable.img_eth_22 - } - override fun convert(value: CryptoCurrencyStatus): TokenItemState { return when (value.value) { is CryptoCurrencyStatus.Loading -> TokenItemState.Loading(id = value.currency.id.value) @@ -51,8 +38,8 @@ internal class CryptoCurrencyStatusToTokenItemConverter( id = currency.id.value, name = currency.name, tokenIconUrl = currency.iconUrl, - tokenIconResId = this.tokenIconResId, - networkIconResId = this.networkIconResId, + tokenIconResId = currency.iconResId, + networkBadgeIconResId = currency.networkBadgeIconResId, amount = getFormattedAmount(), hasPending = value.hasTransactionsInProgress, tokenOptions = if (isWalletContentHidden) { @@ -63,6 +50,7 @@ internal class CryptoCurrencyStatusToTokenItemConverter( priceChange = getPriceChangeConfig(), ) }, + isTestnet = currency.network.isTestnet, onClick = { clickIntents.onTokenClick(currency) }, ) } @@ -84,8 +72,8 @@ internal class CryptoCurrencyStatusToTokenItemConverter( id = currency.id.value, name = currency.name, tokenIconUrl = currency.iconUrl, - tokenIconResId = this.tokenIconResId, - networkIconResId = this.networkIconResId, + tokenIconResId = currency.iconResId, + networkBadgeIconResId = currency.networkBadgeIconResId, ) private fun CryptoCurrencyStatus.getPriceChangeConfig(): PriceChangeConfig { From 731fd53e3d4a0a2739312b5004eba620f894655c Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 22 Aug 2023 19:05:00 +0300 Subject: [PATCH 15/44] Updated on 2026-08-14 --- .../data/tokens/utils/NetworkStatusFactory.kt | 100 +++++++++++++++- domain/legacy/build.gradle.kts | 1 + .../model/CryptoCurrencyAmount.kt | 2 +- .../model/CryptoCurrencyTransaction.kt | 28 +++++ .../model/UpdateWalletManagerResult.kt | 12 +- .../utils/UpdateWalletManagerResultFactory.kt | 113 +++++++++++++++--- domain/tokens/build.gradle.kts | 3 + .../tokens/model/CryptoCurrencyStatus.kt | 30 +++-- .../domain/tokens/model/NetworkAddress.kt | 43 +++++++ .../domain/tokens/model/NetworkStatus.kt | 16 ++- .../domain/tokens/model/PendingTransaction.kt | 40 +++++++ .../operations/CurrencyStatusOperations.kt | 11 +- .../tangem/domain/tokens/mock/MockNetworks.kt | 11 +- .../domain/tokens/mock/MockTokensStates.kt | 6 +- ...ryptoCurrencyStatusToTokenItemConverter.kt | 2 +- 15 files changed, 369 insertions(+), 49 deletions(-) create mode 100644 domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyTransaction.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkAddress.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/PendingTransaction.kt diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt index 6feee38172..0127ed7330 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt @@ -1,10 +1,14 @@ package com.tangem.data.tokens.utils +import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.model.PendingTransaction import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.Network import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount +import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult +import timber.log.Timber import java.math.BigDecimal internal class NetworkStatusFactory { @@ -19,10 +23,18 @@ internal class NetworkStatusFactory { value = when (result) { is UpdateWalletManagerResult.MissedDerivation -> NetworkStatus.MissedDerivation is UpdateWalletManagerResult.Unreachable -> NetworkStatus.Unreachable - is UpdateWalletManagerResult.NoAccount -> NetworkStatus.NoAccount(result.amountToCreateAccount) + is UpdateWalletManagerResult.NoAccount -> NetworkStatus.NoAccount( + address = getNetworkAddress(result.defaultAddress, result.addresses), + amountToCreateAccount = result.amountToCreateAccount, + ) is UpdateWalletManagerResult.Verified -> NetworkStatus.Verified( - amounts = formatAmounts(result.tokensAmounts, currencies), - hasTransactionsInProgress = result.hasTransactionsInProgress, + address = getNetworkAddress(result.defaultAddress, result.addresses), + amounts = formatAmounts(result.currenciesAmounts, currencies), + pendingTransactions = formatTransactions( + networksAddresses = result.addresses, + transactions = result.currentTransactions, + currencies = currencies, + ), ) }, ) @@ -39,13 +51,91 @@ internal class NetworkStatusFactory { is CryptoCurrencyAmount.Coin -> currencies.singleOrNull { it is CryptoCurrency.Coin } is CryptoCurrencyAmount.Token -> currencies.firstOrNull { it is CryptoCurrency.Token && - it.id.rawCurrencyId == amount.id && + it.id.rawCurrencyId == amount.tokenId && it.contractAddress == amount.tokenContractAddress } } - currency?.id?.let { it to amount.value } + if (currency == null) { + Timber.e("Unable to find cryptocurrency for amount: $amount") + null + } else { + currency.id to amount.value + } } .toMap() } + + private fun formatTransactions( + networksAddresses: Set, + transactions: Set, + currencies: Set, + ): Map> { + if (transactions.isEmpty()) return emptyMap() + + return currencies + .asSequence() + .map { currency -> + val currencyTransactions = when (currency) { + is CryptoCurrency.Coin -> transactions.filterTo(hashSetOf()) { transaction -> + transaction is CryptoCurrencyTransaction.Coin + } + is CryptoCurrency.Token -> transactions.filterTo(hashSetOf()) { transaction -> + transaction is CryptoCurrencyTransaction.Token && + transaction.tokenId == currency.id.rawCurrencyId && + transaction.tokenContractAddress == currency.contractAddress + } + } + + currency.id to createCurrentTransactions(networksAddresses, currencyTransactions) + } + .toMap() + } + + private fun createCurrentTransactions( + networksAddresses: Set, + transactions: Set, + ): Set { + return transactions.mapNotNullTo(hashSetOf()) { createCurrentTransaction(networksAddresses, it) } + } + + private fun createCurrentTransaction( + networksAddresses: Set, + transaction: CryptoCurrencyTransaction, + ): PendingTransaction? { + val direction = when { + transaction.toAddress in networksAddresses -> PendingTransaction.Direction.Incoming( + fromAddress = transaction.fromAddress, + ) + transaction.fromAddress in networksAddresses -> PendingTransaction.Direction.Outgoing( + toAddress = transaction.toAddress, + ) + else -> { + Timber.e( + """ + Unable to find transaction direction + |- To address: ${transaction.toAddress} + |- From address: ${transaction.fromAddress} + |- Network addresses: $networksAddresses + """.trimIndent(), + ) + + return null + } + } + + return PendingTransaction( + amount = transaction.amount, + direction = direction, + sentAt = transaction.sentAt, + ) + } + + private fun getNetworkAddress(defaultAddress: String, availableAddresses: Set): NetworkAddress { + return if (availableAddresses.size != 1) { + NetworkAddress.Selectable(defaultAddress, availableAddresses) + } else { + NetworkAddress.Single(defaultAddress) + } + } } \ No newline at end of file diff --git a/domain/legacy/build.gradle.kts b/domain/legacy/build.gradle.kts index 9ba0f990f3..69f443892c 100644 --- a/domain/legacy/build.gradle.kts +++ b/domain/legacy/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { implementation(deps.moshi.kotlin) implementation(deps.timber) implementation(deps.kotlin.coroutines) + implementation(deps.jodatime) /** Testing libraries */ testImplementation(deps.test.junit) diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyAmount.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyAmount.kt index ae72f37ec4..6ae9f6ae43 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyAmount.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyAmount.kt @@ -9,7 +9,7 @@ sealed class CryptoCurrencyAmount { data class Coin(override val value: BigDecimal) : CryptoCurrencyAmount() data class Token( - val id: String?, + val tokenId: String?, val tokenContractAddress: String, override val value: BigDecimal, ) : CryptoCurrencyAmount() diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyTransaction.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyTransaction.kt new file mode 100644 index 0000000000..c0b69b941f --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyTransaction.kt @@ -0,0 +1,28 @@ +package com.tangem.domain.walletmanager.model + +import org.joda.time.DateTime +import java.math.BigDecimal + +sealed class CryptoCurrencyTransaction { + + abstract val amount: BigDecimal + abstract val fromAddress: String? + abstract val toAddress: String? + abstract val sentAt: DateTime + + data class Coin( + override val amount: BigDecimal, + override val fromAddress: String?, + override val toAddress: String?, + override val sentAt: DateTime, + ) : CryptoCurrencyTransaction() + + data class Token( + val tokenId: String?, + val tokenContractAddress: String, + override val amount: BigDecimal, + override val fromAddress: String?, + override val toAddress: String?, + override val sentAt: DateTime, + ) : CryptoCurrencyTransaction() +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/UpdateWalletManagerResult.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/UpdateWalletManagerResult.kt index a516c03d0a..4ad842a7dd 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/UpdateWalletManagerResult.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/UpdateWalletManagerResult.kt @@ -9,9 +9,15 @@ sealed class UpdateWalletManagerResult { object Unreachable : UpdateWalletManagerResult() data class Verified( - val tokensAmounts: Set, - val hasTransactionsInProgress: Boolean, // TODO: May be add recent transactions + val defaultAddress: String, + val addresses: Set, + val currenciesAmounts: Set, + val currentTransactions: Set, ) : UpdateWalletManagerResult() - data class NoAccount(val amountToCreateAccount: BigDecimal) : UpdateWalletManagerResult() + data class NoAccount( + val defaultAddress: String, + val addresses: Set, + val amountToCreateAccount: BigDecimal, + ) : UpdateWalletManagerResult() } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt index df5737de85..4d644e4b7c 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt @@ -1,31 +1,39 @@ package com.tangem.domain.walletmanager.utils import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.address.Address import com.tangem.domain.common.extensions.amountToCreateAccount import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount +import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import org.joda.time.Instant import timber.log.Timber import java.math.BigDecimal +import java.util.Calendar internal class UpdateWalletManagerResultFactory { fun getResult(walletManager: WalletManager): UpdateWalletManagerResult.Verified { - val hasNotConfirmedTransactions = walletManager.wallet - .recentTransactions - .any { it.status != TransactionStatus.Confirmed } - - val amounts = walletManager.wallet.amounts + val wallet = walletManager.wallet return UpdateWalletManagerResult.Verified( - tokensAmounts = getTokensAmounts(amounts.values.toSet()), - hasTransactionsInProgress = hasNotConfirmedTransactions, + defaultAddress = wallet.address, + addresses = getAvailableAddresses(wallet.addresses), + currenciesAmounts = getTokensAmounts(wallet.amounts.values.toSet()), + currentTransactions = getCurrentTransactions(wallet.recentTransactions.toSet()), ) } fun getDemoResult(walletManager: WalletManager, demoAmount: Amount): UpdateWalletManagerResult.Verified { + val wallet = walletManager.wallet + return UpdateWalletManagerResult.Verified( - tokensAmounts = getDemoTokensAmounts(demoAmount, walletManager.cardTokens), - hasTransactionsInProgress = false, + defaultAddress = wallet.address, + addresses = getAvailableAddresses(wallet.addresses), + currenciesAmounts = getDemoTokensAmounts(demoAmount, walletManager.cardTokens), + currentTransactions = getCurrentTransactions(wallet.recentTransactions.toSet()), ) } @@ -40,13 +48,17 @@ internal class UpdateWalletManagerResultFactory { "Unable to get required amount to create account for: $blockchain" } - return UpdateWalletManagerResult.NoAccount(amountToCreateAccount) + return UpdateWalletManagerResult.NoAccount( + defaultAddress = wallet.address, + addresses = getAvailableAddresses(wallet.addresses), + amountToCreateAccount = amountToCreateAccount, + ) } private fun getTokensAmounts(amounts: Set): Set { val mutableAmounts = hashSetOf() - return amounts.mapNotNullTo(mutableAmounts, ::getTokenAmount) + return amounts.mapNotNullTo(mutableAmounts, ::createCurrencyAmount) } private fun getDemoTokensAmounts(demoAmount: Amount, tokens: Set): Set { @@ -58,27 +70,94 @@ internal class UpdateWalletManagerResultFactory { } } - private fun getTokenAmount(amount: Amount): CryptoCurrencyAmount? { + private fun getCurrentTransactions(recentTransactions: Set): Set { + val unconfirmedTransactions = recentTransactions.filter { + it.status == TransactionStatus.Unconfirmed + } + + return unconfirmedTransactions.mapNotNullTo(hashSetOf(), ::createCurrencyTransaction) + } + + private fun createCurrencyAmount(amount: Amount): CryptoCurrencyAmount? { return when (val type = amount.type) { is AmountType.Token -> CryptoCurrencyAmount.Token( - id = type.token.id, + tokenId = type.token.id, tokenContractAddress = type.token.contractAddress, - value = getAmountValue(amount) ?: return null, + value = getCurrencyAmountValue(amount) ?: return null, ) is AmountType.Coin -> CryptoCurrencyAmount.Coin( - value = getAmountValue(amount) ?: return null, + value = getCurrencyAmountValue(amount) ?: return null, ) is AmountType.Reserve -> null } } - private fun getAmountValue(amount: Amount): BigDecimal? { + private fun createCurrencyTransaction(data: TransactionData): CryptoCurrencyTransaction? { + val fromAddress = takeAddressIfNotUnknown(data.sourceAddress) + val toAddress = takeAddressIfNotUnknown(data.destinationAddress) + val amount = getTransactionAmountValue(data.amount) ?: return null + val sentAt = getTransactionSentTime(data.date) ?: return null + + return when (val type = data.amount.type) { + is AmountType.Coin -> CryptoCurrencyTransaction.Coin( + amount = amount, + fromAddress = fromAddress, + toAddress = toAddress, + sentAt = sentAt, + ) + is AmountType.Token -> CryptoCurrencyTransaction.Token( + tokenId = type.token.id, + tokenContractAddress = type.token.contractAddress, + amount = amount, + fromAddress = fromAddress, + toAddress = toAddress, + sentAt = sentAt, + ) + is AmountType.Reserve -> null + } + } + + private fun getAvailableAddresses(addresses: Set
): Set { + return addresses.mapTo(hashSetOf()) { it.value } + } + + private fun getCurrencyAmountValue(amount: Amount): BigDecimal? { val value = amount.value if (value == null) { - Timber.e("Amount not found for currency: ${amount.currencySymbol}") + Timber.e("Currency amount must not be null: ${amount.currencySymbol}") } return value } + + private fun getTransactionAmountValue(amount: Amount): BigDecimal? { + val value = amount.value + + if (value == null) { + Timber.e("Transaction amount must not be null: ${amount.currencySymbol}") + } + + return value + } + + private fun getTransactionSentTime(date: Calendar?): DateTime? { + if (date == null) { + Timber.e("Transaction date must not be null") + return null + } + + val instant = Instant.ofEpochMilli(date.timeInMillis) + val timeZone = DateTimeZone.forTimeZone(date.timeZone) + + return instant.toDateTime(timeZone) + } + + private fun takeAddressIfNotUnknown(address: String): String? { + return address.takeIf { it.isNotBlank() && it != UNKNOWN_TRANSACTION_ADDRESS } + } + + private companion object { + const val UNKNOWN_TRANSACTION_ADDRESS = "unknown" + } } \ No newline at end of file diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index e2b240e5bc..1ca46ccf7a 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -14,6 +14,9 @@ dependencies { /** Project - Other */ implementation(projects.core.utils) + /** Utils */ + implementation(deps.jodatime) + /** Tests */ testImplementation(deps.test.junit) testImplementation(deps.test.coroutine) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt index 1f2db6777f..9c167104ee 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt @@ -35,8 +35,11 @@ data class CryptoCurrencyStatus( /** The change in price of the token. */ open val priceChange: BigDecimal? = null - /** Indicates if there are any transactions in progress related to the token. */ - open val hasTransactionsInProgress: Boolean = false + /** Indicates if there are any transactions in progress related to the cryptocurrency network. */ + open val hasCurrentNetworkTransactions: Boolean = false + + /** The pending cryptocurrency transactions. */ + open val pendingTransactions: Set = emptySet() } /** Represents the Loading state of a token, typically while fetching its details. */ @@ -58,15 +61,17 @@ data class CryptoCurrencyStatus( * @property fiatAmount The fiat equivalent of the token's amount. * @property fiatRate The exchange rate used for converting the token amount to fiat. * @property priceChange The change in price of the token. - * @property hasTransactionsInProgress Indicates if there are any transactions in progress related to the token - * network. + * @property hasCurrentNetworkTransactions Indicates if there are any transactions in progress related to the + * cryptocurrency network. + * @property pendingTransactions The current cryptocurrency transactions. */ data class Loaded( override val amount: BigDecimal, override val fiatAmount: BigDecimal, override val fiatRate: BigDecimal, override val priceChange: BigDecimal, - override val hasTransactionsInProgress: Boolean, + override val hasCurrentNetworkTransactions: Boolean, + override val pendingTransactions: Set, ) : Status() /** @@ -76,25 +81,30 @@ data class CryptoCurrencyStatus( * @property fiatAmount The fiat equivalent of the token's amount (optional). * @property fiatRate The exchange rate used for converting the token amount to fiat (optional). * @property priceChange The change in price of the token (optional). - * @property hasTransactionsInProgress Indicates if there are any transactions in progress related to the token - * network. + * @property hasCurrentNetworkTransactions Indicates if there are any transactions in progress related to the + * cryptocurrency network. + * @property pendingTransactions The current cryptocurrency transactions. */ data class Custom( override val amount: BigDecimal, override val fiatAmount: BigDecimal?, override val fiatRate: BigDecimal?, override val priceChange: BigDecimal?, - override val hasTransactionsInProgress: Boolean, + override val hasCurrentNetworkTransactions: Boolean, + override val pendingTransactions: Set, ) : Status() /** * Represents a state where the token is available, but there is no current quote available for it. * * @property amount The amount of the token. - * @property hasTransactionsInProgress Indicates if there are any transactions in progress related to the token. + * @property hasCurrentNetworkTransactions Indicates if there are any transactions in progress related to the + * cryptocurrency network. + * @property pendingTransactions The current cryptocurrency transactions. */ data class NoQuote( override val amount: BigDecimal, - override val hasTransactionsInProgress: Boolean, + override val hasCurrentNetworkTransactions: Boolean, + override val pendingTransactions: Set, ) : Status() } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkAddress.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkAddress.kt new file mode 100644 index 0000000000..542d48485e --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkAddress.kt @@ -0,0 +1,43 @@ +package com.tangem.domain.tokens.model + +/** + * Represents a network address configuration. + */ +sealed class NetworkAddress { + + /** The default or currently selected network address. */ + abstract val defaultAddress: String + + /** + * Represents a single static network address. + * + * @property defaultAddress The static network address. + */ + data class Single(override val defaultAddress: String) : NetworkAddress() { + + init { + checkDefaultAddress() + } + } + + /** + * Represents a network configuration where an address can be chosen from a set of available addresses. + * + * @property defaultAddress The currently selected or default network address. + * @property availableAddresses The set of available network addresses to choose from. + */ + data class Selectable( + override val defaultAddress: String, + val availableAddresses: Set, + ) : NetworkAddress() { + + init { + checkDefaultAddress() + require(availableAddresses.isNotEmpty()) { "Available network addresses must not be empty" } + } + } + + protected fun checkDefaultAddress() { + require(defaultAddress.isNotBlank()) { "Selected network address must not be blank" } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt index cfb815d9af..e9d5ec7405 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt @@ -33,20 +33,28 @@ data class NetworkStatus( object MissedDerivation : Status() /** - * Represents the verified state of the network, including the amounts associated with different cryptocurrencies and whether there are transactions in progress. + * Represents the verified state of the network, including the amounts associated with different cryptocurrencies + * and whether there are transactions in progress. * + * @property address Network addresses. * @property amounts A map containing the amounts associated with different cryptocurrencies within the network. - * @property hasTransactionsInProgress A boolean indicating whether there are transactions in progress within the network. + * @property pendingTransactions A map containing pending transactions associated with different cryptocurrencies + * within the network. */ data class Verified( + val address: NetworkAddress, val amounts: Map, - val hasTransactionsInProgress: Boolean, + val pendingTransactions: Map>, ) : Status() /** * Represents the state where there is no account, and an amount is required to create one. * + * @property address Network addresses. * @property amountToCreateAccount The amount required to create an account within the network. */ - data class NoAccount(val amountToCreateAccount: BigDecimal) : Status() + data class NoAccount( + val address: NetworkAddress, + val amountToCreateAccount: BigDecimal, + ) : Status() } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/PendingTransaction.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/PendingTransaction.kt new file mode 100644 index 0000000000..2c4084b81a --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/PendingTransaction.kt @@ -0,0 +1,40 @@ +package com.tangem.domain.tokens.model + +import org.joda.time.DateTime +import java.math.BigDecimal + +/** + * Represents a cryptocurrency transaction that is currently in progress. + * + * @property amount The monetary amount involved in the transaction. + * @property direction The direction of the transaction, indicating if it's an incoming or outgoing transaction. + * @property sentAt The timestamp when the transaction was executed. + */ +data class PendingTransaction( + val amount: BigDecimal, + val direction: Direction, + val sentAt: DateTime, +) { + + /** + * Represents the direction of the transaction. + */ + sealed class Direction { + + /** + * Represents an incoming transaction. + * + * @property fromAddress The source address from which the assets are being received. May be `null` if + * transaction received from unknown address. + */ + data class Incoming(val fromAddress: String?) : Direction() + + /** + * Represents an outgoing transaction. + * + * @property toAddress The destination address to which the assets are being sent. May be `null` if transaction + * sent to unknown address. + */ + data class Outgoing(val toAddress: String?) : Direction() + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt index b37f324c1f..3498bb05e3 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt @@ -27,18 +27,22 @@ internal class CurrencyStatusOperations( private fun createStatus(status: NetworkStatus.Verified): CryptoCurrencyStatus.Status { val amount = status.amounts[currency.id] ?: return CryptoCurrencyStatus.Unreachable + val hasCurrentNetworkTransactions = status.pendingTransactions.isNotEmpty() + val currentTransactions = status.pendingTransactions.getOrElse(currency.id, ::emptySet) return when { ignoreQuote -> CryptoCurrencyStatus.NoQuote( amount = amount, - hasTransactionsInProgress = status.hasTransactionsInProgress, + hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, + pendingTransactions = currentTransactions, ) currency is CryptoCurrency.Token && currency.isCustom -> CryptoCurrencyStatus.Custom( amount = amount, fiatAmount = calculateFiatAmountOrNull(amount, quote?.fiatRate), fiatRate = quote?.fiatRate, priceChange = quote?.priceChange, - hasTransactionsInProgress = status.hasTransactionsInProgress, + hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, + pendingTransactions = currentTransactions, ) quote == null -> CryptoCurrencyStatus.Loading else -> CryptoCurrencyStatus.Loaded( @@ -46,7 +50,8 @@ internal class CurrencyStatusOperations( fiatAmount = calculateFiatAmount(amount, quote.fiatRate), fiatRate = quote.fiatRate, priceChange = quote.priceChange, - hasTransactionsInProgress = status.hasTransactionsInProgress, + hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, + pendingTransactions = currentTransactions, ) } } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt index 5823b20556..831d0c308b 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt @@ -2,6 +2,7 @@ package com.tangem.domain.tokens.mock import arrow.core.NonEmptySet import arrow.core.nonEmptySetOf +import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.tokens.models.Network import java.math.BigDecimal @@ -48,6 +49,7 @@ internal object MockNetworks { networkId = network3.id, value = NetworkStatus.NoAccount( amountToCreateAccount = amountToCreateAccount, + address = NetworkAddress.Single(defaultAddress = "mock"), ), ) @@ -61,7 +63,8 @@ internal object MockNetworks { MockTokens.token2.id to BigDecimal.TEN, MockTokens.token3.id to BigDecimal.TEN, ), - hasTransactionsInProgress = false, + pendingTransactions = emptyMap(), + address = NetworkAddress.Single(defaultAddress = "mock"), ), ) @@ -73,7 +76,8 @@ internal object MockNetworks { MockTokens.token5.id to BigDecimal.TEN, MockTokens.token6.id to BigDecimal.TEN, ), - hasTransactionsInProgress = false, + pendingTransactions = emptyMap(), + address = NetworkAddress.Single(defaultAddress = "mock"), ), ) @@ -86,7 +90,8 @@ internal object MockNetworks { MockTokens.token9.id to BigDecimal.TEN, MockTokens.token10.id to BigDecimal.TEN, ), - hasTransactionsInProgress = false, + pendingTransactions = emptyMap(), + address = NetworkAddress.Single(defaultAddress = "mock"), ), ) diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index acd225aa87..aa477fee0c 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -83,7 +83,8 @@ internal object MockTokensStates { fiatAmount = fiatAmount, fiatRate = quote.fiatRate, priceChange = quote.priceChange, - hasTransactionsInProgress = false, + pendingTransactions = emptySet(), + hasCurrentNetworkTransactions = false, ), ) } @@ -92,7 +93,8 @@ internal object MockTokensStates { currency.copy( value = CryptoCurrencyStatus.NoQuote( amount = currency.value.amount!!, - hasTransactionsInProgress = false, + pendingTransactions = emptySet(), + hasCurrentNetworkTransactions = false, ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt index 19347315dd..292c7aca8e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt @@ -41,7 +41,7 @@ internal class CryptoCurrencyStatusToTokenItemConverter( tokenIconResId = currency.iconResId, networkBadgeIconResId = currency.networkBadgeIconResId, amount = getFormattedAmount(), - hasPending = value.hasTransactionsInProgress, + hasPending = value.hasCurrentNetworkTransactions, tokenOptions = if (isWalletContentHidden) { TokenItemState.TokenOptionsState.Hidden(getPriceChangeConfig()) } else { From 0af5b06cbb0f5295940202514175cacc0488d462 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 23 Aug 2023 15:02:05 +0800 Subject: [PATCH 16/44] Updated on 2026-08-14 --- .../marketprice/MarketPriceBlock.kt | 2 +- .../ui/components/transactions/Transaction.kt | 43 +++++++ .../transactions/state/TransactionState.kt | 7 ++ .../wallet/WalletLockedContentState.kt | 7 -- .../common/component/TokenItem.kt | 73 +++++++++++ .../common/state/TokenItemState.kt | 2 + .../domain/WalletAdditionalInfoFactory.kt | 32 +++-- .../wallet/state/WalletSingleCurrencyState.kt | 16 ++- .../state/components/WalletCardState.kt | 82 +++++++------ .../state/components/WalletTokensListState.kt | 15 +-- .../WalletLoadedTokensListConverter.kt | 7 +- .../factory/WalletRefreshStateConverter.kt | 52 ++++---- ...letSingleCurrencyLoadedBalanceConverter.kt | 4 +- .../factory/WalletSkeletonStateConverter.kt | 6 + .../state/factory/WalletStateFactory.kt | 46 ++++++- .../WalletLoadingTxHistoryConverter.kt | 38 +++--- .../wallet/ui/components/common/WalletCard.kt | 114 ++++++++++-------- .../utils/FiatBalanceToWalletCardConverter.kt | 10 +- .../utils/TokenListToWalletStateConverter.kt | 5 +- .../wallet/viewmodels/WalletViewModel.kt | 4 +- 20 files changed, 388 insertions(+), 177 deletions(-) delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/wallet/WalletLockedContentState.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt index 4192706627..f498b4caf7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt @@ -167,7 +167,7 @@ private fun LoadingContent() { RectangleShimmer( modifier = Modifier.size( width = TangemTheme.dimens.size158, - height = TangemTheme.dimens.size20, + height = TangemTheme.dimens.size18, ), ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt index 9256ad0ccc..f31203179d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -158,6 +159,18 @@ private fun Icon(state: TransactionState, modifier: Modifier = Modifier) { is TransactionState.Loading -> { CircleShimmer(modifier = modifier.size(TangemTheme.dimens.size40)) } + is TransactionState.Locked -> { + Box(modifier = modifier.size(TangemTheme.dimens.size40)) { + Box( + modifier = Modifier + .matchParentSize() + .background( + color = TangemTheme.colors.button.secondary, + shape = CircleShape, + ), + ) + } + } } } @@ -206,6 +219,11 @@ private fun Title(state: TransactionState, modifier: Modifier = Modifier) { modifier = modifier.size(width = TangemTheme.dimens.size70, height = TangemTheme.dimens.size12), ) } + is TransactionState.Locked -> { + LockedContent( + modifier = modifier.size(width = TangemTheme.dimens.size70, height = TangemTheme.dimens.size12), + ) + } } } @@ -247,6 +265,11 @@ private fun Subtitle(state: TransactionState, modifier: Modifier = Modifier) { modifier = modifier.size(width = TangemTheme.dimens.size52, height = TangemTheme.dimens.size12), ) } + is TransactionState.Locked -> { + LockedContent( + modifier = modifier.size(width = TangemTheme.dimens.size52, height = TangemTheme.dimens.size12), + ) + } } } @@ -267,6 +290,11 @@ private fun Amount(state: TransactionState, modifier: Modifier = Modifier) { modifier = modifier.size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12), ) } + is TransactionState.Locked -> { + LockedContent( + modifier = modifier.size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12), + ) + } } } @@ -287,9 +315,24 @@ private fun Timestamp(state: TransactionState, modifier: Modifier = Modifier) { modifier = modifier.size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12), ) } + is TransactionState.Locked -> { + LockedContent( + modifier = modifier.size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12), + ) + } } } +@Composable +private fun LockedContent(modifier: Modifier = Modifier) { + Box( + modifier = modifier.background( + color = TangemTheme.colors.field.primary, + shape = RoundedCornerShape(TangemTheme.dimens.radius6), + ), + ) +} + @Preview @Composable private fun Preview_TransactionItem_LightTheme( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionState.kt index 8c5f73c545..05e969d505 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionState.kt @@ -200,4 +200,11 @@ sealed interface TransactionState { * @property txHash transaction hash */ data class Loading(override val txHash: String) : TransactionState + + /** + * Locked state + * + * @property txHash transaction hash + */ + data class Locked(override val txHash: String) : TransactionState } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/wallet/WalletLockedContentState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/wallet/WalletLockedContentState.kt deleted file mode 100644 index e1c2049da6..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/components/wallet/WalletLockedContentState.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.tangem.core.ui.components.wallet - -/** - * Wallet locked content state. - * It allows to divide the locked content of multi-currency and single-currency wallets. - */ -interface WalletLockedContentState \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt index 677001b7e5..b3cdc8cda7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -60,6 +61,7 @@ internal fun TokenItem(state: TokenItemState, modifier: Modifier = Modifier) { is TokenItemState.Loading -> LoadingTokenItem(modifier) is TokenItemState.Draggable -> DraggableTokenItem(state, modifier, reorderableTokenListState = null) is TokenItemState.Unreachable -> UnreachableTokenItem(state, modifier) + is TokenItemState.Locked -> LockedTokenItem(modifier) } } @@ -196,6 +198,77 @@ private fun LoadingTokenItem(modifier: Modifier = Modifier) { } } +@Composable +private fun LockedTokenItem(modifier: Modifier = Modifier) { + BaseSurface(modifier) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = TangemTheme.dimens.spacing12, + vertical = TangemTheme.dimens.spacing4, + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24), + ) { + Box(modifier = Modifier.size(size = TangemTheme.dimens.size42)) { + Box( + modifier = Modifier + .matchParentSize() + .background( + color = TangemTheme.colors.button.secondary, + shape = CircleShape, + ), + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { + LockedContent( + modifier = Modifier.size( + width = TangemTheme.dimens.size72, + height = TangemTheme.dimens.size12, + ), + ) + LockedContent( + modifier = Modifier.size( + width = TangemTheme.dimens.size50, + height = TangemTheme.dimens.size12, + ), + ) + } + Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { + LockedContent( + modifier = Modifier.size( + width = TangemTheme.dimens.size40, + height = TangemTheme.dimens.size12, + ), + ) + LockedContent( + modifier = Modifier.size( + width = TangemTheme.dimens.size40, + height = TangemTheme.dimens.size12, + ), + ) + } + } + } + } +} + +@Composable +private fun LockedContent(modifier: Modifier = Modifier) { + Box( + modifier = modifier.background( + color = TangemTheme.colors.field.primary, + shape = RoundedCornerShape(TangemTheme.dimens.radius6), + ), + ) +} + /** * Block for end part of token item * shows status is reachable, is drag, hidden or show balance 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 ade5bc15b1..90645c7c40 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 @@ -14,6 +14,8 @@ internal sealed interface TokenItemState { /** Loading token state */ data class Loading(override val id: String) : TokenItemState + data class Locked(override val id: String) : TokenItemState + /** * Content token state * 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 dc61e70f2c..4b03d163f5 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 @@ -5,6 +5,7 @@ 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.domain.wallets.models.UserWallet import com.tangem.feature.wallet.impl.R import java.math.BigDecimal @@ -20,50 +21,47 @@ internal object WalletAdditionalInfoFactory { /** * Get additional info * - * @param cardTypesResolver card type resolver - * @param isLocked check if wallet is locked + * @param cardTypesResolver card type resolver + * @param wallet current wallet * @param currencyAmount amount of currency */ fun resolve( cardTypesResolver: CardTypesResolver, - isLocked: Boolean, + wallet: UserWallet, currencyAmount: BigDecimal? = null, ): TextReference { return if (cardTypesResolver.isMultiwalletAllowed()) { - resolveMultiCurrencyInfo(cardTypesResolver, isLocked) + resolveMultiCurrencyInfo(cardTypesResolver, wallet) } else { - resolveSingleCurrencyInfo(cardTypesResolver, isLocked, currencyAmount) + resolveSingleCurrencyInfo(cardTypesResolver, wallet, currencyAmount) } } - private fun resolveMultiCurrencyInfo(cardTypeResolver: CardTypesResolver, isLocked: Boolean): TextReference { - val backupCardsCount = cardTypeResolver.getBackupCardsCount() + private fun resolveMultiCurrencyInfo(cardTypeResolver: CardTypesResolver, wallet: UserWallet): TextReference { + val backupCardsCount = wallet.cardsInWallet.size + 1 val backupInfoRes = TextReference.PluralRes( id = R.plurals.card_label_card_count, count = backupCardsCount, formatArgs = wrappedList(backupCardsCount), ) - return when { - cardTypeResolver.isWallet2() && !isLocked -> { + return if (wallet.isLocked) { + backupInfoRes + DIVIDER_RES + TextReference.Res(R.string.common_locked) + } else { + if (cardTypeResolver.isWallet2()) { backupInfoRes + DIVIDER_RES + TextReference.Res(id = R.string.common_seed_phrase) - } - cardTypeResolver.isTangemWallet() && !isLocked -> { + } else { backupInfoRes } - isLocked -> { - backupInfoRes + TextReference.Res(R.string.common_locked) - } - else -> error("It isn't exist additional info for this case") // FIXME: Crashes on dev cards } } private fun resolveSingleCurrencyInfo( cardTypeResolver: CardTypesResolver, - isLocked: Boolean, + wallet: UserWallet, currencyAmount: BigDecimal?, ): TextReference { - return if (isLocked) { + return if (wallet.isLocked) { TextReference.Res(R.string.common_locked) } else { val blockchain = cardTypeResolver.getBlockchain() 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 2d4bc11664..6991db467a 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 @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state import androidx.compose.runtime.Immutable import androidx.paging.PagingData 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.feature.wallet.presentation.wallet.state.components.* import kotlinx.collections.immutable.ImmutableList @@ -63,7 +64,20 @@ internal sealed class WalletSingleCurrencyState : WalletState.ContentState() { ) override val txHistoryState: TxHistoryState = TxHistoryState.Content( - contentItems = MutableStateFlow(PagingData.empty()), + contentItems = MutableStateFlow( + value = PagingData.from( + data = listOf( + TxHistoryState.TxHistoryItemState.Title(onExploreClick = onExploreClick), + TxHistoryState.TxHistoryItemState.Transaction( + state = TransactionState.Locked(txHash = LOCKED_TX_HASH), + ), + ), + ), + ), ) + + private companion object { + const val LOCKED_TX_HASH = "LOCKED_TX_HASH" + } } } \ No newline at end of file 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 01134378a5..4ac301eb5c 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 @@ -15,6 +15,9 @@ internal sealed interface WalletCardState { /** Title */ val title: String + /** Additional text */ + val additionalInfo: TextReference? + /** Wallet image resource id */ @get:DrawableRes val imageResId: Int? @@ -22,81 +25,92 @@ 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 imageResId: Int?, - override val onClick: (() -> Unit)? = null, override val additionalInfo: TextReference, - val balance: String, - ) : WalletCardState, AdditionalTextAvailability - - /** - * Wallet card loading state - * - * @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 imageResId: Int?, override val onClick: (() -> Unit)? = null, + val balance: String, ) : WalletCardState /** * Wallet card hidden content state * - * @property id wallet id - * @property title wallet name - * @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 additionalInfo wallet additional info + * @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: TextReference = HIDDEN_BALANCE_TEXT, override val imageResId: Int?, override val onClick: (() -> Unit)?, - ) : WalletCardState, AdditionalTextAvailability { + ) : WalletCardState - override val additionalInfo: TextReference = HIDDEN_BALANCE_TEXT - } + /** + * Wallet card locked 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 + */ + data class LockedContent( + override val id: UserWalletId, + override val title: String, + override val additionalInfo: TextReference? = null, + override val imageResId: Int?, + override val onClick: (() -> Unit)?, + ) : WalletCardState /** * 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: TextReference = EMPTY_BALANCE_TEXT, override val imageResId: Int?, override val onClick: (() -> Unit)?, - override val additionalInfo: TextReference = EMPTY_BALANCE_TEXT, - ) : WalletCardState, AdditionalTextAvailability + ) : WalletCardState + + /** + * 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 + */ + data class Loading( + override val id: UserWalletId, + override val title: String, + override val additionalInfo: TextReference? = null, + override val imageResId: Int?, + override val onClick: (() -> Unit)? = null, + ) : WalletCardState companion object { val HIDDEN_BALANCE_TEXT by lazy { TextReference.Str(value = "•••") } 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 4df7b1d04d..8b973054f2 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 @@ -1,6 +1,5 @@ package com.tangem.feature.wallet.presentation.wallet.state.components -import com.tangem.core.ui.components.wallet.WalletLockedContentState import com.tangem.core.ui.extensions.TextReference import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.state.TokenItemState @@ -52,15 +51,13 @@ internal sealed class WalletTokensListState { ) : ContentState(items, onOrganizeTokensClick) /** Locked content state */ - object Locked : - ContentState( - items = persistentListOf( - TokensListItemState.NetworkGroupTitle(value = TextReference.Res(id = R.string.main_tokens)), - TokensListItemState.Token(state = TokenItemState.Loading(id = LOCKED_TOKEN_ID)), - ), - onOrganizeTokensClick = null, + object Locked : ContentState( + items = persistentListOf( + TokensListItemState.NetworkGroupTitle(value = TextReference.Res(id = R.string.main_tokens)), + TokensListItemState.Token(state = TokenItemState.Locked(id = LOCKED_TOKEN_ID)), ), - WalletLockedContentState + onOrganizeTokensClick = null, + ) /** Tokens list item state */ sealed interface TokensListItemState { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt index de50af78d2..1f72ea4c23 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt @@ -6,6 +6,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.wallets.models.UserWallet 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.factory.WalletLoadedTokensListConverter.LoadedTokensListModel @@ -19,7 +20,7 @@ import com.tangem.utils.converter.Converter * * @property currentStateProvider current ui state provider * @param cardTypeResolverProvider card type resolver - * @param isLockedWalletProvider current wallet is locked or not provider + * @param currentWalletProvider current wallet provider * @param clickIntents screen click intents * [REDACTED_AUTHOR] @@ -28,14 +29,14 @@ internal class WalletLoadedTokensListConverter( private val currentStateProvider: Provider, appCurrencyProvider: Provider, cardTypeResolverProvider: Provider, - isLockedWalletProvider: Provider, + currentWalletProvider: Provider, clickIntents: WalletClickIntents, ) : Converter { private val tokenListStateConverter = TokenListToWalletStateConverter( currentStateProvider = currentStateProvider, cardTypeResolverProvider = cardTypeResolverProvider, - isLockedWalletProvider = isLockedWalletProvider, + currentWalletProvider = currentWalletProvider, appCurrencyProvider = appCurrencyProvider, isWalletContentHidden = false, // TODO: [REDACTED_JIRA] clickIntents = clickIntents, 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 index 715a51e7d4..abf72d36c2 100644 --- 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 @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory import com.tangem.common.Provider import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.domain.common.CardTypesResolver 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 @@ -14,12 +15,14 @@ import com.tangem.feature.wallet.presentation.wallet.state.components.WalletToke 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.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.update internal class WalletRefreshStateConverter( private val currentStateProvider: Provider, + private val currentCardTypeResolverProvider: Provider, private val clickIntents: WalletClickIntents, ) : Converter { @@ -41,7 +44,6 @@ internal class WalletRefreshStateConverter( private fun WalletSingleCurrencyState.Content.getRefreshState(): WalletSingleCurrencyState.Content { return copy( - // TODO: [REDACTED_JIRA] walletsListConfig = getWalletsListConfig(), pullToRefreshConfig = getPullToRefreshConfig(), txHistoryState = getTxHistoryState(), @@ -51,18 +53,22 @@ internal class WalletRefreshStateConverter( private fun WalletState.ContentState.getWalletsListConfig(): WalletsListConfig { val selectedWallet = walletsListConfig.wallets[walletsListConfig.selectedWalletIndex] + val additionalInfo = if (currentCardTypeResolverProvider().isMultiwalletAllowed()) { + selectedWallet.additionalInfo + } else { + null + } return walletsListConfig.copy( - wallets = walletsListConfig.wallets - .toPersistentList() - .set( - index = walletsListConfig.selectedWalletIndex, - element = WalletCardState.Loading( - id = selectedWallet.id, - title = selectedWallet.title, - imageResId = selectedWallet.imageResId, - ), + wallets = walletsListConfig.wallets.toPersistentList().set( + index = walletsListConfig.selectedWalletIndex, + element = WalletCardState.Loading( + id = selectedWallet.id, + title = selectedWallet.title, + additionalInfo = additionalInfo, + imageResId = selectedWallet.imageResId, ), + ), ) } @@ -76,10 +82,7 @@ internal class WalletRefreshStateConverter( WalletTokensListState.Loading( items = tokensListState.items .filterIsInstance() - .map { - TokensListItemState.Token(state = TokenItemState.Loading(id = it.state.id)) - } - .toImmutableList(), + .mapToLoadingTokenState(), ) } is WalletTokensListState.Empty -> WalletTokensListState.Loading() @@ -89,18 +92,19 @@ internal class WalletRefreshStateConverter( } } + private fun List.mapToLoadingTokenState(): ImmutableList { + return this + .map { TokensListItemState.Token(state = TokenItemState.Loading(id = it.state.id)) } + .toImmutableList() + } + private fun WalletSingleCurrencyState.Content.getTxHistoryState(): TxHistoryState { - return when (txHistoryState) { - is TxHistoryState.Content -> { - txHistoryState.contentItems.update { - TxHistoryState.getDefaultLoadingTransactions(onExploreClick = clickIntents::onExploreClick) - } - txHistoryState + if (txHistoryState is TxHistoryState.Content) { + txHistoryState.contentItems.update { + TxHistoryState.getDefaultLoadingTransactions(onExploreClick = clickIntents::onExploreClick) } - is TxHistoryState.Empty, - is TxHistoryState.Error, - is TxHistoryState.NotSupported, - -> txHistoryState } + + return txHistoryState } } \ 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 a1f491073b..821fb20f9b 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 @@ -9,6 +9,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.tokens.error.CurrencyError import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState @@ -23,6 +24,7 @@ internal class WalletSingleCurrencyLoadedBalanceConverter( private val currentStateProvider: Provider, private val cardTypeResolverProvider: Provider, private val appCurrencyProvider: Provider, + private val currentWalletProvider: Provider, ) : Converter { override fun convert(value: SingleCurrencyLoadedBalanceModel): WalletSingleCurrencyState.Content { @@ -89,7 +91,7 @@ internal class WalletSingleCurrencyLoadedBalanceConverter( title = selectedWallet.title, additionalInfo = WalletAdditionalInfoFactory.resolve( cardTypesResolver = cardTypeResolverProvider(), - isLocked = false, + wallet = currentWalletProvider(), currencyAmount = status.amount, ), imageResId = selectedWallet.imageResId, 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 597647666c..39f68f74c4 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,6 +5,7 @@ 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 @@ -110,6 +111,11 @@ internal class WalletSkeletonStateConverter( return WalletCardState.Loading( id = wallet.walletId, title = wallet.name, + additionalInfo = if (cardTypeResolver.isMultiwalletAllowed()) { + WalletAdditionalInfoFactory.resolve(cardTypesResolver = cardTypeResolver, wallet = wallet) + } else { + null + }, 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 5b53bed190..c7d6ed7cd6 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 @@ -13,10 +13,12 @@ import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory 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.WalletBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory.WalletLoadedTxHistoryConverter @@ -24,6 +26,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory.Wal import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow /** @@ -31,13 +34,13 @@ import kotlinx.coroutines.flow.Flow * * @property currentStateProvider current ui state provider * @property currentCardTypeResolverProvider current card type resolver - * @property isLockedWalletProvider current wallet is locked or not + * @property currentWalletProvider current wallet * @property clickIntents screen click intents */ internal class WalletStateFactory( private val currentStateProvider: Provider, private val currentCardTypeResolverProvider: Provider, - private val isLockedWalletProvider: Provider, + private val currentWalletProvider: Provider, private val appCurrencyProvider: Provider, private val clickIntents: WalletClickIntents, ) { @@ -48,7 +51,7 @@ internal class WalletStateFactory( WalletLoadedTokensListConverter( currentStateProvider = currentStateProvider, cardTypeResolverProvider = currentCardTypeResolverProvider, - isLockedWalletProvider = isLockedWalletProvider, + currentWalletProvider = currentWalletProvider, appCurrencyProvider = appCurrencyProvider, clickIntents = clickIntents, ) @@ -74,11 +77,16 @@ internal class WalletStateFactory( currentStateProvider = currentStateProvider, cardTypeResolverProvider = currentCardTypeResolverProvider, appCurrencyProvider = appCurrencyProvider, + currentWalletProvider = currentWalletProvider, ) } private val refreshStateConverter by lazy { - WalletRefreshStateConverter(currentStateProvider = currentStateProvider, clickIntents = clickIntents) + WalletRefreshStateConverter( + currentStateProvider = currentStateProvider, + currentCardTypeResolverProvider = currentCardTypeResolverProvider, + clickIntents = clickIntents, + ) } fun getInitialState(): WalletState = WalletState.Initial(onBackClick = clickIntents::onBackClick) @@ -170,7 +178,22 @@ internal class WalletStateFactory( topBarConfig = state.topBarConfig.copy( onMoreClick = clickIntents::onUnlockWalletNotificationClick, ), - walletsListConfig = state.walletsListConfig, + walletsListConfig = state.walletsListConfig.copy( + wallets = state.walletsListConfig.wallets + .map { walletCardState -> + WalletCardState.LockedContent( + id = walletCardState.id, + title = walletCardState.title, + imageResId = walletCardState.imageResId, + onClick = walletCardState.onClick, + additionalInfo = WalletAdditionalInfoFactory.resolve( + cardTypesResolver = cardTypeResolver, + wallet = currentWalletProvider(), + ), + ) + } + .toImmutableList(), + ), pullToRefreshConfig = state.pullToRefreshConfig, onUnlockWalletsNotificationClick = clickIntents::onUnlockWalletNotificationClick, onUnlockClick = clickIntents::onUnlockWalletClick, @@ -182,7 +205,18 @@ internal class WalletStateFactory( topBarConfig = state.topBarConfig.copy( onMoreClick = clickIntents::onUnlockWalletNotificationClick, ), - walletsListConfig = state.walletsListConfig, + walletsListConfig = state.walletsListConfig.copy( + wallets = state.walletsListConfig.wallets + .map { walletCardState -> + WalletCardState.LockedContent( + id = walletCardState.id, + title = walletCardState.title, + imageResId = walletCardState.imageResId, + onClick = walletCardState.onClick, + ) + } + .toImmutableList(), + ), pullToRefreshConfig = state.pullToRefreshConfig, buttons = getButtons(), onUnlockWalletsNotificationClick = clickIntents::onUnlockWalletNotificationClick, 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 ddb1a4ab1c..b8b5ab1a7d 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 @@ -23,26 +23,32 @@ import kotlinx.coroutines.flow.update internal class WalletLoadingTxHistoryConverter( private val currentStateProvider: Provider, private val clickIntents: WalletClickIntents, -) : Converter, WalletSingleCurrencyState.Content> { +) : Converter, WalletState> { - override fun convert(value: Either): WalletSingleCurrencyState.Content { + override fun convert(value: Either): WalletState { return value.fold(ifLeft = ::convertError, ifRight = ::convert) } - private fun convertError(error: TxHistoryStateError): WalletSingleCurrencyState.Content { - return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content).copy( - txHistoryState = when (error) { - is TxHistoryStateError.EmptyTxHistories -> { - Empty(onBuyClick = clickIntents::onBuyClick) - } - is TxHistoryStateError.DataError -> { - Error(onReloadClick = clickIntents::onReloadClick) - } - is TxHistoryStateError.TxHistoryNotImplemented -> { - NotSupported(onExploreClick = clickIntents::onExploreClick) - } - }, - ) + private fun convertError(error: TxHistoryStateError): WalletState { + val state = currentStateProvider() + + return if (state is WalletSingleCurrencyState.Content) { + state.copy( + txHistoryState = when (error) { + is TxHistoryStateError.EmptyTxHistories -> { + Empty(onBuyClick = clickIntents::onBuyClick) + } + is TxHistoryStateError.DataError -> { + Error(onReloadClick = clickIntents::onReloadClick) + } + is TxHistoryStateError.TxHistoryNotImplemented -> { + NotSupported(onExploreClick = clickIntents::onExploreClick) + } + }, + ) + } else { + state + } } private fun convert(value: Int): WalletSingleCurrencyState.Content { 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 57d1ca7765..3d0facde96 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 @@ -5,7 +5,9 @@ import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -23,7 +25,9 @@ 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.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemDimens import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData @@ -118,20 +122,12 @@ private fun Title(state: WalletCardState, modifier: Modifier = Modifier) { 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), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - ) - } - is WalletCardState.Content, - is WalletCardState.Error, - is WalletCardState.Loading, - -> Unit - } + Icon( + modifier = Modifier.size(size = TangemTheme.dimens.size20), + painter = painterResource(id = R.drawable.ic_eye_off_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) } } } @@ -164,60 +160,78 @@ private fun Balance(state: WalletCardState, modifier: Modifier = Modifier) { style = TangemTheme.typography.h2, ) } + is WalletCardState.HiddenContent -> NonContentBalanceText(text = WalletCardState.HIDDEN_BALANCE_TEXT) + is WalletCardState.Error -> NonContentBalanceText(text = WalletCardState.EMPTY_BALANCE_TEXT) is WalletCardState.Loading -> { - RectangleShimmer( - modifier = Modifier.size( - width = TangemTheme.dimens.size102, - height = TangemTheme.dimens.size32, - ), - ) + RectangleShimmer(modifier = Modifier.nonContentBalanceSize(TangemTheme.dimens)) } - is WalletCardState.HiddenContent -> { - Text( - 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, - ) + is WalletCardState.LockedContent -> { + LockedContent(modifier = Modifier.nonContentBalanceSize(TangemTheme.dimens)) } } } } +@Composable +private fun NonContentBalanceText(text: TextReference) { + Text( + text = text.resolveReference(), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.h2, + ) +} + +private fun Modifier.nonContentBalanceSize(dimens: TangemDimens): Modifier { + return size(width = dimens.size102, height = dimens.size32) +} + @OptIn(ExperimentalAnimationApi::class) @Composable private fun AdditionalInfo(state: WalletCardState, modifier: Modifier = Modifier) { AnimatedContent( - targetState = state, + targetState = state.additionalInfo, 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.size84, - height = TangemTheme.dimens.size16, - ), - ) + ) { additionalInfo -> + if (additionalInfo != null) { + AdditionalInfoText(text = additionalInfo) + } else { + when (state) { + is WalletCardState.Loading -> { + RectangleShimmer(modifier = Modifier.nonContentAdditionalInfoSize(TangemTheme.dimens)) + } + is WalletCardState.LockedContent -> { + LockedContent(modifier = Modifier.nonContentAdditionalInfoSize(TangemTheme.dimens)) + } + else -> Unit } } } } +@Composable +private fun AdditionalInfoText(text: TextReference) { + Text( + text = text.resolveReference(), + color = TangemTheme.colors.text.disabled, + style = TangemTheme.typography.caption, + ) +} + +private fun Modifier.nonContentAdditionalInfoSize(dimens: TangemDimens): Modifier { + return size(width = dimens.size84, height = dimens.size16) +} + +@Composable +private fun LockedContent(modifier: Modifier = Modifier) { + Box( + modifier = modifier.background( + color = TangemTheme.colors.field.primary, + shape = RoundedCornerShape(TangemTheme.dimens.radius6), + ), + ) +} + @Composable private fun Image(@DrawableRes id: Int?, modifier: Modifier = Modifier) { AnimatedVisibility(visible = id != null, modifier = modifier) { 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 52542675be..621b82f941 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 @@ -5,6 +5,7 @@ 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.FiatBalance +import com.tangem.domain.wallets.models.UserWallet 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 @@ -13,7 +14,7 @@ internal class FiatBalanceToWalletCardConverter( private val currentState: WalletCardState, private val cardTypeResolverProvider: Provider, private val appCurrencyProvider: Provider, - private val isLockedState: Boolean, + private val currentWalletProvider: Provider, private val isWalletContentHidden: Boolean, ) : Converter { @@ -26,7 +27,7 @@ internal class FiatBalanceToWalletCardConverter( } private fun WalletCardState.toLoadingWalletCardState(): WalletCardState { - return WalletCardState.Loading(id, title, imageResId, onClick) + return WalletCardState.Loading(id, title, additionalInfo, imageResId, onClick) } private fun WalletCardState.toErrorWalletCardState(): WalletCardState { @@ -37,7 +38,7 @@ internal class FiatBalanceToWalletCardConverter( onClick = onClick, additionalInfo = WalletAdditionalInfoFactory.resolve( cardTypesResolver = cardTypeResolverProvider(), - isLocked = isLockedState, + wallet = currentWalletProvider(), ), ) } @@ -47,6 +48,7 @@ internal class FiatBalanceToWalletCardConverter( WalletCardState.HiddenContent( id = currentState.id, title = currentState.title, + additionalInfo = currentState.additionalInfo ?: WalletCardState.HIDDEN_BALANCE_TEXT, imageResId = currentState.imageResId, onClick = currentState.onClick, ) @@ -58,7 +60,7 @@ internal class FiatBalanceToWalletCardConverter( title = currentState.title, additionalInfo = WalletAdditionalInfoFactory.resolve( cardTypesResolver = cardTypeResolverProvider(), - isLocked = isLockedState, + wallet = currentWalletProvider(), ), imageResId = currentState.imageResId, onClick = currentState.onClick, 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 7d29a08e9e..35aa99598f 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,6 +4,7 @@ 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.domain.wallets.models.UserWallet 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.WalletsListConfig @@ -16,7 +17,7 @@ import kotlinx.collections.immutable.toPersistentList internal class TokenListToWalletStateConverter( private val currentStateProvider: Provider, private val cardTypeResolverProvider: Provider, - private val isLockedWalletProvider: Provider, + private val currentWalletProvider: Provider, private val appCurrencyProvider: Provider, private val isWalletContentHidden: Boolean, clickIntents: WalletClickIntents, @@ -46,7 +47,7 @@ internal class TokenListToWalletStateConverter( val selectedWalletCard = walletsListConfig.wallets[selectedWalletIndex] val converter = FiatBalanceToWalletCardConverter( currentState = selectedWalletCard, - isLockedState = isLockedWalletProvider(), + currentWalletProvider = currentWalletProvider, cardTypeResolverProvider = cardTypeResolverProvider, appCurrencyProvider = appCurrencyProvider, isWalletContentHidden = isWalletContentHidden, 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 5deb677137..44ed05d722 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 @@ -96,8 +96,8 @@ internal class WalletViewModel @Inject constructor( index = requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex, ) }, - isLockedWalletProvider = Provider { - wallets[requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex].isLocked + currentWalletProvider = Provider { + wallets[requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex] }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), clickIntents = this, From 09cb780a4f6ca21e3f108d273ed32155b51fff80 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 22 Aug 2023 20:41:14 +0800 Subject: [PATCH 17/44] Updated on 2026-08-14 --- .../tap/di/domain/WalletsDomainModule.kt | 12 +++++++ core/res/src/main/res/values-ru/strings.xml | 5 +++ .../src/main/res/values-zh-rTW/strings.xml | 2 ++ core/res/src/main/res/values/strings.xml | 5 +++ .../wallets/models/DeleteWalletError.kt | 6 ++++ .../wallets/models/UpdateWalletError.kt | 6 ++++ .../wallets/usecase/DeleteWalletUseCase.kt | 31 ++++++++++++++++ .../wallets/usecase/UpdateWalletUseCase.kt | 35 +++++++++++++++++++ .../wallet/viewmodels/WalletClickIntents.kt | 6 ++++ .../wallet/viewmodels/WalletViewModel.kt | 14 ++++++++ 10 files changed, 122 insertions(+) create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/models/DeleteWalletError.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/models/UpdateWalletError.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index 92f1340058..7eb7a3d2cc 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -48,4 +48,16 @@ internal object WalletsDomainModule { fun providesSelectWalletUseCase(walletsStateHolder: WalletsStateHolder): SelectWalletUseCase { return SelectWalletUseCase(walletsStateHolder = walletsStateHolder) } + + @Provides + @ViewModelScoped + fun providesUpdateWalletUseCase(walletsStateHolder: WalletsStateHolder): UpdateWalletUseCase { + return UpdateWalletUseCase(walletsStateHolder = walletsStateHolder) + } + + @Provides + @ViewModelScoped + fun providesDeleteWalletUseCase(walletsStateHolder: WalletsStateHolder): DeleteWalletUseCase { + return DeleteWalletUseCase(walletsStateHolder = walletsStateHolder) + } } \ No newline at end of file diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 843b3f4be1..820055db8d 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -96,6 +96,8 @@ Получить Отклонить Перезагрузить + Переименовать + Сбросить Сохранить изменения Искать Поиск токенов @@ -116,6 +118,7 @@ Транзакции Перевод Я понял + Необходима разблокировка Недоступно Да Адрес контракта скопирован! @@ -472,6 +475,8 @@ Tangem Twin Это действие необратимо. У вас не будет доступа к старому кошельку. Приложите twin-карту с номером %s и не убирайте до окончания операции + Используйте %s или отсканируйте карту, чтобы получить доступ к своему кошельку + Используйте %s или отсканируйте карту Добавить новый кошелек Вы уверены, что хотите удалить этот кошелек? %d выбрано diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index 748251aefa..15209b8a9b 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -80,6 +80,8 @@ OK 主卡片 拒絕 + 重新命名 + 重置 保存設置 搜索 搜尋代幣 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 9d7e67dfea..755f6928a3 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -95,6 +95,8 @@ Receive Reject Reload + Rename + Reset Save changes Search Search tokens @@ -115,6 +117,7 @@ Transactions Transfer I understand + Unlock needed Unreachable Yes Contract address copied! @@ -463,6 +466,8 @@ Tangem Twin This action is irreversible. You will not have access to the old wallet. Tap the twin card with number %s and do not remove until the end of the operation + Use %s or scan a card to have an access to your wallet + Use %s or scan a card Add new wallet Are you sure you want to delete this wallet? %d selected diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/DeleteWalletError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/DeleteWalletError.kt new file mode 100644 index 0000000000..6ed1ab11b9 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/DeleteWalletError.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.wallets.models + +sealed interface DeleteWalletError { + + object DataError : DeleteWalletError +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/UpdateWalletError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/UpdateWalletError.kt new file mode 100644 index 0000000000..48a93dd33b --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/UpdateWalletError.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.wallets.models + +sealed interface UpdateWalletError { + + object DataError : UpdateWalletError +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt new file mode 100644 index 0000000000..e4102d98c2 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt @@ -0,0 +1,31 @@ +package com.tangem.domain.wallets.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.common.doOnFailure +import com.tangem.common.doOnSuccess +import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.models.DeleteWalletError +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Use case for updating user wallet + * + * @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager' + * +[REDACTED_AUTHOR] + */ +class DeleteWalletUseCase(private val walletsStateHolder: WalletsStateHolder) { + + suspend operator fun invoke(userWalletId: UserWalletId): Either { + val userWalletsListManager = walletsStateHolder.userWalletsListManager + ?: return DeleteWalletError.DataError.left() + + userWalletsListManager.delete(userWalletIds = listOf(userWalletId)) + .doOnSuccess { return Unit.right() } + .doOnFailure { return DeleteWalletError.DataError.left() } + + return Unit.right() + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt new file mode 100644 index 0000000000..1990c26dda --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt @@ -0,0 +1,35 @@ +package com.tangem.domain.wallets.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.common.doOnFailure +import com.tangem.common.doOnSuccess +import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.models.UpdateWalletError +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Use case for updating user wallet + * + * @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager' + * +[REDACTED_AUTHOR] + */ +class UpdateWalletUseCase(private val walletsStateHolder: WalletsStateHolder) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + update: suspend (UserWallet) -> UserWallet, + ): Either { + val userWalletsListManager = walletsStateHolder.userWalletsListManager + ?: return UpdateWalletError.DataError.left() + + userWalletsListManager.update(userWalletId, update) + .doOnSuccess { return Unit.right() } + .doOnFailure { return UpdateWalletError.DataError.left() } + + return Unit.right() + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt index 48d9423033..ccb9eeeb22 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt @@ -2,7 +2,9 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels import com.tangem.core.ui.components.transactions.intents.TxHistoryClickIntents import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +@Suppress("TooManyFunctions") internal interface WalletClickIntents : TxHistoryClickIntents { fun onBackClick() @@ -36,4 +38,8 @@ internal interface WalletClickIntents : TxHistoryClickIntents { fun onBottomSheetDismiss() fun onTokenClick(currency: CryptoCurrency) + + fun onRenameClick(userWalletId: UserWalletId, name: String) + + fun onDeleteClick(userWalletId: UserWalletId) } \ No newline at end of file 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 44ed05d722..af6c22462d 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 @@ -60,6 +60,8 @@ internal class WalletViewModel @Inject constructor( private val saveWalletUseCase: SaveWalletUseCase, private val getSelectedWalletUseCase: GetSelectedWalletUseCase, private val selectWalletUseCase: SelectWalletUseCase, + private val updateWalletUseCase: UpdateWalletUseCase, + private val deleteWalletUseCase: DeleteWalletUseCase, private val getBiometricsStatusUseCase: GetBiometricsStatusUseCase, private val setAccessCodeRequestPolicyUseCase: SetAccessCodeRequestPolicyUseCase, private val getAccessCodeSavingStatusUseCase: GetAccessCodeSavingStatusUseCase, @@ -459,6 +461,18 @@ internal class WalletViewModel @Inject constructor( router.openTokenDetails(currency = currency) } + override fun onRenameClick(userWalletId: UserWalletId, name: String) { + viewModelScope.launch(dispatchers.io) { + updateWalletUseCase(userWalletId = userWalletId, update = { it.copy(name) }) + } + } + + override fun onDeleteClick(userWalletId: UserWalletId) { + viewModelScope.launch(dispatchers.io) { + deleteWalletUseCase(userWalletId) + } + } + private fun createSelectedAppCurrencyFlow(): StateFlow { return getSelectedAppCurrencyUseCase() .map { maybeAppCurrency -> From 5bffc604dbf4d5a5c9695810de0bf98d3eca0d19 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 23 Aug 2023 13:22:33 +0300 Subject: [PATCH 18/44] Updated on 2026-08-14 --- .../domain/common/extensions/WalletManagerFactory.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/WalletManagerFactory.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/WalletManagerFactory.kt index 551f75119b..b46153ff06 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/WalletManagerFactory.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/WalletManagerFactory.kt @@ -15,7 +15,6 @@ import com.tangem.domain.common.configs.Wallet2CardConfig import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse -import com.tangem.blockchain.common.CardanoAddressConfig fun WalletManagerFactory.makeWalletManagerForApp( scanResponse: ScanResponse, @@ -57,7 +56,7 @@ fun WalletManagerFactory.makeWalletManagerForApp( derivationPath = derivationPath ?: return null, derivedWalletKeys = derivedKeys ?: return null, isWallet2 = scanResponse.cardTypesResolver.isWallet2(), - ) + ) ?: return null createWalletManager( blockchain = environmentBlockchain, @@ -80,8 +79,8 @@ private fun makePublicKey( derivationPath: DerivationPath, derivedWalletKeys: Map, isWallet2: Boolean, -): Wallet.PublicKey { - val derivedKey = derivedWalletKeys[derivationPath] ?: error("No derivation found") +): Wallet.PublicKey? { + val derivedKey = derivedWalletKeys[derivationPath] ?: return null val derivationKey = Wallet.HDKey( path = derivationPath, From dd9d7f2e45ae9e2bb59cec3ec4bd6c14d20f92f1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 21 Aug 2023 19:14:02 +0300 Subject: [PATCH 19/44] Updated on 2026-08-14 --- .../tangem/core/ui/components/SettingsRow.kt | 74 ++++++++++++++++++ .../presentation/common/WalletPreviewData.kt | 36 ++++++--- .../common/component/TokenItem.kt | 25 ++++-- .../common/state/TokenItemState.kt | 6 +- .../wallet/state/ActionsBottomSheetConfig.kt | 17 +++++ .../wallet/state/TokenActionButtonConfig.kt | 18 +++++ .../wallet/state/WalletMultiCurrencyState.kt | 1 + .../state/factory/TokenActionsProvider.kt | 52 +++++++++++++ .../factory/WalletSkeletonStateConverter.kt | 1 + .../state/factory/WalletStateFactory.kt | 25 ++++-- .../presentation/wallet/ui/WalletScreen.kt | 16 +++- .../ui/components/TokenActionsBottomSheet.kt | 76 +++++++++++++++++++ ...ryptoCurrencyStatusToTokenItemConverter.kt | 3 +- .../wallet/viewmodels/WalletClickIntents.kt | 8 +- .../wallet/viewmodels/WalletViewModel.kt | 30 ++++++-- 15 files changed, 351 insertions(+), 37 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/SettingsRow.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/ActionsBottomSheetConfig.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/TokenActionButtonConfig.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/SettingsRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/SettingsRow.kt new file mode 100644 index 0000000000..823a93b8e3 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/SettingsRow.kt @@ -0,0 +1,74 @@ +package com.tangem.core.ui.components + +import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import com.tangem.core.ui.res.TangemTheme + +/** + * [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=281-248&mode=design&t=bXqehWPHyATKcZEW-4) + * */ +@Composable +fun SimpleSettingsRow( + title: String, + @DrawableRes icon: Int, + onItemsClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + subtitle: String? = null, +) { + Row( + modifier = modifier + .height(TangemTheme.dimens.size56) + .fillMaxWidth() + .clickable( + onClick = { + if (enabled) { + onItemsClick() + } + }, + ), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + ) { + val textColor: Color by animateColorAsState( + targetValue = if (enabled) { + TangemTheme.colors.text.primary1 + } else { + TangemTheme.colors.text.secondary + }, + ) + Icon( + painter = painterResource(id = icon), + contentDescription = null, + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing20), + tint = textColor, + ) + Column(modifier = Modifier.padding(end = TangemTheme.dimens.spacing20)) { + Text( + text = title, + style = TangemTheme.typography.subtitle1, + color = textColor, + ) + AnimatedVisibility( + visible = !subtitle.isNullOrEmpty(), + ) { + Text( + text = subtitle ?: "", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + } + } + } +} \ 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 aaa70af11f..f076a7996d 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 @@ -18,6 +18,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.* import com.tangem.feature.wallet.presentation.wallet.state.components.* import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.TokensListItemState import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.MutableStateFlow import java.util.UUID @@ -99,7 +100,8 @@ internal object WalletPreviewData { ), ), isTestnet = false, - onClick = {}, + onItemClick = {}, + onItemLongClick = {}, ) } @@ -126,7 +128,8 @@ internal object WalletPreviewData { ), ), isTestnet = false, - onClick = {}, + onItemClick = {}, + onItemLongClick = {}, ) } @@ -258,15 +261,25 @@ internal object WalletPreviewData { ) } - private val manageButtons by lazy { - persistentListOf( - WalletManageButton.Buy(onClick = {}), - WalletManageButton.Send(onClick = {}), - WalletManageButton.Receive(onClick = {}), - WalletManageButton.Exchange(onClick = {}), - WalletManageButton.CopyAddress(onClick = {}), - ) - } + val actionsBottomSheet = ActionsBottomSheetConfig( + isShow = true, + onDismissRequest = {}, + actions = listOf( + TokenActionButtonConfig( + text = "Send", + iconResId = R.drawable.ic_share_24, + onClick = {}, + ), + ).toImmutableList(), + ) + + private val manageButtons = persistentListOf( + WalletManageButton.Buy(onClick = {}), + WalletManageButton.Send(onClick = {}), + WalletManageButton.Receive(onClick = {}), + WalletManageButton.Exchange(onClick = {}), + WalletManageButton.CopyAddress(onClick = {}), + ) val multicurrencyWalletScreenState by lazy { WalletMultiCurrencyState.Content( @@ -336,6 +349,7 @@ internal object WalletPreviewData { WalletNotification.ScanCard(onClick = {}), ), bottomSheetConfig = bottomSheet, + tokenActionsBottomSheet = actionsBottomSheet, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt index b3cdc8cda7..84efe5f86a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -4,8 +4,10 @@ import androidx.annotation.DrawableRes import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape @@ -21,7 +23,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorMatrix +import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -65,11 +69,18 @@ internal fun TokenItem(state: TokenItemState, modifier: Modifier = Modifier) { } } +@OptIn(ExperimentalFoundationApi::class) @Composable private fun ContentTokenItem(content: TokenItemState.Content, modifier: Modifier = Modifier) { + val hapticFeedback = LocalHapticFeedback.current InternalTokenItem( - modifier = modifier, - onClick = content.onClick, + modifier = modifier.combinedClickable( + onClick = content.onItemClick, + onLongClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + content.onItemLongClick() + }, + ), name = content.name, tokenIconUrl = content.tokenIconUrl, tokenIconResId = content.tokenIconResId, @@ -300,15 +311,15 @@ private fun InternalTokenItem( options: @Composable ConstraintLayoutScope.(ref: ConstrainedLayoutReference) -> Unit, modifier: Modifier = Modifier, isTestnet: Boolean = false, - onClick: (() -> Unit)? = null, ) { - BaseSurface( - modifier = modifier, - onClick = onClick, + Box( + modifier = modifier + .defaultMinSize(minHeight = TOKEN_ITEM_HEIGHT) + .background(color = TangemTheme.colors.background.primary), ) { ConstraintLayout( modifier = Modifier - .fillMaxWidth() + .fillMaxSize() .padding( horizontal = TangemTheme.dimens.spacing14, vertical = TangemTheme.dimens.spacing4, 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 90645c7c40..d732e148c0 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 @@ -28,7 +28,8 @@ internal sealed interface TokenItemState { * @property hasPending pending tx in blockchain * @property tokenOptions state for token options * @property isTestnet indicates whether the token is from test network or not - * @property onClick callback which will be called when an item is clicked + * @property onItemClick callback which will be called when an item is clicked + * @property onItemLongClick callback which will be called when an item is long clicked */ data class Content( override val id: String, @@ -40,7 +41,8 @@ internal sealed interface TokenItemState { val hasPending: Boolean, val tokenOptions: TokenOptionsState, val isTestnet: Boolean, - val onClick: () -> Unit, + val onItemClick: () -> Unit, + val onItemLongClick: () -> Unit, ) : TokenItemState /** diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/ActionsBottomSheetConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/ActionsBottomSheetConfig.kt new file mode 100644 index 0000000000..cb7e79cee3 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/ActionsBottomSheetConfig.kt @@ -0,0 +1,17 @@ +package com.tangem.feature.wallet.presentation.wallet.state + +import kotlinx.collections.immutable.ImmutableList + +/** + * Config for the token actions bottom sheet + * + * @property isShow flag that determine if bottom sheet is shown + * @property onDismissRequest lambda be invoked when bottom sheet is dismissed + * @property actions actions + * + */ +internal data class ActionsBottomSheetConfig( + val isShow: Boolean, + val onDismissRequest: () -> Unit, + val actions: ImmutableList, +) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/TokenActionButtonConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/TokenActionButtonConfig.kt new file mode 100644 index 0000000000..934bfd6230 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/TokenActionButtonConfig.kt @@ -0,0 +1,18 @@ +package com.tangem.feature.wallet.presentation.wallet.state + +import androidx.annotation.DrawableRes + +/** + * Action button config + * + * @property text text + * @property iconResId icon resource id + * @property onClick lambda be invoked when action component is clicked + * @property enabled enabled + */ +data class TokenActionButtonConfig( + val text: String, + @DrawableRes val iconResId: Int, + val onClick: () -> Unit, + val enabled: Boolean = true, +) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMultiCurrencyState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMultiCurrencyState.kt index 87d675cd95..def8561d02 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMultiCurrencyState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMultiCurrencyState.kt @@ -22,6 +22,7 @@ internal sealed class WalletMultiCurrencyState : WalletState.ContentState() { override val notifications: ImmutableList, override val bottomSheetConfig: WalletBottomSheetConfig?, override val tokensListState: WalletTokensListState, + val tokenActionsBottomSheet: ActionsBottomSheetConfig?, ) : WalletMultiCurrencyState() data class Locked( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt new file mode 100644 index 0000000000..f9a7dbfc4e --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt @@ -0,0 +1,52 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory + +import com.tangem.common.Provider +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.wallet.state.TokenActionButtonConfig +import com.tangem.feature.wallet.presentation.wallet.state.WalletState +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +/** + * Converter from loaded [TokenItemState.Content] to ImmutableList<[TokenActionButtonConfig]> + * + * @property currentStateProvider current ui state provider + * + */ +@Suppress("UnusedPrivateMember") +internal class TokenActionsProvider( + private val currentStateProvider: Provider, +) { + + @Suppress("UnusedPrivateMember") + fun provideActions(tokenId: String): ImmutableList { + // TODO: [REDACTED_JIRA] + return mockTokenActionButtonConfig().toImmutableList() + } + + private fun mockTokenActionButtonConfig(): List { + return listOf( + TokenActionButtonConfig( + text = "Send", + iconResId = R.drawable.ic_plus_24, + onClick = {}, + ), + TokenActionButtonConfig( + text = "Buy", + iconResId = R.drawable.ic_plus_24, + onClick = {}, + ), + TokenActionButtonConfig( + text = "Sell", + iconResId = R.drawable.ic_plus_24, + onClick = {}, + ), + TokenActionButtonConfig( + text = "Swap", + iconResId = R.drawable.ic_plus_24, + onClick = {}, + ), + ) + } +} \ 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 39f68f74c4..10026ccc62 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 @@ -51,6 +51,7 @@ internal class WalletSkeletonStateConverter( tokensListState = WalletTokensListState.Loading(), notifications = persistentListOf(), bottomSheetConfig = null, + tokenActionsBottomSheet = null, ) } 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 c7d6ed7cd6..200a79c740 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 @@ -14,6 +14,7 @@ import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.state.ActionsBottomSheetConfig 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 @@ -45,6 +46,7 @@ internal class WalletStateFactory( private val clickIntents: WalletClickIntents, ) { + private val tokenActionsProvider by lazy { TokenActionsProvider(currentStateProvider = currentStateProvider) } private val skeletonConverter by lazy { WalletSkeletonStateConverter(currentStateProvider, clickIntents) } private val loadedTokensListConverter by lazy { @@ -119,29 +121,29 @@ internal class WalletStateFactory( fun getStateAfterContentRefreshing(): WalletState = refreshStateConverter.convert(Unit) - fun getStateWithOpenBottomSheet(content: WalletBottomSheetConfig.BottomSheetContentConfig): WalletState { + fun getStateWithOpenWalletBottomSheet(content: WalletBottomSheetConfig.BottomSheetContentConfig): WalletState { return when (val state = currentStateProvider() as WalletState.ContentState) { is WalletMultiCurrencyState.Content -> state.copy( bottomSheetConfig = WalletBottomSheetConfig( isShow = true, - onDismissRequest = clickIntents::onBottomSheetDismiss, + onDismissRequest = clickIntents::onDismissBottomSheet, content = content, ), ) is WalletMultiCurrencyState.Locked -> state.copy( isBottomSheetShow = true, - onBottomSheetDismiss = clickIntents::onBottomSheetDismiss, + onBottomSheetDismiss = clickIntents::onDismissBottomSheet, ) is WalletSingleCurrencyState.Content -> state.copy( bottomSheetConfig = WalletBottomSheetConfig( isShow = true, - onDismissRequest = clickIntents::onBottomSheetDismiss, + onDismissRequest = clickIntents::onDismissBottomSheet, content = content, ), ) is WalletSingleCurrencyState.Locked -> state.copy( isBottomSheetShow = true, - onBottomSheetDismiss = clickIntents::onBottomSheetDismiss, + onBottomSheetDismiss = clickIntents::onDismissBottomSheet, ) } } @@ -159,6 +161,19 @@ internal class WalletStateFactory( } } + fun getStateWithTokenActionBottomSheet(tokenId: String): WalletState { + return when (val state = currentStateProvider() as WalletState.ContentState) { + is WalletMultiCurrencyState.Content -> state.copy( + tokenActionsBottomSheet = ActionsBottomSheetConfig( + isShow = true, + actions = tokenActionsProvider.provideActions(tokenId = tokenId), + onDismissRequest = clickIntents::onDismissActionsBottomSheet, + ), + ) + else -> state + } + } + fun getLoadingTxHistoryState(itemsCountEither: Either): WalletState { return loadingTransactionsStateConverter.convert(value = itemsCountEither) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 8c94b30d15..fa7dc84810 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -23,6 +23,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencySt 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.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.ui.components.TokenActionsBottomSheet import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList import com.tangem.feature.wallet.presentation.wallet.ui.components.common.* import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.organizeButton @@ -122,12 +123,23 @@ private fun WalletContent(state: WalletState.ContentState) { } } - val bottomSheetConfig = state.bottomSheetConfig + WalletBottomSheets(state = state) + + WalletSideEffects(lazyListState = walletsListState, walletsListConfig = state.walletsListConfig) +} + +@Composable +private fun WalletBottomSheets(state: WalletState) { + val bottomSheetConfig = (state as? WalletState.ContentState)?.bottomSheetConfig if (bottomSheetConfig != null && bottomSheetConfig.isShow) { WalletBottomSheet(config = bottomSheetConfig) } - WalletSideEffects(lazyListState = walletsListState, walletsListConfig = state.walletsListConfig) + (state as? WalletMultiCurrencyState.Content)?.let { multiCurrencyState -> + if (multiCurrencyState.tokenActionsBottomSheet != null && multiCurrencyState.tokenActionsBottomSheet.isShow) { + TokenActionsBottomSheet(config = multiCurrencyState.tokenActionsBottomSheet) + } + } } // region Preview diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt new file mode 100644 index 0000000000..a79d41bb23 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt @@ -0,0 +1,76 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.SimpleSettingsRow +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.wallet.presentation.common.WalletPreviewData +import com.tangem.feature.wallet.presentation.wallet.state.ActionsBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.state.TokenActionButtonConfig +import kotlinx.collections.immutable.ImmutableList + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun TokenActionsBottomSheet(config: ActionsBottomSheetConfig) { + ModalBottomSheet( + onDismissRequest = config.onDismissRequest, + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + containerColor = TangemTheme.colors.background.primary, + dragHandle = { BottomSheetDefaults.DragHandle() }, + ) { + ActionsBottomSheetContent(config.actions) + } +} + +@Composable +private fun ActionsBottomSheetContent(actions: ImmutableList) { + Column( + modifier = Modifier.background(TangemTheme.colors.background.primary), + ) { + actions.forEach { action -> + SimpleSettingsRow( + title = action.text, + icon = action.iconResId, + enabled = action.enabled, + onItemsClick = action.onClick, + ) + } + } +} + +@Preview +@Composable +private fun ActionsBottomSheetContent_Light( + @PreviewParameter(ActionsBottomSheetContentConfigProvider::class) + config: ActionsBottomSheetConfig, +) { + TangemTheme(isDark = false) { + // Use preview of content because ModalBottomSheet isn't supported in Preview mode + ActionsBottomSheetContent(actions = config.actions) + } +} + +@Preview +@Composable +private fun ActionsBottomSheetContent_Dark( + @PreviewParameter(ActionsBottomSheetContentConfigProvider::class) + config: ActionsBottomSheetConfig, +) { + TangemTheme(isDark = false) { + // Use preview of content because ModalBottomSheet isn't supported in Preview mode + ActionsBottomSheetContent(actions = config.actions) + } +} + +private class ActionsBottomSheetContentConfigProvider : CollectionPreviewParameterProvider( + collection = listOf(WalletPreviewData.actionsBottomSheet), +) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt index 292c7aca8e..5c860ab3e5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt @@ -51,7 +51,8 @@ internal class CryptoCurrencyStatusToTokenItemConverter( ) }, isTestnet = currency.network.isTestnet, - onClick = { clickIntents.onTokenClick(currency) }, + onItemClick = { clickIntents.onTokenItemClick(currency) }, + onItemLongClick = { clickIntents.onTokenItemLongClick(currency) }, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt index ccb9eeeb22..ad201200ed 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt @@ -35,9 +35,13 @@ internal interface WalletClickIntents : TxHistoryClickIntents { fun onUnlockWalletNotificationClick() - fun onBottomSheetDismiss() + fun onDismissBottomSheet() - fun onTokenClick(currency: CryptoCurrency) + fun onTokenItemClick(currency: CryptoCurrency) + + fun onTokenItemLongClick(currency: CryptoCurrency) + + fun onDismissActionsBottomSheet() fun onRenameClick(userWalletId: UserWalletId, name: String) 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 af6c22462d..551b652d6d 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 @@ -296,7 +296,7 @@ internal class WalletViewModel @Inject constructor( override fun onBackupCardClick() = router.openOnboardingScreen() override fun onCriticalWarningAlreadySignedHashesClick() { - uiState = stateFactory.getStateWithOpenBottomSheet( + uiState = stateFactory.getStateWithOpenWalletBottomSheet( content = WalletBottomSheetConfig.BottomSheetContentConfig.CriticalWarningAlreadySignedHashes( onOkClick = {}, onCancelClick = {}, @@ -309,7 +309,7 @@ internal class WalletViewModel @Inject constructor( } override fun onLikeTangemAppClick() { - uiState = stateFactory.getStateWithOpenBottomSheet( + uiState = stateFactory.getStateWithOpenWalletBottomSheet( content = WalletBottomSheetConfig.BottomSheetContentConfig.LikeTangemApp( onRateTheAppClick = ::onRateTheAppClick, onShareClick = ::onShareClick, @@ -445,7 +445,7 @@ internal class WalletViewModel @Inject constructor( "Impossible to unlock wallet if state isn't WalletLockedState" } - uiState = stateFactory.getStateWithOpenBottomSheet( + uiState = stateFactory.getStateWithOpenWalletBottomSheet( content = when (state) { is WalletMultiCurrencyState.Locked -> state.bottomSheetConfig.content is WalletSingleCurrencyState.Locked -> state.bottomSheetConfig.content @@ -453,12 +453,14 @@ internal class WalletViewModel @Inject constructor( ) } - override fun onBottomSheetDismiss() { - uiState = stateFactory.getStateWithClosedBottomSheet() + override fun onTokenItemClick(currency: CryptoCurrency) { + router.openTokenDetails(currency = currency) } - override fun onTokenClick(currency: CryptoCurrency) { - router.openTokenDetails(currency = currency) + override fun onTokenItemLongClick(currency: CryptoCurrency) { + uiState = stateFactory.getStateWithTokenActionBottomSheet( + tokenId = currency.id.value, + ) } override fun onRenameClick(userWalletId: UserWalletId, name: String) { @@ -484,4 +486,18 @@ internal class WalletViewModel @Inject constructor( initialValue = AppCurrency.Default, ) } + + override fun onDismissBottomSheet() { + uiState = stateFactory.getStateWithClosedBottomSheet() + } + + override fun onDismissActionsBottomSheet() { + (uiState as? WalletMultiCurrencyState.Content)?.let { state -> + uiState = state.copy( + tokenActionsBottomSheet = state.tokenActionsBottomSheet?.copy( + isShow = false, + ), + ) + } + } } \ No newline at end of file From 544c243291b2ff16d5a54da91eeef3803104f7a1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 24 Aug 2023 16:19:54 +0800 Subject: [PATCH 20/44] Updated on 2026-08-14 --- .../ui/WalletSelectorBottomSheetFragment.kt | 24 ++-- .../components/RenameWalletDialogContent.kt | 69 ----------- .../wallets/RenameWalletDialogContent.kt | 59 +++++++++ .../presentation/common/WalletPreviewData.kt | 12 +- .../router/DefaultWalletRouter.kt | 4 + .../presentation/router/InnerWalletRouter.kt | 3 + .../state/components/WalletCardState.kt | 47 +++++-- .../WalletLoadedTokensListConverter.kt | 4 +- .../factory/WalletRefreshStateConverter.kt | 2 + ...letSingleCurrencyLoadedBalanceConverter.kt | 9 +- .../factory/WalletSkeletonStateConverter.kt | 4 +- .../state/factory/WalletStateFactory.kt | 6 +- .../wallet/ui/components/common/WalletCard.kt | 117 ++++++++++++++++-- .../utils/FiatBalanceToWalletCardConverter.kt | 11 +- .../wallet/utils/TokenListErrorConverter.kt | 23 +++- .../wallet/viewmodels/WalletViewModel.kt | 5 +- 16 files changed, 270 insertions(+), 129 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/RenameWalletDialogContent.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/wallets/RenameWalletDialogContent.kt diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt index ecb5291d26..e631d96a83 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt @@ -7,29 +7,19 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.SnackbarHost import androidx.compose.material.SnackbarHostState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.State -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.fragment.app.viewModels import com.tangem.core.analytics.Analytics +import com.tangem.core.ui.components.wallets.RenameWalletDialogContent import com.tangem.core.ui.fragments.ComposeBottomSheetFragment import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.common.analytics.events.MyWallets import com.tangem.tap.features.details.ui.cardsettings.resolveReference -import com.tangem.tap.features.walletSelector.ui.components.BiometricsDisabledWarningContent -import com.tangem.tap.features.walletSelector.ui.components.BiometricsLockoutWarningContent -import com.tangem.tap.features.walletSelector.ui.components.KeyInvalidatedWarningContent -import com.tangem.tap.features.walletSelector.ui.components.RemoveWalletDialogContent -import com.tangem.tap.features.walletSelector.ui.components.RenameWalletDialogContent -import com.tangem.tap.features.walletSelector.ui.components.WalletSelectorScreenContent +import com.tangem.tap.features.walletSelector.ui.components.* import com.tangem.tap.features.walletSelector.ui.model.DialogModel import com.tangem.tap.features.walletSelector.ui.model.WarningModel import dagger.hilt.android.AndroidEntryPoint @@ -90,7 +80,13 @@ internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment RemoveWalletDialogContent(dialog) - is DialogModel.RenameWalletDialog -> RenameWalletDialogContent(dialog) + is DialogModel.RenameWalletDialog -> { + RenameWalletDialogContent( + name = dialog.currentName, + onConfirm = dialog.onConfirm, + onDismiss = dialog.onDismiss, + ) + } is WarningModel.BiometricsLockoutWarning -> BiometricsLockoutWarningContent(dialog) is WarningModel.KeyInvalidatedWarning -> KeyInvalidatedWarningContent(dialog) is WarningModel.BiometricsDisabledWarning -> BiometricsDisabledWarningContent(dialog) diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/RenameWalletDialogContent.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/RenameWalletDialogContent.kt deleted file mode 100644 index 7bc58f301a..0000000000 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/RenameWalletDialogContent.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.tangem.tap.features.walletSelector.ui.components - -import androidx.compose.foundation.layout.Column -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.AdditionalTextInputDialogParams -import com.tangem.core.ui.components.DialogButton -import com.tangem.core.ui.components.TextInputDialog -import com.tangem.core.ui.res.TangemTheme -import com.tangem.tap.features.walletSelector.ui.model.DialogModel -import com.tangem.wallet.R - -@Composable -internal fun RenameWalletDialogContent(dialog: DialogModel.RenameWalletDialog) { - var value by remember { - mutableStateOf(TextFieldValue(text = dialog.currentName)) - } - - TextInputDialog( - fieldValue = value, - confirmButton = DialogButton( - title = stringResource(id = R.string.common_ok), - enabled = value.text.isNotEmpty() && value.text != dialog.currentName, - onClick = { dialog.onConfirm(value.text) }, - ), - onDismissDialog = dialog.onDismiss, - onValueChange = { newValue -> - value = newValue - }, - title = stringResource(R.string.user_wallet_list_rename_popup_title), - dismissButton = DialogButton( - title = stringResource(id = R.string.common_cancel), - onClick = dialog.onDismiss, - ), - textFieldParams = AdditionalTextInputDialogParams( - label = stringResource(R.string.user_wallet_list_rename_popup_placeholder), - ), - ) -} - -// region Preview -@Composable -private fun RenameWalletDialogContentSample(modifier: Modifier = Modifier) { - Column( - modifier = modifier, - ) { - RenameWalletDialogContent(dialog = DialogModel.RenameWalletDialog("", {}, {})) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun RenameWalletDialogContentPreview_Light() { - TangemTheme { - RenameWalletDialogContentSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun RenameWalletDialogContentPreview_Dark() { - TangemTheme(isDark = true) { - RenameWalletDialogContentSample() - } -} -// endregion Preview \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/wallets/RenameWalletDialogContent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/wallets/RenameWalletDialogContent.kt new file mode 100644 index 0000000000..eabd0fbeb8 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/wallets/RenameWalletDialogContent.kt @@ -0,0 +1,59 @@ +package com.tangem.core.ui.components.wallets + +import androidx.compose.runtime.* +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.components.AdditionalTextInputDialogParams +import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.TextInputDialog +import com.tangem.core.ui.res.TangemTheme + +/** + * Rename a wallet dialog + * + * @param name wallet name + * @param onConfirm lambda be invoked when Confirm button is clicked + * @param onDismiss lambda be invoked when dialog is dismissed + */ +@Composable +fun RenameWalletDialogContent(name: String, onConfirm: (newName: String) -> Unit, onDismiss: () -> Unit) { + var value by remember { mutableStateOf(TextFieldValue(text = name)) } + + TextInputDialog( + fieldValue = value, + confirmButton = DialogButton( + title = stringResource(id = R.string.common_ok), + enabled = value.text.isNotEmpty() && value.text != name, + onClick = { onConfirm(value.text) }, + ), + onDismissDialog = onDismiss, + onValueChange = { value = it }, + title = stringResource(R.string.user_wallet_list_rename_popup_title), + dismissButton = DialogButton(title = stringResource(id = R.string.common_cancel), onClick = onDismiss), + textFieldParams = AdditionalTextInputDialogParams( + label = stringResource(R.string.user_wallet_list_rename_popup_placeholder), + ), + ) +} + +// region Preview + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun RenameWalletDialogContentPreview_Light() { + TangemTheme(isDark = false) { + RenameWalletDialogContent(name = "", onConfirm = {}, onDismiss = {}) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun RenameWalletDialogContentPreview_Dark() { + TangemTheme(isDark = true) { + RenameWalletDialogContent(name = "", onConfirm = {}, onDismiss = {}) + } +} + +// endregion Preview \ 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 f076a7996d..91d8ccbd5d 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 @@ -35,7 +35,8 @@ internal object WalletPreviewData { balance = "8923,05 $", additionalInfo = TextReference.Str("3 cards • Seed phrase"), imageResId = R.drawable.ill_businessman_3d, - onClick = null, + onRenameClick = { _, _ -> }, + onDeleteClick = {}, ) } @@ -44,7 +45,8 @@ internal object WalletPreviewData { id = UserWalletId("321"), title = "Wallet 1", imageResId = R.drawable.ill_businessman_3d, - onClick = null, + onRenameClick = { _, _ -> }, + onDeleteClick = {}, ) } @@ -53,7 +55,8 @@ internal object WalletPreviewData { id = UserWalletId("42"), title = "Wallet 1", imageResId = R.drawable.ill_businessman_3d, - onClick = null, + onRenameClick = { _, _ -> }, + onDeleteClick = {}, ) } @@ -62,7 +65,8 @@ internal object WalletPreviewData { id = UserWalletId("24"), title = "Wallet 1", imageResId = R.drawable.ill_businessman_3d, - onClick = null, + onRenameClick = { _, _ -> }, + onDeleteClick = {}, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 84ca2ae72b..1aa417f71f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -113,6 +113,10 @@ internal class DefaultWalletRouter(private val navigationStateHolder: Navigation ) } + override fun openStoriesScreen() { + navigationStateHolder.navigate(action = NavigationAction.NavigateTo(screen = AppScreen.Home)) + } + private companion object { const val BACKSTACK_ENTRY_COUNT_TO_CLOSE_WALLET_SCREEN = 2 } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index 4a0d24bbaa..510e8034ef 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -44,4 +44,7 @@ internal interface InnerWalletRouter : WalletRouter { /** Open token details screen */ fun openTokenDetails(currency: CryptoCurrency) + + /** Open stories screen */ + fun openStoriesScreen() } \ No newline at end of file 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 4ac301eb5c..e25b851c21 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 @@ -22,8 +22,11 @@ internal sealed interface WalletCardState { @get:DrawableRes val imageResId: Int? - /** Lambda be invoked when card is clicked */ - val onClick: (() -> Unit)? + /** Lambda be invoked when Rename button is clicked */ + val onRenameClick: (UserWalletId, String) -> Unit + + /** Lambda be invoked when Delete button is clicked */ + val onDeleteClick: (UserWalletId) -> Unit /** * Wallet card content state @@ -32,7 +35,8 @@ internal sealed interface WalletCardState { * @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 onRenameClick lambda be invoked when Rename button is clicked + * @property onDeleteClick lambda be invoked when Delete button is clicked * @property balance wallet balance */ data class Content( @@ -40,7 +44,8 @@ internal sealed interface WalletCardState { override val title: String, override val additionalInfo: TextReference, override val imageResId: Int?, - override val onClick: (() -> Unit)? = null, + override val onRenameClick: (UserWalletId, String) -> Unit, + override val onDeleteClick: (UserWalletId) -> Unit, val balance: String, ) : WalletCardState @@ -51,14 +56,16 @@ internal sealed interface WalletCardState { * @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 onRenameClick lambda be invoked when Rename button is clicked + * @property onDeleteClick lambda be invoked when Delete button is clicked */ data class HiddenContent( override val id: UserWalletId, override val title: String, override val additionalInfo: TextReference = HIDDEN_BALANCE_TEXT, override val imageResId: Int?, - override val onClick: (() -> Unit)?, + override val onRenameClick: (UserWalletId, String) -> Unit, + override val onDeleteClick: (UserWalletId) -> Unit, ) : WalletCardState /** @@ -68,14 +75,16 @@ internal sealed interface WalletCardState { * @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 onRenameClick lambda be invoked when Rename button is clicked + * @property onDeleteClick lambda be invoked when Delete button is clicked */ data class LockedContent( override val id: UserWalletId, override val title: String, override val additionalInfo: TextReference? = null, override val imageResId: Int?, - override val onClick: (() -> Unit)?, + override val onRenameClick: (UserWalletId, String) -> Unit, + override val onDeleteClick: (UserWalletId) -> Unit, ) : WalletCardState /** @@ -85,14 +94,16 @@ internal sealed interface WalletCardState { * @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 onRenameClick lambda be invoked when Rename button is clicked + * @property onDeleteClick lambda be invoked when Delete button is clicked */ data class Error( override val id: UserWalletId, override val title: String, override val additionalInfo: TextReference = EMPTY_BALANCE_TEXT, override val imageResId: Int?, - override val onClick: (() -> Unit)?, + override val onRenameClick: (UserWalletId, String) -> Unit, + override val onDeleteClick: (UserWalletId) -> Unit, ) : WalletCardState /** @@ -102,16 +113,28 @@ internal sealed interface WalletCardState { * @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 onRenameClick lambda be invoked when Rename button is clicked + * @property onDeleteClick lambda be invoked when Delete button is clicked */ data class Loading( override val id: UserWalletId, override val title: String, override val additionalInfo: TextReference? = null, override val imageResId: Int?, - override val onClick: (() -> Unit)? = null, + override val onRenameClick: (UserWalletId, String) -> Unit, + override val onDeleteClick: (UserWalletId) -> Unit, ) : WalletCardState + fun copySealed(title: String = this.title): WalletCardState { + return when (this) { + is Content -> copy(title = title) + is Error -> copy(title = title) + is HiddenContent -> copy(title = title) + is Loading -> copy(title = title) + is LockedContent -> copy(title = title) + } + } + companion object { val HIDDEN_BALANCE_TEXT by lazy { TextReference.Str(value = "•••") } val EMPTY_BALANCE_TEXT by lazy { TextReference.Str(value = "—") } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt index 1f72ea4c23..a98b75e4f8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt @@ -31,7 +31,7 @@ internal class WalletLoadedTokensListConverter( cardTypeResolverProvider: Provider, currentWalletProvider: Provider, clickIntents: WalletClickIntents, -) : Converter { +) : Converter { private val tokenListStateConverter = TokenListToWalletStateConverter( currentStateProvider = currentStateProvider, @@ -46,7 +46,7 @@ internal class WalletLoadedTokensListConverter( currentStateProvider = currentStateProvider, ) - override fun convert(value: LoadedTokensListModel): WalletMultiCurrencyState.Content { + override fun convert(value: LoadedTokensListModel): WalletState { return value.tokenListEither.fold( ifLeft = tokenListErrorStateConverter::convert, ifRight = { 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 index abf72d36c2..faa6a40114 100644 --- 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 @@ -67,6 +67,8 @@ internal class WalletRefreshStateConverter( title = selectedWallet.title, additionalInfo = additionalInfo, imageResId = selectedWallet.imageResId, + onRenameClick = selectedWallet.onRenameClick, + onDeleteClick = selectedWallet.onDeleteClick, ), ), ) 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 821fb20f9b..6e1cc58e52 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 @@ -95,7 +95,8 @@ internal class WalletSingleCurrencyLoadedBalanceConverter( currencyAmount = status.amount, ), imageResId = selectedWallet.imageResId, - onClick = selectedWallet.onClick, + onRenameClick = selectedWallet.onRenameClick, + onDeleteClick = selectedWallet.onDeleteClick, balance = formatFiatAmount(status = status, appCurrency = appCurrencyProvider()), ) } @@ -104,7 +105,8 @@ internal class WalletSingleCurrencyLoadedBalanceConverter( id = selectedWallet.id, title = selectedWallet.title, imageResId = selectedWallet.imageResId, - onClick = selectedWallet.onClick, + onRenameClick = selectedWallet.onRenameClick, + onDeleteClick = selectedWallet.onDeleteClick, ) } is CryptoCurrencyStatus.MissedDerivation, @@ -116,7 +118,8 @@ internal class WalletSingleCurrencyLoadedBalanceConverter( id = selectedWallet.id, title = selectedWallet.title, imageResId = selectedWallet.imageResId, - onClick = selectedWallet.onClick, + onRenameClick = selectedWallet.onRenameClick, + onDeleteClick = selectedWallet.onDeleteClick, ) } } 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 10026ccc62..a783f78001 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 @@ -97,7 +97,7 @@ internal class WalletSkeletonStateConverter( // If wallet is initialized, return it, otherwise return loading state if (initializedWallet !is WalletCardState.Loading) { - initializedWallet + initializedWallet.copySealed(title = wallet.name) } else { createWalletLoadingState(wallet) } @@ -118,6 +118,8 @@ internal class WalletSkeletonStateConverter( null }, imageResId = WalletImageResolver.resolve(cardTypesResolver = cardTypeResolver), + onRenameClick = clickIntents::onRenameClick, + onDeleteClick = clickIntents::onDeleteClick, ) } 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 200a79c740..52787d6a81 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 @@ -200,11 +200,12 @@ internal class WalletStateFactory( id = walletCardState.id, title = walletCardState.title, imageResId = walletCardState.imageResId, - onClick = walletCardState.onClick, additionalInfo = WalletAdditionalInfoFactory.resolve( cardTypesResolver = cardTypeResolver, wallet = currentWalletProvider(), ), + onRenameClick = walletCardState.onRenameClick, + onDeleteClick = walletCardState.onDeleteClick, ) } .toImmutableList(), @@ -227,7 +228,8 @@ internal class WalletStateFactory( id = walletCardState.id, title = walletCardState.title, imageResId = walletCardState.imageResId, - onClick = walletCardState.onClick, + onRenameClick = walletCardState.onRenameClick, + onDeleteClick = walletCardState.onDeleteClick, ) } .toImmutableList(), 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 3d0facde96..494e36cee2 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 @@ -1,23 +1,38 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common import androidx.annotation.DrawableRes +import androidx.annotation.StringRes import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.background +import androidx.compose.foundation.* +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Edit +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.ConstraintLayoutScope @@ -25,8 +40,10 @@ 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.components.wallets.RenameWalletDialogContent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemDimens import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.impl.R @@ -44,7 +61,12 @@ import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCard @Composable internal fun WalletCard(state: WalletCardState, modifier: Modifier = Modifier) { @Suppress("DestructuringDeclarationWithTooManyEntries") - CardContainer(onClick = state.onClick, modifier = modifier) { + CardContainer( + name = state.title, + onDeleteClick = { state.onDeleteClick(state.id) }, + onRenameClick = { state.onRenameClick(state.id, it) }, + modifier = modifier, + ) { val (title, balance, additionalText, image) = createRefs() val contentVerticalMargin = TangemTheme.dimens.spacing12 @@ -92,16 +114,43 @@ internal fun WalletCard(state: WalletCardState, modifier: Modifier = Modifier) { @Composable private fun CardContainer( - onClick: (() -> Unit)?, + name: String, + onDeleteClick: () -> Unit, + onRenameClick: (String) -> Unit, modifier: Modifier = Modifier, - content: @Composable ConstraintLayoutScope.() -> Unit, + content: @Composable (ConstraintLayoutScope.() -> Unit), ) { + var isMenuVisible by rememberSaveable { mutableStateOf(value = false) } + var pressOffset by remember { mutableStateOf(value = DpOffset.Zero) } + var itemHeight by remember { mutableStateOf(value = 0.dp) } + + val density = LocalDensity.current + val interactionSource = remember { MutableInteractionSource() } + val haptic = LocalHapticFeedback.current + Surface( - modifier = modifier.defaultMinSize(minHeight = TangemTheme.dimens.size108), + modifier = modifier + .defaultMinSize(minHeight = TangemTheme.dimens.size108) + .onSizeChanged { itemHeight = with(density) { it.height.toDp() } } + .clip(shape = TangemTheme.shapes.roundedCornersXMedium) + .indication(interactionSource = interactionSource, indication = LocalIndication.current) + .pointerInput(true) { + detectTapGestures( + onLongPress = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + isMenuVisible = true + pressOffset = DpOffset(x = it.x.toDp(), y = it.y.toDp()) + }, + onPress = { + val press = PressInteraction.Press(it) + interactionSource.emit(press) + tryAwaitRelease() + interactionSource.emit(PressInteraction.Release(press)) + }, + ) + }, shape = TangemTheme.shapes.roundedCornersXMedium, color = TangemTheme.colors.background.primary, - onClick = onClick ?: {}, - enabled = onClick != null, ) { ConstraintLayout( modifier = Modifier @@ -111,6 +160,50 @@ private fun CardContainer( content() } } + + var isRenameWalletDialogVisible by rememberSaveable { mutableStateOf(value = false) } + + DropdownMenu( + expanded = isMenuVisible, + onDismissRequest = { isMenuVisible = false }, + modifier = Modifier.background(color = TangemTheme.colors.background.secondary), + offset = pressOffset.copy(y = pressOffset.y - itemHeight), + ) { + MenuItem( + textResId = R.string.common_rename, + imageVector = Icons.Outlined.Edit, + onClick = { + isMenuVisible = false + isRenameWalletDialogVisible = true + }, + ) + MenuItem(textResId = R.string.common_delete, imageVector = Icons.Outlined.Delete, onClick = onDeleteClick) + } + + if (isRenameWalletDialogVisible) { + RenameWalletDialogContent( + name = name, + onConfirm = { + onRenameClick(it) + isRenameWalletDialogVisible = false + }, + onDismiss = { isRenameWalletDialogVisible = false }, + ) + } +} + +@Composable +private fun MenuItem(@StringRes textResId: Int, imageVector: ImageVector, onClick: () -> Unit) { + DropdownMenuItem( + text = { Text(text = stringResource(id = textResId), style = TangemTheme.typography.subtitle2) }, + modifier = Modifier.background(color = TangemTheme.colors.background.secondary), + trailingIcon = { Icon(imageVector = imageVector, contentDescription = null) }, + onClick = onClick, + colors = MenuDefaults.itemColors( + textColor = TangemTheme.colors.text.primary1, + trailingIconColor = TangemColorPalette.Dark6, + ), + ) } @Composable 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 621b82f941..05340d37ed 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 @@ -27,7 +27,7 @@ internal class FiatBalanceToWalletCardConverter( } private fun WalletCardState.toLoadingWalletCardState(): WalletCardState { - return WalletCardState.Loading(id, title, additionalInfo, imageResId, onClick) + return WalletCardState.Loading(id, title, additionalInfo, imageResId, onRenameClick, onDeleteClick) } private fun WalletCardState.toErrorWalletCardState(): WalletCardState { @@ -35,7 +35,8 @@ internal class FiatBalanceToWalletCardConverter( id = id, title = title, imageResId = imageResId, - onClick = onClick, + onDeleteClick = onDeleteClick, + onRenameClick = onRenameClick, additionalInfo = WalletAdditionalInfoFactory.resolve( cardTypesResolver = cardTypeResolverProvider(), wallet = currentWalletProvider(), @@ -50,7 +51,8 @@ internal class FiatBalanceToWalletCardConverter( title = currentState.title, additionalInfo = currentState.additionalInfo ?: WalletCardState.HIDDEN_BALANCE_TEXT, imageResId = currentState.imageResId, - onClick = currentState.onClick, + onRenameClick = currentState.onRenameClick, + onDeleteClick = currentState.onDeleteClick, ) } else { val appCurrency = appCurrencyProvider() @@ -63,7 +65,8 @@ internal class FiatBalanceToWalletCardConverter( wallet = currentWalletProvider(), ), imageResId = currentState.imageResId, - onClick = currentState.onClick, + onRenameClick = currentState.onRenameClick, + onDeleteClick = currentState.onDeleteClick, balance = formatFiatAmount( fiatAmount = this.amount, fiatCurrencyCode = appCurrency.code, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorConverter.kt index ac32cdb876..82709f83a7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorConverter.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.utils import com.tangem.common.Provider import com.tangem.domain.tokens.error.TokenListError 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.WalletTokensListState import com.tangem.utils.converter.Converter @@ -10,11 +11,23 @@ import kotlinx.collections.immutable.persistentListOf internal class TokenListErrorConverter( private val currentStateProvider: Provider, -) : Converter { +) : Converter { - override fun convert(value: TokenListError): WalletMultiCurrencyState.Content { - return requireNotNull(currentStateProvider() as? WalletMultiCurrencyState.Content).copy( - tokensListState = WalletTokensListState.Content(items = persistentListOf(), onOrganizeTokensClick = null), - ) + override fun convert(value: TokenListError): WalletState { + return when (val state = currentStateProvider()) { + is WalletMultiCurrencyState.Content -> { + state.copy( + tokensListState = WalletTokensListState.Content( + items = persistentListOf(), + onOrganizeTokensClick = null, + ), + ) + } + is WalletMultiCurrencyState.Locked, + is WalletSingleCurrencyState.Content, + is WalletSingleCurrencyState.Locked, + is WalletState.Initial, + -> state + } } } \ No newline at end of file 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 551b652d6d..8e578615f1 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 @@ -471,7 +471,10 @@ internal class WalletViewModel @Inject constructor( override fun onDeleteClick(userWalletId: UserWalletId) { viewModelScope.launch(dispatchers.io) { - deleteWalletUseCase(userWalletId) + val either = deleteWalletUseCase(userWalletId) + + val state = requireNotNull(uiState as? WalletState.ContentState) + if (state.walletsListConfig.wallets.size <= 1 && either.isRight()) router.openStoriesScreen() } } From 564ab852c20e7b414a3513501cc0027b52feffcc Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 24 Aug 2023 16:20:10 +0300 Subject: [PATCH 21/44] Updated on 2026-08-14 --- app/build.gradle.kts | 4 +++ .../tap/di/domain/AppThemeDomainModule.kt | 24 +++++++++++++ data/app-theme/.gitignore | 1 + data/app-theme/build.gradle.kts | 34 +++++++++++++++++++ .../apptheme/MockAppThemeModeRepository.kt | 19 +++++++++++ .../apptheme/di/AppThemeModeDataModule.kt | 20 +++++++++++ domain/app-theme/.gitignore | 1 + domain/app-theme/build.gradle.kts | 12 +++++++ domain/app-theme/models/.gitignore | 1 + domain/app-theme/models/build.gradle.kts | 4 +++ .../domain/apptheme/model/AppThemeMode.kt | 30 ++++++++++++++++ .../apptheme/ChangeAppThemeModeUseCase.kt | 31 +++++++++++++++++ .../domain/apptheme/GetAppThemeModeUseCase.kt | 33 ++++++++++++++++++ .../apptheme/error/AppThemeModeError.kt | 6 ++++ .../repository/AppThemeModeRepository.kt | 24 +++++++++++++ settings.gradle.kts | 3 ++ 16 files changed, 247 insertions(+) create mode 100644 app/src/main/java/com/tangem/tap/di/domain/AppThemeDomainModule.kt create mode 100644 data/app-theme/.gitignore create mode 100644 data/app-theme/build.gradle.kts create mode 100644 data/app-theme/src/main/kotlin/com/tangem/data/apptheme/MockAppThemeModeRepository.kt create mode 100644 data/app-theme/src/main/kotlin/com/tangem/data/apptheme/di/AppThemeModeDataModule.kt create mode 100644 domain/app-theme/.gitignore create mode 100644 domain/app-theme/build.gradle.kts create mode 100644 domain/app-theme/models/.gitignore create mode 100644 domain/app-theme/models/build.gradle.kts create mode 100644 domain/app-theme/models/src/main/kotlin/com/tangem/domain/apptheme/model/AppThemeMode.kt create mode 100644 domain/app-theme/src/main/kotlin/com/tangem/domain/apptheme/ChangeAppThemeModeUseCase.kt create mode 100644 domain/app-theme/src/main/kotlin/com/tangem/domain/apptheme/GetAppThemeModeUseCase.kt create mode 100644 domain/app-theme/src/main/kotlin/com/tangem/domain/apptheme/error/AppThemeModeError.kt create mode 100644 domain/app-theme/src/main/kotlin/com/tangem/domain/apptheme/repository/AppThemeModeRepository.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5e5e70656a..ef97f665df 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -35,6 +35,8 @@ dependencies { implementation(projects.domain.txhistory) implementation(projects.domain.appCurrency) implementation(projects.domain.appCurrency.models) + implementation(projects.domain.appTheme) + implementation(projects.domain.appTheme.models) implementation(project(":common")) implementation(project(":core:analytics")) @@ -55,6 +57,7 @@ dependencies { implementation(projects.data.tokens) implementation(projects.data.txhistory) implementation(projects.data.appCurrency) + implementation(projects.data.appTheme) /** Features */ implementation(project(":features:onboarding")) @@ -86,6 +89,7 @@ dependencies { implementation(deps.lifecycle.runtime.ktx) implementation(deps.lifecycle.common.java8) implementation(deps.lifecycle.viewModel.ktx) + implementation(deps.lifecycle.compose) /** Compose libraries */ implementation(deps.compose.constraintLayout) diff --git a/app/src/main/java/com/tangem/tap/di/domain/AppThemeDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AppThemeDomainModule.kt new file mode 100644 index 0000000000..b4378101b0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/AppThemeDomainModule.kt @@ -0,0 +1,24 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.apptheme.ChangeAppThemeModeUseCase +import com.tangem.domain.apptheme.GetAppThemeModeUseCase +import com.tangem.domain.apptheme.repository.AppThemeModeRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent + +@Module +@InstallIn(ViewModelComponent::class) +internal object AppThemeDomainModule { + + @Provides + fun provideGetAppThemeModeUpdatesUseCase(appThemeModeRepository: AppThemeModeRepository): GetAppThemeModeUseCase { + return GetAppThemeModeUseCase(appThemeModeRepository) + } + + @Provides + fun provideChangeAppThemeModeUseCase(appThemeModeRepository: AppThemeModeRepository): ChangeAppThemeModeUseCase { + return ChangeAppThemeModeUseCase(appThemeModeRepository) + } +} \ No newline at end of file diff --git a/data/app-theme/.gitignore b/data/app-theme/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/data/app-theme/.gitignore @@ -0,0 +1 @@ +/build diff --git a/data/app-theme/build.gradle.kts b/data/app-theme/build.gradle.kts new file mode 100644 index 0000000000..b0f1f17a6a --- /dev/null +++ b/data/app-theme/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.data.apptheme" +} + +dependencies { + + /** Project - Domain */ + implementation(projects.domain.core) + implementation(projects.domain.appTheme) + implementation(projects.domain.appTheme.models) + + /** Project - Data */ + implementation(projects.core.datasource) + implementation(projects.data.common) + + /** Project - Utils */ + implementation(projects.core.utils) + + /** DI */ + implementation(deps.hilt.core) + kapt(deps.hilt.kapt) + + /** Other */ + implementation(deps.kotlin.coroutines) + implementation(deps.timber) + implementation(deps.jodatime) +} \ No newline at end of file diff --git a/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/MockAppThemeModeRepository.kt b/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/MockAppThemeModeRepository.kt new file mode 100644 index 0000000000..a4a885aab2 --- /dev/null +++ b/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/MockAppThemeModeRepository.kt @@ -0,0 +1,19 @@ +package com.tangem.data.apptheme + +import com.tangem.domain.apptheme.model.AppThemeMode +import com.tangem.domain.apptheme.repository.AppThemeModeRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +internal class MockAppThemeModeRepository : AppThemeModeRepository { + + private val appThemeModeFlow = MutableStateFlow(AppThemeMode.DEFAULT) + + override fun getAppThemeMode(): Flow { + return appThemeModeFlow + } + + override suspend fun changeAppThemeMode(mode: AppThemeMode) { + appThemeModeFlow.value = mode + } +} \ No newline at end of file diff --git a/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/di/AppThemeModeDataModule.kt b/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/di/AppThemeModeDataModule.kt new file mode 100644 index 0000000000..6847fad0b8 --- /dev/null +++ b/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/di/AppThemeModeDataModule.kt @@ -0,0 +1,20 @@ +package com.tangem.data.apptheme.di + +import com.tangem.data.apptheme.MockAppThemeModeRepository +import com.tangem.domain.apptheme.repository.AppThemeModeRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object AppThemeModeDataModule { + + @Provides + @Singleton + fun provideAppThemeModeRepository(): AppThemeModeRepository { + return MockAppThemeModeRepository() + } +} \ No newline at end of file diff --git a/domain/app-theme/.gitignore b/domain/app-theme/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/domain/app-theme/.gitignore @@ -0,0 +1 @@ +/build diff --git a/domain/app-theme/build.gradle.kts b/domain/app-theme/build.gradle.kts new file mode 100644 index 0000000000..79f941dbbb --- /dev/null +++ b/domain/app-theme/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +dependencies { + + /** Project - Domain */ + implementation(projects.core.utils) + implementation(projects.domain.core) + implementation(projects.domain.appTheme.models) +} \ No newline at end of file diff --git a/domain/app-theme/models/.gitignore b/domain/app-theme/models/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/domain/app-theme/models/.gitignore @@ -0,0 +1 @@ +/build diff --git a/domain/app-theme/models/build.gradle.kts b/domain/app-theme/models/build.gradle.kts new file mode 100644 index 0000000000..7ff7fb7522 --- /dev/null +++ b/domain/app-theme/models/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} \ No newline at end of file diff --git a/domain/app-theme/models/src/main/kotlin/com/tangem/domain/apptheme/model/AppThemeMode.kt b/domain/app-theme/models/src/main/kotlin/com/tangem/domain/apptheme/model/AppThemeMode.kt new file mode 100644 index 0000000000..f6ff6269f7 --- /dev/null +++ b/domain/app-theme/models/src/main/kotlin/com/tangem/domain/apptheme/model/AppThemeMode.kt @@ -0,0 +1,30 @@ +package com.tangem.domain.apptheme.model + +/** + * Enumerates the possible modes for the application's theme. + */ +enum class AppThemeMode { + /** + * Forces the dark theme mode regardless of system settings. + */ + FORCE_DARK, + + /** + * Forces the light theme mode regardless of system settings. + */ + FORCE_LIGHT, + + /** + * Follows the system-wide theme mode. + */ + FOLLOW_SYSTEM, + + ; + + companion object { + /** + * The default [AppThemeMode]. + */ + val DEFAULT: AppThemeMode = FORCE_LIGHT + } +} \ No newline at end of file diff --git a/domain/app-theme/src/main/kotlin/com/tangem/domain/apptheme/ChangeAppThemeModeUseCase.kt b/domain/app-theme/src/main/kotlin/com/tangem/domain/apptheme/ChangeAppThemeModeUseCase.kt new file mode 100644 index 0000000000..718f75a1a3 --- /dev/null +++ b/domain/app-theme/src/main/kotlin/com/tangem/domain/apptheme/ChangeAppThemeModeUseCase.kt @@ -0,0 +1,31 @@ +package com.tangem.domain.apptheme + +import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.domain.apptheme.error.AppThemeModeError +import com.tangem.domain.apptheme.model.AppThemeMode +import com.tangem.domain.apptheme.repository.AppThemeModeRepository + +/** + * Use case responsible for changing the application theme mode. + * + * @property appThemeModeRepository The repository providing access to theme mode settings. + */ +class ChangeAppThemeModeUseCase( + private val appThemeModeRepository: AppThemeModeRepository, +) { + + /** + * Changes the application theme mode. + * + * @param mode The new [AppThemeMode] to set. + * @return An [Either] instance. The right side contains a [Unit] value indicating success, + * and the left side contains any [AppThemeModeError] that occurred during the process. + */ + suspend operator fun invoke(mode: AppThemeMode): Either = either { + catch({ appThemeModeRepository.changeAppThemeMode(mode) }) { + raise(AppThemeModeError.DataError(it)) + } + } +} \ No newline at end of file diff --git a/domain/app-theme/src/main/kotlin/com/tangem/domain/apptheme/GetAppThemeModeUseCase.kt b/domain/app-theme/src/main/kotlin/com/tangem/domain/apptheme/GetAppThemeModeUseCase.kt new file mode 100644 index 0000000000..171209c0ba --- /dev/null +++ b/domain/app-theme/src/main/kotlin/com/tangem/domain/apptheme/GetAppThemeModeUseCase.kt @@ -0,0 +1,33 @@ +package com.tangem.domain.apptheme + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.apptheme.error.AppThemeModeError +import com.tangem.domain.apptheme.model.AppThemeMode +import com.tangem.domain.apptheme.repository.AppThemeModeRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map + +/** + * Use case responsible for retrieving the current application theme mode. + * + * @property appThemeModeRepository The repository providing access to theme mode settings. + */ +class GetAppThemeModeUseCase( + private val appThemeModeRepository: AppThemeModeRepository, +) { + + /** + * Invokes the use case to retrieve the current application theme mode. + * + * @return A [Flow] emitting an [Either] instance. The right side contains the retrieved + * [AppThemeMode], and the left side contains any [AppThemeModeError] that occurred during the process. + */ + operator fun invoke(): Flow> { + return appThemeModeRepository.getAppThemeMode() + .map> { it.right() } + .catch { emit(AppThemeModeError.DataError(it).left()) } + } +} \ No newline at end of file diff --git a/domain/app-theme/src/main/kotlin/com/tangem/domain/apptheme/error/AppThemeModeError.kt b/domain/app-theme/src/main/kotlin/com/tangem/domain/apptheme/error/AppThemeModeError.kt new file mode 100644 index 0000000000..2ef8cfebb3 --- /dev/null +++ b/domain/app-theme/src/main/kotlin/com/tangem/domain/apptheme/error/AppThemeModeError.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.apptheme.error + +sealed class AppThemeModeError { + + data class DataError(val cause: Throwable) : AppThemeModeError() +} \ No newline at end of file diff --git a/domain/app-theme/src/main/kotlin/com/tangem/domain/apptheme/repository/AppThemeModeRepository.kt b/domain/app-theme/src/main/kotlin/com/tangem/domain/apptheme/repository/AppThemeModeRepository.kt new file mode 100644 index 0000000000..3d2e2d5935 --- /dev/null +++ b/domain/app-theme/src/main/kotlin/com/tangem/domain/apptheme/repository/AppThemeModeRepository.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.apptheme.repository + +import com.tangem.domain.apptheme.model.AppThemeMode +import kotlinx.coroutines.flow.Flow + +/** + * Represents a repository for managing the application's theme mode settings. + */ +interface AppThemeModeRepository { + + /** + * Retrieves the current application theme mode as a flow. + * + * @return A [Flow] emitting the current [AppThemeMode]. + */ + fun getAppThemeMode(): Flow + + /** + * Changes the application's theme mode to the specified [mode]. + * + * @param mode The new [AppThemeMode] to be set. + */ + suspend fun changeAppThemeMode(mode: AppThemeMode) +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 33ccfc81d6..d2d077c6e7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -110,6 +110,8 @@ include(":domain:txhistory") include(":domain:txhistory:models") include(":domain:app-currency") include(":domain:app-currency:models") +include(":domain:app-theme") +include(":domain:app-theme:models") // endregion Domain modules // region Data modules @@ -120,4 +122,5 @@ include(":data:source:preferences") include(":data:settings") include(":data:txhistory") include(":data:app-currency") +include(":data:app-theme") // endregion Data modules \ No newline at end of file From 575389311f5eff95048c7d1197df01da21f2e410 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 24 Aug 2023 16:21:28 +0800 Subject: [PATCH 22/44] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 8 ++ .../buttons/HorizontalActionChips.kt | 2 +- .../ui/components/buttons/actions/Actions.kt | 43 ++++--- .../tokens/GetCryptoCurrencyActionsUseCase.kt | 8 +- .../presentation/common/WalletPreviewData.kt | 16 +-- .../state/components/WalletManageButton.kt | 80 ++++++------- .../WalletCryptoCurrencyActionsConverter.kt | 50 ++++++++ .../state/factory/WalletLockedConverter.kt | 110 ++++++++++++++++++ .../factory/WalletRefreshStateConverter.kt | 40 ++++--- .../factory/WalletSkeletonStateConverter.kt | 13 +-- .../state/factory/WalletStateFactory.kt | 98 ++++------------ .../wallet/viewmodels/WalletClickIntents.kt | 6 + .../wallet/viewmodels/WalletViewModel.kt | 29 ++++- 13 files changed, 333 insertions(+), 170 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletCryptoCurrencyActionsConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLockedConverter.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 0acb93c93c..3dd86e4e03 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -70,4 +70,12 @@ internal object TokensDomainModule { ): ApplyTokenListSortingUseCase { return ApplyTokenListSortingUseCase(currenciesRepository, dispatchers) } + + @Provides + @ViewModelScoped + fun provideGetCryptoCurrencyActionsUseCase( + dispatchers: CoroutineDispatcherProvider, + ): GetCryptoCurrencyActionsUseCase { + return GetCryptoCurrencyActionsUseCase(dispatchers) + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt index 6b20a931f8..b34aa9c970 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt @@ -33,7 +33,7 @@ fun HorizontalActionChips( ) { items( items = buttons, - key = { config -> "${config.text.hashCode()} ${config.iconResId}" }, + key = { config -> config.text.hashCode() }, itemContent = { ActionButton(config = it) }, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt index 65422a2f7b..2167ad385c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.buttons.actions +import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -7,6 +8,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -78,43 +80,52 @@ private fun Button( modifier: Modifier = Modifier, color: Color = TangemTheme.colors.button.secondary, ) { + val backgroundColor by animateColorAsState( + targetValue = if (config.enabled) color else TangemTheme.colors.button.disabled, + label = "Update background color", + ) + Row( modifier = modifier .heightIn(min = TangemTheme.dimens.size36) .clip(shape) - .background( - color = if (config.enabled) color else TangemTheme.colors.button.disabled, - shape = shape, - ) + .background(color = backgroundColor, shape = shape) .clickable(enabled = config.enabled, onClick = config.onClick) - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing24, - ) + .padding(start = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing24) .padding(vertical = TangemTheme.dimens.spacing8), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, ) { - Icon( - painter = painterResource(id = config.iconResId), - contentDescription = null, - modifier = Modifier.size(size = TangemTheme.dimens.size20), - tint = when { + val iconTint by animateColorAsState( + targetValue = when { !config.enabled -> TangemTheme.colors.icon.informative config.dimContent -> TangemTheme.colors.icon.secondary else -> TangemTheme.colors.icon.primary1 }, + label = "Update tint color", + ) + + Icon( + painter = painterResource(id = config.iconResId), + contentDescription = null, + modifier = Modifier.size(size = TangemTheme.dimens.size20), + tint = iconTint, ) SpacerW8() - Text( - text = config.text.resolveReference(), - color = when { + val textColor by animateColorAsState( + targetValue = when { !config.enabled -> TangemTheme.colors.text.disabled config.dimContent -> TangemTheme.colors.text.secondary else -> TangemTheme.colors.text.primary1 }, + label = "Update text color", + ) + + Text( + text = config.text.resolveReference(), + color = textColor, overflow = TextOverflow.Ellipsis, maxLines = 1, style = TangemTheme.typography.button, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index ec8f8f39f8..d705aacf94 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -3,7 +3,9 @@ package com.tangem.domain.tokens import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn class GetCryptoCurrencyActionsUseCase( private val dispatchers: CoroutineDispatcherProvider, @@ -22,10 +24,10 @@ class GetCryptoCurrencyActionsUseCase( tokenId = tokenId, states = listOf( TokenActionsState.ActionState.Buy(true), - TokenActionsState.ActionState.Sell(true), + TokenActionsState.ActionState.Send(true), TokenActionsState.ActionState.Receive(true), - TokenActionsState.ActionState.Swap(true), TokenActionsState.ActionState.Sell(true), + TokenActionsState.ActionState.Swap(true), ), ) } 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 91d8ccbd5d..bf75186348 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 @@ -277,13 +277,15 @@ internal object WalletPreviewData { ).toImmutableList(), ) - private val manageButtons = persistentListOf( - WalletManageButton.Buy(onClick = {}), - WalletManageButton.Send(onClick = {}), - WalletManageButton.Receive(onClick = {}), - WalletManageButton.Exchange(onClick = {}), - WalletManageButton.CopyAddress(onClick = {}), - ) + private val manageButtons by lazy { + persistentListOf( + WalletManageButton.Buy(enabled = true, onClick = {}), + WalletManageButton.Send(enabled = true, onClick = {}), + WalletManageButton.Receive(onClick = {}), + WalletManageButton.Sell(enabled = true, onClick = {}), + WalletManageButton.Swap(enabled = true, onClick = {}), + ) + } val multicurrencyWalletScreenState by lazy { WalletMultiCurrencyState.Content( 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 e8745338ed..46f70a5b22 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 @@ -8,7 +8,7 @@ import com.tangem.feature.wallet.impl.R /** * Wallet manage button state * - * @param config action config + * @property config action config * [REDACTED_AUTHOR] */ @@ -16,87 +16,79 @@ import com.tangem.feature.wallet.impl.R sealed class WalletManageButton(val config: ActionButtonConfig) { /** Lambda be invoked when manage button is clicked */ - abstract val onClick: (() -> Unit)? + abstract val onClick: () -> Unit /** * Buy * - * @param onClick lambda be invoked when manage button is clicked + * @property enabled button click availability + * @property onClick lambda be invoked when Buy button is clicked */ - data class Buy(override val onClick: (() -> Unit)? = null) : WalletManageButton( + data class Buy(val enabled: Boolean, override val onClick: () -> Unit) : WalletManageButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.common_buy), iconResId = R.drawable.ic_plus_24, - onClick = onClick ?: {}, - enabled = onClick != null, - ), - ) - - /** - * Sell - * - * @param onClick lambda be invoked when manage button is clicked - */ - data class Sell(override val onClick: (() -> Unit)? = null) : WalletManageButton( - config = ActionButtonConfig( - text = TextReference.Res(id = R.string.common_sell), - iconResId = R.drawable.ic_currency_24, - onClick = onClick ?: {}, - enabled = onClick != null, + onClick = onClick, + enabled = enabled, ), ) /** * Send * - * @param onClick lambda be invoked when manage button is clicked + * @property enabled button click availability + * @property onClick lambda be invoked when Send button is clicked */ - data class Send(override val onClick: (() -> Unit)? = null) : WalletManageButton( + data class Send(val enabled: Boolean, override val onClick: () -> Unit) : WalletManageButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.common_send), iconResId = R.drawable.ic_arrow_up_24, - onClick = onClick ?: {}, - enabled = onClick != null, + onClick = onClick, + enabled = enabled, ), ) /** * Receive * - * @param onClick lambda be invoked when manage button is clicked + * @property onClick lambda be invoked when Receive button is clicked */ data class Receive(override val onClick: () -> Unit) : WalletManageButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.common_receive), iconResId = R.drawable.ic_arrow_down_24, onClick = onClick, + enabled = true, ), ) /** - * Exchange + * Sell * - * @param onClick lambda be invoked when manage button is clicked + * @property enabled button click availability + * @property onClick lambda be invoked when Sell button is clicked */ - data class Exchange(override val onClick: (() -> Unit)? = null) : WalletManageButton( + data class Sell(val enabled: Boolean, override val onClick: () -> Unit) : WalletManageButton( config = ActionButtonConfig( - text = TextReference.Res(id = R.string.common_exchange), - iconResId = R.drawable.ic_exchange_vertical_24, - onClick = onClick ?: {}, - enabled = onClick != null, - ), - ) - - /** - * Copy address - * - * @param onClick lambda be invoked when manage button is clicked - */ - data class CopyAddress(override val onClick: () -> Unit) : WalletManageButton( - config = ActionButtonConfig( - text = TextReference.Res(id = R.string.common_copy_address), - iconResId = R.drawable.ic_copy_24, + text = TextReference.Res(id = R.string.common_sell), + iconResId = R.drawable.ic_currency_24, onClick = onClick, + enabled = enabled, + ), + ) + + /** + * Swap + * + * @property enabled button click availability + * @property onClick lambda be invoked when Swap button is clicked + */ + data class Swap(val enabled: Boolean, override val onClick: () -> Unit) : WalletManageButton( + config = ActionButtonConfig( + text = TextReference.Res(id = R.string.common_swap), + iconResId = R.drawable.ic_exchange_vertical_24, + onClick = onClick, + enabled = enabled, ), ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletCryptoCurrencyActionsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletCryptoCurrencyActionsConverter.kt new file mode 100644 index 0000000000..ea88f686dd --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletCryptoCurrencyActionsConverter.kt @@ -0,0 +1,50 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory + +import com.tangem.common.Provider +import com.tangem.domain.tokens.model.TokenActionsState +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.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +internal class WalletCryptoCurrencyActionsConverter( + private val currentStateProvider: Provider, + private val clickIntents: WalletClickIntents, +) : Converter, WalletState> { + + override fun convert(value: List): WalletState { + return when (val state = currentStateProvider()) { + is WalletSingleCurrencyState.Content -> state.copy(buttons = value.mapToManageButtons()) + is WalletSingleCurrencyState.Locked, + is WalletMultiCurrencyState, + is WalletState.Initial, + -> state + } + } + + private fun List.mapToManageButtons(): ImmutableList { + return this + .mapNotNull { action -> + when (action) { + is TokenActionsState.ActionState.Buy -> { + WalletManageButton.Buy(enabled = action.enabled, onClick = clickIntents::onBuyClick) + } + is TokenActionsState.ActionState.Receive -> { + WalletManageButton.Receive(onClick = clickIntents::onReceiveClick) + } + is TokenActionsState.ActionState.Sell -> { + WalletManageButton.Sell(enabled = action.enabled, onClick = clickIntents::onSellClick) + } + is TokenActionsState.ActionState.Send -> { + WalletManageButton.Send(enabled = action.enabled, onClick = clickIntents::onSellClick) + } + is TokenActionsState.ActionState.Swap -> null + } + } + .toImmutableList() + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLockedConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLockedConverter.kt new file mode 100644 index 0000000000..5ec6613bb8 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLockedConverter.kt @@ -0,0 +1,110 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory + +import com.tangem.common.Provider +import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +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.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTopBarConfig +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.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +internal class WalletLockedConverter( + private val currentStateProvider: Provider, + private val currentCardTypeResolverProvider: Provider, + private val currentWalletProvider: Provider, + private val clickIntents: WalletClickIntents, +) : Converter { + + override fun convert(value: Unit): WalletState { + return when (val state = currentStateProvider()) { + is WalletState.ContentState -> { + val cardTypeResolver = currentCardTypeResolverProvider() + + if (cardTypeResolver.isMultiwalletAllowed()) { + state.toMultiCurrencyLockedState(cardTypeResolver) + } else { + state.toSingleCurrencyLockedState(cardTypeResolver) + } + } + is WalletState.Initial -> state + } + } + + private fun WalletState.ContentState.toMultiCurrencyLockedState( + cardTypeResolver: CardTypesResolver, + ): WalletMultiCurrencyState.Locked { + return WalletMultiCurrencyState.Locked( + onBackClick = onBackClick, + topBarConfig = createTopBarConfig(), + walletsListConfig = createWalletsListConfig(cardTypeResolver), + pullToRefreshConfig = pullToRefreshConfig, + onUnlockWalletsNotificationClick = clickIntents::onUnlockWalletNotificationClick, + onUnlockClick = clickIntents::onUnlockWalletClick, + onScanClick = clickIntents::onScanCardClick, + ) + } + + private fun WalletState.ContentState.toSingleCurrencyLockedState( + cardTypeResolver: CardTypesResolver, + ): WalletSingleCurrencyState.Locked { + return WalletSingleCurrencyState.Locked( + onBackClick = onBackClick, + topBarConfig = createTopBarConfig(), + walletsListConfig = createWalletsListConfig(cardTypeResolver), + pullToRefreshConfig = pullToRefreshConfig, + buttons = createButtons(), + onUnlockWalletsNotificationClick = clickIntents::onUnlockWalletNotificationClick, + onUnlockClick = clickIntents::onUnlockWalletClick, + onScanClick = clickIntents::onScanCardClick, + onExploreClick = clickIntents::onExploreClick, + ) + } + + private fun WalletState.ContentState.createTopBarConfig(): WalletTopBarConfig { + return topBarConfig.copy(onMoreClick = clickIntents::onUnlockWalletNotificationClick) + } + + private fun WalletState.ContentState.createWalletsListConfig( + cardTypeResolver: CardTypesResolver, + ): WalletsListConfig { + return walletsListConfig.copy( + wallets = walletsListConfig.wallets + .map { walletCardState -> + WalletCardState.LockedContent( + id = walletCardState.id, + title = walletCardState.title, + additionalInfo = if (cardTypeResolver.isMultiwalletAllowed()) { + WalletAdditionalInfoFactory.resolve( + cardTypesResolver = cardTypeResolver, + wallet = currentWalletProvider(), + ) + } else { + null + }, + imageResId = walletCardState.imageResId, + onRenameClick = walletCardState.onRenameClick, + onDeleteClick = walletCardState.onDeleteClick, + ) + } + .toImmutableList(), + ) + } + + private fun createButtons(): ImmutableList { + return persistentListOf( + WalletManageButton.Buy(enabled = false, onClick = {}), + WalletManageButton.Send(enabled = false, onClick = {}), + WalletManageButton.Receive(onClick = {}), + WalletManageButton.Sell(enabled = false, onClick = {}), + ) + } +} \ No newline at end of file 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 index faa6a40114..f820ff8444 100644 --- 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 @@ -8,11 +8,8 @@ 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.* 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.ImmutableList @@ -36,22 +33,23 @@ internal class WalletRefreshStateConverter( private fun WalletMultiCurrencyState.Content.getRefreshState(): WalletMultiCurrencyState.Content { return copy( - walletsListConfig = getWalletsListConfig(), - pullToRefreshConfig = getPullToRefreshConfig(), - tokensListState = getTokenListState(), + walletsListConfig = createWalletsListConfig(), + pullToRefreshConfig = createPullToRefreshConfig(), + tokensListState = createTokenListState(), ) } private fun WalletSingleCurrencyState.Content.getRefreshState(): WalletSingleCurrencyState.Content { return copy( - walletsListConfig = getWalletsListConfig(), - pullToRefreshConfig = getPullToRefreshConfig(), - txHistoryState = getTxHistoryState(), + walletsListConfig = createWalletsListConfig(), + pullToRefreshConfig = createPullToRefreshConfig(), + buttons = buttons.mapToDisabledButton(), + txHistoryState = createTxHistoryState(), marketPriceBlockState = MarketPriceBlockState.Loading(currencyName = marketPriceBlockState.currencyName), ) } - private fun WalletState.ContentState.getWalletsListConfig(): WalletsListConfig { + private fun WalletState.ContentState.createWalletsListConfig(): WalletsListConfig { val selectedWallet = walletsListConfig.wallets[walletsListConfig.selectedWalletIndex] val additionalInfo = if (currentCardTypeResolverProvider().isMultiwalletAllowed()) { selectedWallet.additionalInfo @@ -74,11 +72,11 @@ internal class WalletRefreshStateConverter( ) } - private fun WalletState.ContentState.getPullToRefreshConfig(): WalletPullToRefreshConfig { + private fun WalletState.ContentState.createPullToRefreshConfig(): WalletPullToRefreshConfig { return pullToRefreshConfig.copy(isRefreshing = true) } - private fun WalletMultiCurrencyState.Content.getTokenListState(): WalletTokensListState { + private fun WalletMultiCurrencyState.Content.createTokenListState(): WalletTokensListState { return when (tokensListState) { is WalletTokensListState.Content -> { WalletTokensListState.Loading( @@ -100,7 +98,21 @@ internal class WalletRefreshStateConverter( .toImmutableList() } - private fun WalletSingleCurrencyState.Content.getTxHistoryState(): TxHistoryState { + private fun ImmutableList.mapToDisabledButton(): ImmutableList { + return this + .mapNotNull { button -> + when (button) { + is WalletManageButton.Buy -> button.copy(enabled = false) + is WalletManageButton.Send -> button.copy(enabled = false) + is WalletManageButton.Receive -> button + is WalletManageButton.Sell -> button.copy(enabled = false) + is WalletManageButton.Swap -> null + } + } + .toImmutableList() + } + + private fun WalletSingleCurrencyState.Content.createTxHistoryState(): TxHistoryState { if (txHistoryState is TxHistoryState.Content) { txHistoryState.contentItems.update { TxHistoryState.getDefaultLoadingTransactions(onExploreClick = clickIntents::onExploreClick) 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 a783f78001..6b5fcc5d49 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 @@ -63,7 +63,7 @@ internal class WalletSkeletonStateConverter( pullToRefreshConfig = createPullToRefreshConfig(), notifications = persistentListOf(), bottomSheetConfig = null, - buttons = getButtons(), + buttons = createButtons(), marketPriceBlockState = MarketPriceBlockState.Loading(currencyName = currencyName), txHistoryState = TxHistoryState.Content( contentItems = MutableStateFlow( @@ -127,15 +127,12 @@ internal class WalletSkeletonStateConverter( return WalletPullToRefreshConfig(isRefreshing = false, onRefresh = clickIntents::onRefreshSwipe) } - // TODO: [REDACTED_JIRA] - private fun getButtons(): ImmutableList { + private fun createButtons(): ImmutableList { return persistentListOf( - WalletManageButton.Buy(), - WalletManageButton.Send(), + WalletManageButton.Buy(enabled = false, onClick = {}), + WalletManageButton.Send(enabled = false, onClick = {}), WalletManageButton.Receive(onClick = {}), - WalletManageButton.Exchange(), - WalletManageButton.Sell(), - WalletManageButton.CopyAddress(onClick = {}), + WalletManageButton.Sell(enabled = false, onClick = {}), ) } 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 52787d6a81..af79773b6c 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 @@ -8,26 +8,22 @@ import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.tokens.error.CurrencyError import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.domain.wallets.models.UserWallet -import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.state.ActionsBottomSheetConfig 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.WalletBottomSheetConfig -import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState -import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory.WalletLoadedTxHistoryConverter import com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory.WalletLoadingTxHistoryConverter import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow /** @@ -83,6 +79,15 @@ internal class WalletStateFactory( ) } + private val lockedConverter by lazy { + WalletLockedConverter( + currentStateProvider = currentStateProvider, + currentCardTypeResolverProvider = currentCardTypeResolverProvider, + currentWalletProvider = currentWalletProvider, + clickIntents = clickIntents, + ) + } + private val refreshStateConverter by lazy { WalletRefreshStateConverter( currentStateProvider = currentStateProvider, @@ -91,6 +96,13 @@ internal class WalletStateFactory( ) } + private val cryptoCurrencyActionsConverter by lazy { + WalletCryptoCurrencyActionsConverter( + currentStateProvider = currentStateProvider, + clickIntents = clickIntents, + ) + } + fun getInitialState(): WalletState = WalletState.Initial(onBackClick = clickIntents::onBackClick) fun getSkeletonState(wallets: List, selectedWalletIndex: Int): WalletState { @@ -184,77 +196,7 @@ internal class WalletStateFactory( return loadedTxHistoryConverter.convert(txHistoryEither) } - fun getLockedState(): WalletState { - val cardTypeResolver = currentCardTypeResolverProvider() - val state = requireNotNull(currentStateProvider() as? WalletState.ContentState) - return if (cardTypeResolver.isMultiwalletAllowed()) { - WalletMultiCurrencyState.Locked( - onBackClick = state.onBackClick, - topBarConfig = state.topBarConfig.copy( - onMoreClick = clickIntents::onUnlockWalletNotificationClick, - ), - walletsListConfig = state.walletsListConfig.copy( - wallets = state.walletsListConfig.wallets - .map { walletCardState -> - WalletCardState.LockedContent( - id = walletCardState.id, - title = walletCardState.title, - imageResId = walletCardState.imageResId, - additionalInfo = WalletAdditionalInfoFactory.resolve( - cardTypesResolver = cardTypeResolver, - wallet = currentWalletProvider(), - ), - onRenameClick = walletCardState.onRenameClick, - onDeleteClick = walletCardState.onDeleteClick, - ) - } - .toImmutableList(), - ), - pullToRefreshConfig = state.pullToRefreshConfig, - onUnlockWalletsNotificationClick = clickIntents::onUnlockWalletNotificationClick, - onUnlockClick = clickIntents::onUnlockWalletClick, - onScanClick = clickIntents::onScanCardClick, - ) - } else { - WalletSingleCurrencyState.Locked( - onBackClick = state.onBackClick, - topBarConfig = state.topBarConfig.copy( - onMoreClick = clickIntents::onUnlockWalletNotificationClick, - ), - walletsListConfig = state.walletsListConfig.copy( - wallets = state.walletsListConfig.wallets - .map { walletCardState -> - WalletCardState.LockedContent( - id = walletCardState.id, - title = walletCardState.title, - imageResId = walletCardState.imageResId, - onRenameClick = walletCardState.onRenameClick, - onDeleteClick = walletCardState.onDeleteClick, - ) - } - .toImmutableList(), - ), - pullToRefreshConfig = state.pullToRefreshConfig, - buttons = getButtons(), - onUnlockWalletsNotificationClick = clickIntents::onUnlockWalletNotificationClick, - onUnlockClick = clickIntents::onUnlockWalletClick, - onScanClick = clickIntents::onScanCardClick, - onExploreClick = clickIntents::onExploreClick, - ) - } - } - - // TODO: [REDACTED_JIRA] - private fun getButtons(): ImmutableList { - return persistentListOf( - WalletManageButton.Buy(), - WalletManageButton.Send(), - WalletManageButton.Receive(onClick = {}), - WalletManageButton.Exchange(), - WalletManageButton.Sell(), - WalletManageButton.CopyAddress(onClick = {}), - ) - } + fun getLockedState(): WalletState = lockedConverter.convert(Unit) fun getSingleCurrencyLoadedBalanceState( cryptoCurrencyEither: Either, @@ -267,4 +209,8 @@ internal class WalletStateFactory( ), ) } + + fun getSingleCurrencyManageButtonsState(actions: List): WalletState { + return cryptoCurrencyActionsConverter.convert(value = actions) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt index ad201200ed..ee7ed4346d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt @@ -46,4 +46,10 @@ internal interface WalletClickIntents : TxHistoryClickIntents { fun onRenameClick(userWalletId: UserWalletId, name: String) fun onDeleteClick(userWalletId: UserWalletId) + + fun onSendClick() + + fun onReceiveClick() + + fun onSellClick() } \ No newline at end of file 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 8e578615f1..d27c5d8ba9 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 @@ -17,6 +17,7 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.settings.IsUserAlreadyRateAppUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase import com.tangem.domain.tokens.GetPrimaryCurrencyUseCase import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.model.TokenList @@ -76,6 +77,7 @@ internal class WalletViewModel @Inject constructor( private val getExploreUrlUseCase: GetExploreUrlUseCase, private val unlockWalletsUseCase: UnlockWalletsUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, private val dispatchers: CoroutineDispatcherProvider, ) : ViewModel(), DefaultLifecycleObserver, WalletClickIntents { @@ -112,6 +114,7 @@ internal class WalletViewModel @Inject constructor( private val tokensJobHolder = JobHolder() private val marketPriceJobHolder = JobHolder() + private val buttonsJobHolder = JobHolder() private val notificationsJobHolder = JobHolder() override fun onCreate(owner: LifecycleOwner) { @@ -178,8 +181,10 @@ internal class WalletViewModel @Inject constructor( private fun updateSingleCurrencyContent(index: Int, isRefreshing: Boolean) { val wallet = getWallet(index) + val blockchain = getCardTypeResolver(index).getBlockchain() + updateButtons(userWalletId = wallet.walletId, currencyId = blockchain.id) updateTxHistory( - blockchain = getCardTypeResolver(index).getBlockchain(), + blockchain = blockchain, derivationStyle = wallet.scanResponse.derivationStyleProvider.getDerivationStyle(), ) updateMarketPrice(userWalletId = wallet.walletId, isRefreshing = isRefreshing) @@ -225,6 +230,15 @@ internal class WalletViewModel @Inject constructor( .saveIn(marketPriceJobHolder) } + private fun updateButtons(userWalletId: UserWalletId, currencyId: String) { + getCryptoCurrencyActionsUseCase(userWalletId = userWalletId, tokenId = currencyId) + .distinctUntilChanged() + .onEach { uiState = stateFactory.getSingleCurrencyManageButtonsState(actions = it.states) } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(buttonsJobHolder) + } + private fun updateNotifications(index: Int, tokenList: TokenList? = null) { notificationsListFactory.create( cardTypesResolver = getCardTypeResolver(index = index), @@ -338,6 +352,7 @@ internal class WalletViewModel @Inject constructor( */ tokensJobHolder.update(job = null) marketPriceJobHolder.update(job = null) + buttonsJobHolder.update(job = null) notificationsJobHolder.update(job = null) val cacheState = WalletStateCache.getState(userWalletId = state.walletsListConfig.wallets[index].id) @@ -410,6 +425,18 @@ internal class WalletViewModel @Inject constructor( // TODO: [REDACTED_JIRA] } + override fun onSendClick() { + // TODO: [REDACTED_JIRA] + } + + override fun onReceiveClick() { + // TODO: [REDACTED_JIRA] + } + + override fun onSellClick() { + // TODO: [REDACTED_JIRA] + } + override fun onReloadClick() { uiState = stateFactory.getStateAfterContentRefreshing() updateSingleCurrencyContent( From 20c2f574070815b738ad1e7c0dd1716a27398045 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 25 Aug 2023 20:18:44 +0800 Subject: [PATCH 23/44] Updated on 2026-08-14 --- .../java/com/tangem/tap/TapApplication.kt | 7 +- .../com/tangem/tap/di/AppStateHolderModule.kt | 5 + .../tangem/tap/features/demo/DemoHelper.kt | 5 +- .../handlers/SellCurrencyIntentHandler.kt | 4 +- .../send/redux/middlewares/SendMiddleware.kt | 4 +- .../tap/features/send/ui/SendFragment.kt | 6 +- .../middlewares/TradeCryptoMiddleware.kt | 139 ++++++++++++++++-- .../redux/middlewares/WalletMiddleware.kt | 3 +- .../wallet/redux/reducers/WalletReducer.kt | 1 - .../wallet/ui/WalletDetailsFragment.kt | 7 +- .../ChooseTradeActionBottomSheetDialog.kt | 7 +- ...sianCardholdersWarningBottomSheetDialog.kt | 4 +- .../wallet/ui/wallet/SingleWalletView.kt | 7 +- .../CurrencyExchangeManager.kt | 22 +-- .../com/tangem/tap/proxy/AppStateHolder.kt | 8 +- .../tap/proxy/redux/DaggerGraphState.kt | 2 + .../tangem/domain/redux/ReduxStateHolder.kt | 8 + .../DefaultWalletManagersFacade.kt | 2 +- .../walletmanager/WalletManagersFacade.kt | 10 ++ domain/tokens/build.gradle.kts | 2 + .../domain/tokens/legacy/TradeCryptoAction.kt | 43 ++++++ .../tokens/model/CryptoCurrencyStatus.kt | 6 + .../operations/CurrencyStatusOperations.kt | 3 + .../domain/tokens/mock/MockTokensStates.kt | 12 +- features/wallet/impl/build.gradle.kts | 1 + .../WalletCryptoCurrencyActionsConverter.kt | 2 +- .../wallet/viewmodels/WalletViewModel.kt | 34 ++++- 27 files changed, 292 insertions(+), 62 deletions(-) create mode 100644 domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index e44d8366ab..65bbb228d0 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -26,6 +26,7 @@ import com.tangem.domain.DomainLayer import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.common.LogConfig +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.WalletManagersRepository import com.tangem.feature.learn2earn.domain.api.Learn2earnInteractor import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles @@ -41,8 +42,8 @@ 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.log.TimberFormatStrategy import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.appReducer import com.tangem.tap.common.redux.global.GlobalAction @@ -166,6 +167,9 @@ class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var appCurrencyRepository: AppCurrencyRepository + @Inject + lateinit var walletManagersFacade: WalletManagersFacade + override fun onCreate() { super.onCreate() @@ -185,6 +189,7 @@ class TapApplication : Application(), ImageLoaderFactory { tokenDetailsFeatureToggles = tokenDetailsFeatureToggles, scanCardProcessor = scanCardProcessor, appCurrencyRepository = appCurrencyRepository, + walletManagersFacade = walletManagersFacade, ), ), ) diff --git a/app/src/main/java/com/tangem/tap/di/AppStateHolderModule.kt b/app/src/main/java/com/tangem/tap/di/AppStateHolderModule.kt index 05cc610f95..5016fe7d2e 100644 --- a/app/src/main/java/com/tangem/tap/di/AppStateHolderModule.kt +++ b/app/src/main/java/com/tangem/tap/di/AppStateHolderModule.kt @@ -1,6 +1,7 @@ package com.tangem.tap.di import com.tangem.core.navigation.NavigationStateHolder +import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.tap.proxy.AppStateHolder import dagger.Binds @@ -20,4 +21,8 @@ internal interface AppStateHolderModule { @Binds @Singleton fun bindsNavigationStateHolder(appStateHolder: AppStateHolder): NavigationStateHolder + + @Binds + @Singleton + fun bindsReduxStateHolder(appStateHolder: AppStateHolder): ReduxStateHolder } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt index fcf584cf31..1de357e9df 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.demo import com.tangem.domain.demo.DemoConfig import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.tap.common.extensions.dispatchNotification import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.details.redux.DetailsAction @@ -21,8 +22,8 @@ object DemoHelper { private val disabledActionFeatures = listOf( WalletConnectAction.StartWalletConnect::class.java, - WalletAction.TradeCryptoAction.Buy::class.java, - WalletAction.TradeCryptoAction.Sell::class.java, + TradeCryptoAction.Buy::class.java, + TradeCryptoAction.Sell::class.java, BackupAction.StartBackup::class.java, WalletAction.ExploreAddress::class.java, DetailsAction.ResetToFactory.Start::class.java, diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt index 9d28a45db6..2879f9ef13 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt @@ -1,9 +1,9 @@ package com.tangem.tap.features.intentHandler.handlers import android.content.Intent +import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.features.intentHandler.IntentHandler -import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.store import timber.log.Timber @@ -22,7 +22,7 @@ class SellCurrencyIntentHandler : IntentHandler { Timber.d("MoonPay Sell: $amount $currency to $destinationAddress") store.dispatchWithMain( - WalletAction.TradeCryptoAction.SendCrypto( + TradeCryptoAction.SendCrypto( currencyId = currency, amount = amount, destinationAddress = destinationAddress, diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index 628e934435..cb2e4c26bd 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -19,6 +19,7 @@ import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.extensions.minimalAmount import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Basic @@ -38,7 +39,6 @@ import com.tangem.tap.features.send.redux.* import com.tangem.tap.features.send.redux.FeeAction.RequestFee import com.tangem.tap.features.send.redux.states.* import com.tangem.tap.features.wallet.models.Currency -import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.wallet.R import kotlinx.coroutines.Dispatchers @@ -254,7 +254,7 @@ private fun sendTransaction( ), ) Analytics.sendSelectedCurrencyEvent(mainCurrencyType) - dispatch(WalletAction.TradeCryptoAction.FinishSelling(externalTransactionData.transactionId)) + dispatch(TradeCryptoAction.FinishSelling(externalTransactionData.transactionId)) } else { Analytics.send( Basic.TransactionSent( diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt index 748ecacb4b..0e963d3ac5 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt @@ -18,6 +18,7 @@ import com.google.android.material.textfield.TextInputEditText import com.tangem.Message import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.NavigationAction +import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.sdk.extensions.hideSoftKeyboard import com.tangem.tap.common.KeyboardObserver import com.tangem.tap.common.analytics.events.Token @@ -39,7 +40,6 @@ import com.tangem.tap.features.send.redux.FeeActionUi.* import com.tangem.tap.features.send.redux.states.FeeType import com.tangem.tap.features.send.redux.states.MainCurrencyType import com.tangem.tap.features.send.ui.stateSubscribers.SendStateSubscriber -import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter import com.tangem.tap.mainScope import com.tangem.tap.store @@ -337,9 +337,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { if (externalTransactionData == null) { store.dispatch(NavigationAction.PopBackTo()) } else { - store.dispatch( - WalletAction.TradeCryptoAction.FinishSelling(externalTransactionData.transactionId), - ) + store.dispatch(TradeCryptoAction.FinishSelling(externalTransactionData.transactionId)) } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index 65be537ac8..2528753b4e 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -4,12 +4,16 @@ import androidx.core.os.bundleOf import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.Blockchain import com.tangem.common.extensions.guard import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.common.extensions.toNetworkId +import com.tangem.domain.common.util.derivationStyleProvider +import com.tangem.domain.tokens.legacy.TradeCryptoAction +import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.feature.swap.presentation.SwapFragment import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Token @@ -27,6 +31,7 @@ import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.buyErc20TestnetTokens +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store import kotlinx.coroutines.launch @@ -35,19 +40,27 @@ import kotlinx.serialization.json.Json import com.tangem.feature.swap.domain.models.domain.Currency as SwapCurrency class TradeCryptoMiddleware { - fun handle(state: () -> AppState?, action: WalletAction.TradeCryptoAction) { + fun handle(state: () -> AppState?, action: TradeCryptoAction) { if (DemoHelper.tryHandle(state, action)) return when (action) { - is WalletAction.TradeCryptoAction.Buy -> proceedBuyAction(state, action) - is WalletAction.TradeCryptoAction.Sell -> proceedSellAction() - is WalletAction.TradeCryptoAction.SendCrypto -> preconfigureAndOpenSendScreen(action) - is WalletAction.TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId) - is WalletAction.TradeCryptoAction.Swap -> openSwap() + is TradeCryptoAction.Buy -> proceedBuyAction(state, action) + is TradeCryptoAction.Sell -> proceedSellAction() + is TradeCryptoAction.SendCrypto -> preconfigureAndOpenSendScreen(action) + is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId) + is TradeCryptoAction.Swap -> { + openSwap(currency = store.state.walletState.selectedWalletData?.currency?.toSwapCurrency()) + } + is TradeCryptoAction.New.Buy -> proceedNewBuyAction(state, action) + TradeCryptoAction.New.Send -> store.dispatch(WalletAction.Send()) + is TradeCryptoAction.New.Sell -> proceedNewSellAction(action) + is TradeCryptoAction.New.Swap -> { + openSwap(currency = action.cryptoCurrency.toSwapCurrency()) + } } } - private fun proceedBuyAction(state: () -> AppState?, action: WalletAction.TradeCryptoAction.Buy) { + private fun proceedBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) { val selectedWalletData = store.state.walletState.selectedWalletData ?: return val currency = chooseAppropriateCurrency(store.state.walletState) ?: return @@ -75,7 +88,7 @@ class TradeCryptoMiddleware { buyErc20TestnetTokens( card = card, walletManager = walletManager, - token = currency.token, + destinationAddress = currency.token.contractAddress, ) } return @@ -93,6 +106,56 @@ class TradeCryptoMiddleware { } } + private fun proceedNewBuyAction(state: () -> AppState?, action: TradeCryptoAction.New.Buy) { + val networkAddress = action.cryptoCurrencyStatus.value.networkAddress?.defaultAddress ?: return + + if (action.checkUserLocation && state()?.globalState?.userCountryCode == RUSSIA_COUNTRY_CODE) { + store.dispatchOnMain(WalletAction.DialogAction.RussianCardholdersWarningDialog()) + return + } + + val status = action.cryptoCurrencyStatus + val currency = status.currency + val blockchain = Blockchain.fromId(currency.network.id.value) + if (currency is CryptoCurrency.Token && currency.network.isTestnet) { + scope.launch { + val walletManager = store.state.daggerGraphState + .get(DaggerGraphState::walletManagersFacade) + .getOrCreateWalletManager( + userWallet = action.userWallet, + blockchain = blockchain, + derivationPath = blockchain.derivationPath( + style = action.userWallet.scanResponse.derivationStyleProvider.getDerivationStyle(), + ), + ) + + if (walletManager !is EthereumWalletManager) { + store.dispatchDebugErrorNotification("Testnet tokens available only for the Ethereum") + return@launch + } + + buyErc20TestnetTokens( + card = action.userWallet.scanResponse.card, + walletManager = walletManager, + destinationAddress = currency.contractAddress, + ) + } + return + } + + val exchangeManager = store.state.globalState.exchangeManager + exchangeManager.getUrl( + action = CurrencyExchangeManager.Action.Buy, + blockchain = blockchain, + cryptoCurrencyName = currency.symbol, + fiatCurrencyName = action.appCurrencyCode, + walletAddress = networkAddress, + )?.let { + store.dispatchOpenUrl(it) + Analytics.send(Token.Topup.ScreenOpened()) + } + } + private fun proceedSellAction() { val selectedWalletData = store.state.walletState.selectedWalletData ?: return val currency = chooseAppropriateCurrency(store.state.walletState) ?: return @@ -115,6 +178,22 @@ class TradeCryptoMiddleware { } } + private fun proceedNewSellAction(action: TradeCryptoAction.New.Sell) { + val networkAddress = action.cryptoCurrencyStatus.value.networkAddress?.defaultAddress ?: return + val currency = action.cryptoCurrencyStatus.currency + + store.state.globalState.exchangeManager.getUrl( + action = CurrencyExchangeManager.Action.Sell, + blockchain = Blockchain.fromId(currency.network.id.value), + cryptoCurrencyName = currency.symbol, + fiatCurrencyName = action.appCurrencyCode, + walletAddress = networkAddress, + )?.let { + store.dispatchOpenUrl(it) + Analytics.send(Token.Withdraw.ScreenOpened()) + } + } + private fun chooseAppropriateCurrency(walletState: WalletState): Currency? { return if (walletState.primaryTokenData == null) { walletState.selectedWalletData?.currency @@ -126,7 +205,7 @@ class TradeCryptoMiddleware { } } - private fun preconfigureAndOpenSendScreen(action: WalletAction.TradeCryptoAction.SendCrypto) { + private fun preconfigureAndOpenSendScreen(action: TradeCryptoAction.SendCrypto) { val selectedWalletData = store.state.walletState.selectedWalletData ?: return Analytics.send(Token.ButtonSend(AnalyticsParam.CurrencyType.Currency(selectedWalletData.currency))) @@ -160,16 +239,44 @@ class TradeCryptoMiddleware { )?.let { store.dispatchOpenUrl(it) } } - private fun openSwap() { - val currency = store.state.walletState.selectedWalletData?.currency?.toSwapCurrency() - val bundle = - bundleOf( - SwapFragment.CURRENCY_BUNDLE_KEY to Json.encodeToString(currency), - SwapFragment.DERIVATION_PATH to store.state.walletState.selectedWalletData?.currency?.derivationPath, - ) + private fun openSwap(currency: SwapCurrency?) { + val bundle = bundleOf( + SwapFragment.CURRENCY_BUNDLE_KEY to Json.encodeToString(currency), + SwapFragment.DERIVATION_PATH to store.state.walletState.selectedWalletData?.currency?.derivationPath, + ) + store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Swap, bundle = bundle)) } + private fun CryptoCurrency.toSwapCurrency(): SwapCurrency { + val blockchain = Blockchain.fromId(network.id.value) + + return when (this) { + is CryptoCurrency.Coin -> { + SwapCurrency.NativeToken( + id = blockchain.toCoinId(), + name = name, + symbol = symbol, + networkId = blockchain.toNetworkId(), + // no need to set logoUrl for blockchain cause + // error when form url with coinId, coinId of eth and arbitrum the same + logoUrl = "", + ) + } + is CryptoCurrency.Token -> { + SwapCurrency.NonNativeToken( + id = id.value, + name = name, + symbol = symbol, + networkId = blockchain.toNetworkId(), + logoUrl = getIconUrl(id.value), + contractAddress = contractAddress, + decimalCount = decimals, + ) + } + } + } + private fun Currency.toSwapCurrency(): SwapCurrency { return when (this) { is Currency.Blockchain -> { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt index c84ac7fa2d..a6a5fa0e78 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt @@ -12,6 +12,7 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.datasource.connection.NetworkConnectionManager +import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.userwallets.GetCardImageUseCase import com.tangem.domain.wallets.legacy.lockIfLockable import com.tangem.tap.* @@ -87,7 +88,7 @@ class WalletMiddleware { val walletState = store.state.walletState when (action) { - is WalletAction.TradeCryptoAction -> tradeCryptoMiddleware.handle(state, action) + is TradeCryptoAction -> tradeCryptoMiddleware.handle(state, action) is WalletAction.Warnings -> warningsMiddleware.handle(action, globalState) is WalletAction.MultiWallet -> multiWalletMiddleware.handle(action, walletState) is WalletAction.AppCurrencyAction -> appCurrencyMiddleware.handle(action) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt index e91e0a1194..3fb8a17540 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt @@ -68,7 +68,6 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS error = null, ) } - is WalletAction.TradeCryptoAction -> return newState is WalletAction.AppCurrencyAction -> { newState = appCurrencyReducer.reduce(action, newState) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt index f515410168..94e15e9e65 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt @@ -21,6 +21,7 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.derivationStyleProvider +import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.feature.swap.api.SwapFeatureToggleManager import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.sdk.extensions.dpToPx @@ -276,9 +277,9 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), SafeSt ) { val exchangeManager = store.state.globalState.exchangeManager binding.rowButtons.apply { - onBuyClick = { store.dispatch(WalletAction.TradeCryptoAction.Buy()) } - onSellClick = { store.dispatch(WalletAction.TradeCryptoAction.Sell) } - onSwapClick = { store.dispatch(WalletAction.TradeCryptoAction.Swap) } + onBuyClick = { store.dispatch(TradeCryptoAction.Buy()) } + onSellClick = { store.dispatch(TradeCryptoAction.Sell) } + onSwapClick = { store.dispatch(TradeCryptoAction.Swap) } onTradeClick = { store.dispatch( WalletAction.DialogAction.ChooseTradeActionDialog( diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ChooseTradeActionBottomSheetDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ChooseTradeActionBottomSheetDialog.kt index 1495219c54..25ce55e44c 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ChooseTradeActionBottomSheetDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ChooseTradeActionBottomSheetDialog.kt @@ -4,6 +4,7 @@ import android.content.Context import android.os.Bundle import android.view.LayoutInflater import com.google.android.material.bottomsheet.BottomSheetDialog +import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.tap.common.extensions.show import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.models.WalletDialog @@ -39,15 +40,15 @@ class ChooseTradeActionBottomSheetDialog( dialogBtnBuy.setOnClickListener { dismiss() - store.dispatch(WalletAction.TradeCryptoAction.Buy()) + store.dispatch(TradeCryptoAction.Buy()) } dialogBtnSell.setOnClickListener { dismiss() - store.dispatch(WalletAction.TradeCryptoAction.Sell) + store.dispatch(TradeCryptoAction.Sell) } dialogBtnSwap.setOnClickListener { dismiss() - store.dispatch(WalletAction.TradeCryptoAction.Swap) + store.dispatch(TradeCryptoAction.Swap) } } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/RussianCardholdersWarningBottomSheetDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/RussianCardholdersWarningBottomSheetDialog.kt index b7ec4ee4a1..185c5e366a 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/RussianCardholdersWarningBottomSheetDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/RussianCardholdersWarningBottomSheetDialog.kt @@ -6,10 +6,10 @@ import android.view.LayoutInflater import com.google.android.material.bottomsheet.BottomSheetDialog import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.NavigationAction +import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.extensions.dispatchDialogHide import com.tangem.tap.common.extensions.dispatchOpenUrl -import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.models.WalletDialog import com.tangem.tap.store import com.tangem.wallet.databinding.DialogRussiansCardholdersWarningBinding @@ -40,7 +40,7 @@ class RussianCardholdersWarningBottomSheetDialog( if (dialogData != null) { store.dispatchOpenUrl(dialogData.topUpUrl) } else { - store.dispatch(WalletAction.TradeCryptoAction.Buy(checkUserLocation = false)) + store.dispatch(TradeCryptoAction.Buy(checkUserLocation = false)) } dismiss() } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt index 23a4645bb2..7ce1a4d49e 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt @@ -4,6 +4,7 @@ import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.LinearLayoutManager import com.tangem.core.analytics.Analytics +import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.extensions.* import com.tangem.tap.domain.model.WalletDataModel @@ -151,9 +152,9 @@ class SingleWalletView : WalletView() { val exchangeManager = store.state.globalState.exchangeManager binding?.rowButtons?.apply { - onBuyClick = { store.dispatch(WalletAction.TradeCryptoAction.Buy()) } - onSellClick = { store.dispatch(WalletAction.TradeCryptoAction.Sell) } - onSwapClick = { store.dispatch(WalletAction.TradeCryptoAction.Swap) } + onBuyClick = { store.dispatch(TradeCryptoAction.Buy()) } + onSellClick = { store.dispatch(TradeCryptoAction.Sell) } + onSwapClick = { store.dispatch(TradeCryptoAction.Swap) } onTradeClick = { store.dispatch( WalletAction.DialogAction.ChooseTradeActionDialog( diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt index d3294b1ed2..ad03f2c04a 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt @@ -5,7 +5,6 @@ import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result import com.tangem.domain.models.scan.CardDTO @@ -87,17 +86,12 @@ class CurrencyExchangeManager( } } -suspend fun buyErc20TestnetTokens(card: CardDTO, walletManager: EthereumWalletManager, token: Token) { +suspend fun buyErc20TestnetTokens(card: CardDTO, walletManager: EthereumWalletManager, destinationAddress: String) { walletManager.safeUpdate() val amountToSend = Amount(walletManager.wallet.blockchain) - val destinationAddress = token.contractAddress - - val feeResult = walletManager.getFee( - amountToSend, - destinationAddress, - ) as? Result.Success ?: return + val feeResult = walletManager.getFee(amountToSend, destinationAddress) as? Result.Success ?: return val fee = when (val feeForTx = feeResult.data) { is TransactionFee.Choosable -> feeForTx.minimum is TransactionFee.Single -> feeForTx.normal @@ -106,8 +100,6 @@ suspend fun buyErc20TestnetTokens(card: CardDTO, walletManager: EthereumWalletMa val coinValue = walletManager.wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO if (coinValue < fee.amount.value) return - val transaction = walletManager.createTransaction(amountToSend, fee, destinationAddress) - val signer = TangemSigner( card = card, tangemSdk = store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).sdk, @@ -121,5 +113,13 @@ suspend fun buyErc20TestnetTokens(card: CardDTO, walletManager: EthereumWalletMa ), ) } - walletManager.send(transaction, signer) + + walletManager.send( + transactionData = walletManager.createTransaction( + amount = amountToSend, + fee = fee, + destination = destinationAddress, + ), + signer = signer, + ) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt index 8ea80aec87..b89e7cbe68 100644 --- a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt +++ b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt @@ -4,6 +4,7 @@ import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.NavigationStateHolder import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.tap.common.entities.FiatCurrency @@ -14,6 +15,7 @@ import com.tangem.tap.domain.walletStores.WalletStoresManager import com.tangem.tap.features.wallet.redux.WalletState import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import org.rekotlin.Action import org.rekotlin.Store import javax.inject.Inject @@ -21,7 +23,7 @@ import javax.inject.Inject * Holds objects from old modules, that missing in DI graph. * Object sets manually to use in new modules and [AppStateHolder] proxies its to DI. */ -class AppStateHolder @Inject constructor() : WalletsStateHolder, NavigationStateHolder { +class AppStateHolder @Inject constructor() : WalletsStateHolder, NavigationStateHolder, ReduxStateHolder { override var userWalletsListManager: UserWalletsListManager? = null set(value) { @@ -50,4 +52,8 @@ class AppStateHolder @Inject constructor() : WalletsStateHolder, NavigationState override fun navigate(action: NavigationAction) { mainStore?.dispatch(action) } + + override fun dispatch(action: Action) { + mainStore?.dispatch(action) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index b9be4b9f93..c677f70300 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -6,6 +6,7 @@ import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles import com.tangem.features.tokendetails.navigation.TokenDetailsRouter @@ -33,6 +34,7 @@ data class DaggerGraphState( val scanCardProcessor: ScanCardProcessor? = null, val cardSdkConfigRepository: CardSdkConfigRepository? = null, val appCurrencyRepository: AppCurrencyRepository? = null, + val walletManagersFacade: WalletManagersFacade? = null, ) : StateType { inline fun get(getDependency: DaggerGraphState.() -> T?): T { diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt new file mode 100644 index 0000000000..4d43b6da00 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.redux + +import org.rekotlin.Action + +interface ReduxStateHolder { + + fun dispatch(action: Action) +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index e119f09792..deb238337d 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -168,7 +168,7 @@ class DefaultWalletManagersFacade( } } - private suspend fun getOrCreateWalletManager( + override suspend fun getOrCreateWalletManager( userWallet: UserWallet, blockchain: Blockchain, derivationPath: DerivationPath?, diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index c923ac2c76..4c296b01e4 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -1,11 +1,15 @@ package com.tangem.domain.walletmanager +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.WalletManager +import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.Network import com.tangem.domain.txhistory.models.PaginationWrapper import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryState import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId // TODO: Move to its own module @@ -60,4 +64,10 @@ interface WalletManagersFacade { page: Int, pageSize: Int, ): PaginationWrapper + + suspend fun getOrCreateWalletManager( + userWallet: UserWallet, + blockchain: Blockchain, + derivationPath: DerivationPath?, + ): WalletManager? } \ No newline at end of file diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index 1ca46ccf7a..6fae380259 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -7,6 +7,7 @@ dependencies { /** Project - Domain */ implementation(projects.domain.core) + implementation(projects.domain.models) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) implementation(projects.domain.appCurrency.models) @@ -16,6 +17,7 @@ dependencies { /** Utils */ implementation(deps.jodatime) + implementation(deps.reKotlin) /** Tests */ testImplementation(deps.test.junit) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt new file mode 100644 index 0000000000..3ef7c99d04 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt @@ -0,0 +1,43 @@ +package com.tangem.domain.tokens.legacy + +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.wallets.models.UserWallet +import org.rekotlin.Action + +sealed class TradeCryptoAction : Action { + + data class Buy(val checkUserLocation: Boolean = true) : TradeCryptoAction() + + object Sell : TradeCryptoAction() + + data class SendCrypto( + val currencyId: String, + val amount: String, + val destinationAddress: String, + val transactionId: String, + ) : TradeCryptoAction() + + data class FinishSelling(val transactionId: String) : TradeCryptoAction() + + object Swap : TradeCryptoAction() + + sealed class New : TradeCryptoAction() { + + data class Buy( + val userWallet: UserWallet, + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val appCurrencyCode: String, + val checkUserLocation: Boolean = true, + ) : New() + + data class Sell( + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val appCurrencyCode: String, + ) : New() + + object Send : New() + + data class Swap(val cryptoCurrency: CryptoCurrency) : New() + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt index 9c167104ee..b71f6496b5 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt @@ -40,6 +40,9 @@ data class CryptoCurrencyStatus( /** The pending cryptocurrency transactions. */ open val pendingTransactions: Set = emptySet() + + /** The network address */ + open val networkAddress: NetworkAddress? = null } /** Represents the Loading state of a token, typically while fetching its details. */ @@ -72,6 +75,7 @@ data class CryptoCurrencyStatus( override val priceChange: BigDecimal, override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, + override val networkAddress: NetworkAddress?, ) : Status() /** @@ -92,6 +96,7 @@ data class CryptoCurrencyStatus( override val priceChange: BigDecimal?, override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, + override val networkAddress: NetworkAddress?, ) : Status() /** @@ -106,5 +111,6 @@ data class CryptoCurrencyStatus( override val amount: BigDecimal, override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, + override val networkAddress: NetworkAddress?, ) : Status() } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt index 3498bb05e3..2082010b8d 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt @@ -35,6 +35,7 @@ internal class CurrencyStatusOperations( amount = amount, hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, pendingTransactions = currentTransactions, + networkAddress = status.address, ) currency is CryptoCurrency.Token && currency.isCustom -> CryptoCurrencyStatus.Custom( amount = amount, @@ -43,6 +44,7 @@ internal class CurrencyStatusOperations( priceChange = quote?.priceChange, hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, pendingTransactions = currentTransactions, + networkAddress = status.address, ) quote == null -> CryptoCurrencyStatus.Loading else -> CryptoCurrencyStatus.Loaded( @@ -52,6 +54,7 @@ internal class CurrencyStatusOperations( priceChange = quote.priceChange, hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, pendingTransactions = currentTransactions, + networkAddress = status.address, ) } } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index aa477fee0c..d01254b1e7 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -85,16 +85,22 @@ internal object MockTokensStates { priceChange = quote.priceChange, pendingTransactions = emptySet(), hasCurrentNetworkTransactions = false, + networkAddress = requireNotNull(networkStatus.value as? NetworkStatus.Verified).address, ), ) } - val noQuotesTokensStatuses = loadedTokensStates.map { currency -> - currency.copy( + val noQuotesTokensStatuses = loadedTokensStates.map { status -> + status.copy( value = CryptoCurrencyStatus.NoQuote( - amount = currency.value.amount!!, + amount = status.value.amount!!, pendingTransactions = emptySet(), hasCurrentNetworkTransactions = false, + networkAddress = requireNotNull( + value = MockNetworks.verifiedNetworksStatuses + .first { it.networkId == status.currency.network.id } + .value as? NetworkStatus.Verified, + ).address, ), ) } diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 48a2eca7c6..0e84af12c3 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { implementation(deps.arrow.core) implementation(deps.jodatime) implementation(deps.kotlin.immutable.collections) + implementation(deps.reKotlin) implementation(deps.tangem.card.core) implementation(deps.tangem.blockchain) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletCryptoCurrencyActionsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletCryptoCurrencyActionsConverter.kt index ea88f686dd..597680d5eb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletCryptoCurrencyActionsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletCryptoCurrencyActionsConverter.kt @@ -40,7 +40,7 @@ internal class WalletCryptoCurrencyActionsConverter( WalletManageButton.Sell(enabled = action.enabled, onClick = clickIntents::onSellClick) } is TokenActionsState.ActionState.Send -> { - WalletManageButton.Send(enabled = action.enabled, onClick = clickIntents::onSellClick) + WalletManageButton.Send(enabled = action.enabled, onClick = clickIntents::onSendClick) } is TokenActionsState.ActionState.Swap -> null } 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 d27c5d8ba9..288a73f73e 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 @@ -16,10 +16,13 @@ import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.settings.IsUserAlreadyRateAppUseCase import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase import com.tangem.domain.tokens.GetPrimaryCurrencyUseCase import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.tokens.legacy.TradeCryptoAction +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.Network @@ -78,6 +81,7 @@ internal class WalletViewModel @Inject constructor( private val unlockWalletsUseCase: UnlockWalletsUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, + private val reduxStateHolder: ReduxStateHolder, private val dispatchers: CoroutineDispatcherProvider, ) : ViewModel(), DefaultLifecycleObserver, WalletClickIntents { @@ -111,6 +115,7 @@ internal class WalletViewModel @Inject constructor( var uiState: WalletState by uiStateHolder(initialState = stateFactory.getInitialState()) private var wallets: List by Delegates.notNull() + private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null private val tokensJobHolder = JobHolder() private val marketPriceJobHolder = JobHolder() @@ -219,11 +224,13 @@ internal class WalletViewModel @Inject constructor( private fun updateMarketPrice(userWalletId: UserWalletId, isRefreshing: Boolean) { getPrimaryCurrencyUseCase(userWalletId = userWalletId) .distinctUntilChanged() - .onEach { + .onEach { either -> uiState = stateFactory.getSingleCurrencyLoadedBalanceState( - cryptoCurrencyEither = it, + cryptoCurrencyEither = either, isRefreshing = isRefreshing, ) + + either.onRight { status -> cryptoCurrencyStatus = status } } .flowOn(dispatchers.io) .launchIn(viewModelScope) @@ -422,11 +429,21 @@ internal class WalletViewModel @Inject constructor( } override fun onBuyClick() { - // TODO: [REDACTED_JIRA] + val state = uiState as? WalletState.ContentState ?: return + val status = cryptoCurrencyStatus ?: return + val wallet = getWallet(index = state.walletsListConfig.selectedWalletIndex) + + reduxStateHolder.dispatch( + TradeCryptoAction.New.Buy( + userWallet = wallet, + cryptoCurrencyStatus = status, + appCurrencyCode = selectedAppCurrencyFlow.value.code, + ), + ) } override fun onSendClick() { - // TODO: [REDACTED_JIRA] + reduxStateHolder.dispatch(TradeCryptoAction.New.Send) } override fun onReceiveClick() { @@ -434,7 +451,14 @@ internal class WalletViewModel @Inject constructor( } override fun onSellClick() { - // TODO: [REDACTED_JIRA] + val status = cryptoCurrencyStatus ?: return + + reduxStateHolder.dispatch( + TradeCryptoAction.New.Sell( + cryptoCurrencyStatus = status, + appCurrencyCode = selectedAppCurrencyFlow.value.code, + ), + ) } override fun onReloadClick() { From 23256d93db9a215006f0fa77af2c7d77ddc66937 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 25 Aug 2023 17:36:54 +0500 Subject: [PATCH 24/44] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 1 + .../ui/components/transactions/Transaction.kt | 24 +++++++++-------- .../transactions/state/TransactionState.kt | 26 ++++++++++--------- .../SdkTransactionHistoryItemConverter.kt | 19 +++++++++----- .../domain/txhistory/models/TxHistoryItem.kt | 12 +++++++-- .../TokenDetailsTxHistoryItemFlowConverter.kt | 15 ++++++++--- .../presentation/common/WalletPreviewData.kt | 4 +-- .../WalletTxHistoryItemFlowConverter.kt | 15 ++++++++--- gradle/dependencies.toml | 2 +- 10 files changed, 77 insertions(+), 42 deletions(-) diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 820055db8d..95a129cac5 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -460,6 +460,7 @@ контракт: %s У вас еще нет транзакций Не удалось загрузить историю транзакций.\nНажмите на кнопку перезагрузки, чтобы обновить информацию. + Несколько адресов История транзакций в настоящее время не поддерживается для этого блокчейна. Но не волнуйтесь, мы работаем над этим! А пока вы можете проверить ее в обозревателе. от: %s на: %s diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 755f6928a3..86e75e5d4d 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -451,6 +451,7 @@ contract: %s You don\'t have any transactions yet Failed to load transaction history.\nClick on reload button to update the information. + Multiple addresses Transaction history is currently not supported for this blockchain. But don\'t worry, we\'re working on it! In the meantime you can check it in the explorer. from: %s to: %s diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt index f31203179d..b3622b4b1b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt @@ -23,6 +23,8 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.transactions.state.TransactionState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import java.util.UUID @@ -237,7 +239,7 @@ private fun Subtitle(state: TransactionState, modifier: Modifier = Modifier) { is TransactionState.Send, -> stringResource( id = R.string.transaction_history_transaction_to_address, - state.address, + state.address.resolveReference(), ) is TransactionState.Receiving, is TransactionState.Receive, @@ -245,13 +247,13 @@ private fun Subtitle(state: TransactionState, modifier: Modifier = Modifier) { is TransactionState.Approved, -> stringResource( id = R.string.transaction_history_transaction_from_address, - state.address, + state.address.resolveReference(), ) is TransactionState.Swapping, is TransactionState.Swapped, -> stringResource( id = R.string.transaction_history_contract_address, - state.address, + state.address.resolveReference(), ) }, modifier = modifier, @@ -357,49 +359,49 @@ private class TransactionItemStateProvider : CollectionPreviewParameterProvider< collection = listOf( TransactionState.Sending( txHash = UUID.randomUUID().toString(), - address = "33BddS...ga2B", + address = TextReference.Str("33BddS...ga2B"), amount = "-0.500913 BTC", timestamp = "8:41", ), TransactionState.Receiving( txHash = UUID.randomUUID().toString(), - address = "33BddS...ga2B", + address = TextReference.Str("33BddS...ga2B"), amount = "+0.500913 BTC", timestamp = "8:41", ), TransactionState.Approving( txHash = UUID.randomUUID().toString(), - address = "33BddS...ga2B", + address = TextReference.Str("33BddS...ga2B"), amount = "+0.500913 BTC", timestamp = "8:41", ), TransactionState.Swapping( txHash = UUID.randomUUID().toString(), - address = "33BddS...ga2B", + address = TextReference.Str("33BddS...ga2B"), amount = "+0.500913 BTC", timestamp = "8:41", ), TransactionState.Send( txHash = UUID.randomUUID().toString(), - address = "33BddS...ga2B", + address = TextReference.Str("33BddS...ga2B"), amount = "-0.500913 BTC", timestamp = "8:41", ), TransactionState.Receive( txHash = UUID.randomUUID().toString(), - address = "33BddS...ga2B", + address = TextReference.Str("33BddS...ga2B"), amount = "+0.500913 BTC", timestamp = "8:41", ), TransactionState.Approved( txHash = UUID.randomUUID().toString(), - address = "33BddS...ga2B", + address = TextReference.Str("33BddS...ga2B"), amount = "+0.500913 BTC", timestamp = "8:41", ), TransactionState.Swapped( txHash = UUID.randomUUID().toString(), - address = "33BddS...ga2B", + address = TextReference.Str("33BddS...ga2B"), amount = "+0.500913 BTC", timestamp = "8:41", ), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionState.kt index 05e969d505..8178e83e61 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionState.kt @@ -1,5 +1,7 @@ package com.tangem.core.ui.components.transactions.state +import com.tangem.core.ui.extensions.TextReference + /** * Transaction component state * @@ -20,14 +22,14 @@ sealed interface TransactionState { */ sealed class Content( override val txHash: String, - open val address: String, + open val address: TextReference, open val amount: String, open val timestamp: String, ) : TransactionState { fun copySealed( txHash: String = this.txHash, - address: String = this.address, + address: TextReference = this.address, amount: String = this.amount, timestamp: String = this.timestamp, ): Content { @@ -54,7 +56,7 @@ sealed interface TransactionState { */ sealed class ProcessedTransactionContent( override val txHash: String, - override val address: String, + override val address: TextReference, override val amount: String, override val timestamp: String, ) : Content(txHash, address, amount, timestamp) @@ -69,7 +71,7 @@ sealed interface TransactionState { */ sealed class CompletedTransactionContent( override val txHash: String, - override val address: String, + override val address: TextReference, override val amount: String, override val timestamp: String, ) : Content(txHash, address, amount, timestamp) @@ -84,7 +86,7 @@ sealed interface TransactionState { */ data class Sending( override val txHash: String, - override val address: String, + override val address: TextReference, override val amount: String, override val timestamp: String, ) : ProcessedTransactionContent(txHash, address, amount, timestamp) @@ -99,7 +101,7 @@ sealed interface TransactionState { */ data class Receiving( override val txHash: String, - override val address: String, + override val address: TextReference, override val amount: String, override val timestamp: String, ) : ProcessedTransactionContent(txHash, address, amount, timestamp) @@ -114,7 +116,7 @@ sealed interface TransactionState { */ data class Approving( override val txHash: String, - override val address: String, + override val address: TextReference, override val amount: String, override val timestamp: String, ) : ProcessedTransactionContent(txHash, address, amount, timestamp) @@ -129,7 +131,7 @@ sealed interface TransactionState { */ data class Swapping( override val txHash: String, - override val address: String, + override val address: TextReference, override val amount: String, override val timestamp: String, ) : ProcessedTransactionContent(txHash, address, amount, timestamp) @@ -144,7 +146,7 @@ sealed interface TransactionState { */ data class Send( override val txHash: String, - override val address: String, + override val address: TextReference, override val amount: String, override val timestamp: String, ) : CompletedTransactionContent(txHash, address, amount, timestamp) @@ -159,7 +161,7 @@ sealed interface TransactionState { */ data class Receive( override val txHash: String, - override val address: String, + override val address: TextReference, override val amount: String, override val timestamp: String, ) : CompletedTransactionContent(txHash, address, amount, timestamp) @@ -174,7 +176,7 @@ sealed interface TransactionState { */ data class Approved( override val txHash: String, - override val address: String, + override val address: TextReference, override val amount: String, override val timestamp: String, ) : CompletedTransactionContent(txHash, address, amount, timestamp) @@ -189,7 +191,7 @@ sealed interface TransactionState { */ data class Swapped( override val txHash: String, - override val address: String, + override val address: TextReference, override val amount: String, override val timestamp: String, ) : CompletedTransactionContent(txHash, address, amount, timestamp) diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryItemConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryItemConverter.kt index 8df6e63c54..df57ff2349 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryItemConverter.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryItemConverter.kt @@ -11,12 +11,7 @@ internal class SdkTransactionHistoryItemConverter : Converter - TxHistoryItem.TransactionDirection.Incoming(direction.from) - is SdkTransactionHistoryItem.TransactionDirection.Outgoing -> - TxHistoryItem.TransactionDirection.Outgoing(direction.to) - }, + direction = value.direction.toDomain(), status = when (value.status) { TransactionStatus.Confirmed -> TxHistoryItem.TxStatus.Confirmed TransactionStatus.Unconfirmed -> TxHistoryItem.TxStatus.Unconfirmed @@ -26,4 +21,16 @@ internal class SdkTransactionHistoryItemConverter : Converter + TxHistoryItem.TransactionDirection.Incoming(address.toDomain()) + is TransactionHistoryItem.TransactionDirection.Outgoing -> + TxHistoryItem.TransactionDirection.Outgoing(address.toDomain()) + } + + private fun TransactionHistoryItem.Address.toDomain(): TxHistoryItem.Address = when (this) { + TransactionHistoryItem.Address.Multiple -> TxHistoryItem.Address.Multiple + is TransactionHistoryItem.Address.Single -> TxHistoryItem.Address.Single(rawAddress = rawAddress) + } } \ No newline at end of file diff --git a/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryItem.kt b/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryItem.kt index 447a9395ff..c17f0dccb6 100644 --- a/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryItem.kt +++ b/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryItem.kt @@ -11,8 +11,11 @@ data class TxHistoryItem( val amount: BigDecimal, ) { sealed interface TransactionDirection { - data class Incoming(val from: String) : TransactionDirection - data class Outgoing(val to: String) : TransactionDirection + + val address: Address + + data class Incoming(override val address: Address) : TransactionDirection + data class Outgoing(override val address: Address) : TransactionDirection } sealed interface TransactionType { @@ -20,4 +23,9 @@ data class TxHistoryItem( } enum class TxStatus { Confirmed, Unconfirmed } + + sealed class Address { + data class Single(val rawAddress: String) : Address() + object Multiple : Address() + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt index eafd6c9a31..a8f2d95f25 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt @@ -7,8 +7,10 @@ import com.tangem.core.ui.components.transactions.intents.TxHistoryClickIntents import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.features.tokendetails.impl.R import com.tangem.utils.converter.Converter import com.tangem.utils.extensions.isToday import com.tangem.utils.extensions.isYesterday @@ -100,13 +102,13 @@ internal class TokenDetailsTxHistoryItemFlowConverter( return when (item.status) { TxHistoryItem.TxStatus.Confirmed -> TransactionState.Receive( txHash = item.txHash, - address = direction.from.toBriefAddressFormat(), + address = direction.extractAddress(), amount = item.amount.toCryptoCurrencyFormat(), timestamp = item.getRawTimestamp(), ) TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Receiving( txHash = item.txHash, - address = direction.from.toBriefAddressFormat(), + address = direction.extractAddress(), amount = item.amount.toCryptoCurrencyFormat(), timestamp = item.getRawTimestamp(), ) @@ -120,13 +122,13 @@ internal class TokenDetailsTxHistoryItemFlowConverter( return when (item.status) { TxHistoryItem.TxStatus.Confirmed -> TransactionState.Send( txHash = item.txHash, - address = direction.to.toBriefAddressFormat(), + address = direction.extractAddress(), amount = item.amount.toCryptoCurrencyFormat(), timestamp = item.getRawTimestamp(), ) TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Sending( txHash = item.txHash, - address = direction.to.toBriefAddressFormat(), + address = direction.extractAddress(), amount = item.amount.toCryptoCurrencyFormat(), timestamp = item.getRawTimestamp(), ) @@ -220,4 +222,9 @@ internal class TokenDetailsTxHistoryItemFlowConverter( DateTime(this.toLong(), DateTimeZone.getDefault()), ) } + + private fun TxHistoryItem.TransactionDirection.extractAddress(): TextReference = when (val addr = address) { + TxHistoryItem.Address.Multiple -> TextReference.Res(R.string.transaction_history_multiple_addresses) + is TxHistoryItem.Address.Single -> TextReference.Str(addr.rawAddress.toBriefAddressFormat()) + } } \ 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 bf75186348..1e70c05083 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 @@ -387,7 +387,7 @@ internal object WalletPreviewData { TxHistoryState.TxHistoryItemState.Transaction( TransactionState.Sending( txHash = UUID.randomUUID().toString(), - address = "33BddS...ga2B", + address = TextReference.Str("33BddS...ga2B"), amount = "-0.500913 BTC", timestamp = "8:41", ), @@ -396,7 +396,7 @@ internal object WalletPreviewData { TxHistoryState.TxHistoryItemState.Transaction( TransactionState.Sending( txHash = UUID.randomUUID().toString(), - address = "33BddS...ga2B", + address = TextReference.Str("33BddS...ga2B"), amount = "-0.500913 BTC", timestamp = "8:41", ), 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 fae55f6b5b..5e4a05132f 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 @@ -7,8 +7,10 @@ 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.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.feature.wallet.impl.R 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 @@ -115,13 +117,13 @@ internal class WalletTxHistoryItemFlowConverter( return when (item.status) { TxHistoryItem.TxStatus.Confirmed -> TransactionState.Receive( txHash = item.txHash, - address = direction.from.toBriefAddressFormat(), + address = direction.extractAddress(), amount = item.amount.toCryptoCurrencyFormat(blockchain = blockchain), timestamp = item.getRawTimestamp(), ) TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Receiving( txHash = item.txHash, - address = direction.from.toBriefAddressFormat(), + address = direction.extractAddress(), amount = item.amount.toCryptoCurrencyFormat(blockchain = blockchain), timestamp = item.getRawTimestamp(), ) @@ -136,13 +138,13 @@ internal class WalletTxHistoryItemFlowConverter( return when (item.status) { TxHistoryItem.TxStatus.Confirmed -> TransactionState.Send( txHash = item.txHash, - address = direction.to.toBriefAddressFormat(), + address = direction.extractAddress(), amount = item.amount.toCryptoCurrencyFormat(blockchain = blockchain), timestamp = item.getRawTimestamp(), ) TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Sending( txHash = item.txHash, - address = direction.to.toBriefAddressFormat(), + address = direction.extractAddress(), amount = item.amount.toCryptoCurrencyFormat(blockchain = blockchain), timestamp = item.getRawTimestamp(), ) @@ -240,4 +242,9 @@ internal class WalletTxHistoryItemFlowConverter( DateTime(this.toLong(), DateTimeZone.getDefault()), ) } + + private fun TxHistoryItem.TransactionDirection.extractAddress(): TextReference = when (val addr = address) { + TxHistoryItem.Address.Multiple -> TextReference.Res(R.string.transaction_history_multiple_addresses) + is TxHistoryItem.Address.Single -> TextReference.Str(addr.rawAddress.toBriefAddressFormat()) + } } \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 04b0bc5086..2ebbd8a165 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -80,7 +80,7 @@ okHttp-prettyLogging = "3.1.0" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-327" +tangemBlockchainSdk = "develop-330" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-289" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds From 37046075989018fc82917430599aa044b1b39d85 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 29 Aug 2023 08:17:41 +0300 Subject: [PATCH 25/44] Updated on 2026-08-14 --- .../products/note/OnboardingNoteFragment.kt | 6 +++++ .../OnboardingOtherCardsFragment.kt | 2 ++ .../products/twins/ui/TwinsCardsFragment.kt | 20 +++++++++++--- .../wallet/ui/OnboardingWalletFragment.kt | 8 ++++++ ...y.xml => selector_btn_content_primary.xml} | 0 ...xml => selector_btn_content_secondary.xml} | 0 .../layout_onboarding_container_bottom.xml | 3 ++- app/src/main/res/layout/fragment_send.xml | 2 +- .../layout_onboarding_buttons_add_cards.xml | 5 ++-- .../layout_onboarding_buttons_common.xml | 3 ++- .../layout_onboarding_container_bottom.xml | 3 ++- .../view_wallet_details_buttons_row.xml | 10 +++---- app/src/main/res/values/styles.xml | 26 +++++++++++++------ .../wallet2/ui/CheckSeedPhraseScreen.kt | 4 +-- .../wallet2/ui/ImportSeedPhraseScreen.kt | 5 ++-- .../presentation/wallet2/ui/IntroScreen.kt | 4 +-- 16 files changed, 72 insertions(+), 29 deletions(-) rename app/src/main/res/color/{selector_btn_text_primary.xml => selector_btn_content_primary.xml} (100%) rename app/src/main/res/color/{selector_btn_text_secondary.xml => selector_btn_content_secondary.xml} (100%) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt index 6cdb83e4d7..7ac3806126 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt @@ -103,13 +103,16 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { tvBody.isVisible = false btnMainAction.text = "" + btnMainAction.icon = null btnAlternativeAction.text = "" + btnAlternativeAction.icon = null tvHeader.text = "" tvBody.text = "" } private fun setupCreateWalletState(state: OnboardingNoteState) = with(mainBinding.onboardingActionContainer) { btnMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet) + btnMainAction.setIconResource(R.drawable.ic_tangem_24) btnMainAction.setOnClickListener { Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet()) store.dispatch(OnboardingNoteAction.CreateWallet) @@ -133,6 +136,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { private fun setupTopUpWalletState(state: OnboardingNoteState) = with(mainBinding.onboardingActionContainer) { if (state.isBuyAllowed) { btnMainAction.setText(R.string.onboarding_top_up_button_but_crypto) + btnMainAction.icon = null btnMainAction.setOnClickListener { store.dispatch(OnboardingNoteAction.TopUp) } @@ -144,6 +148,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { } } else { btnMainAction.setText(R.string.onboarding_button_receive_crypto) + btnMainAction.icon = null btnMainAction.setOnClickListener { store.dispatch(OnboardingNoteAction.ShowAddressInfoDialog) } @@ -179,6 +184,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { private fun setupDoneState(state: OnboardingNoteState) = with(mainBinding.onboardingActionContainer) { btnMainAction.setText(R.string.common_continue) + btnMainAction.icon = null btnMainAction.setOnClickListener { showConfetti(false) store.dispatch(OnboardingNoteAction.Done) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt index 83674d9227..2e6ac715e0 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt @@ -72,6 +72,7 @@ class OnboardingOtherCardsFragment : BaseOnboardingFragment() { } private fun setupWelcomeState(state: TwinCardsState) { - setupWelcomeState(state) { store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateFirstWallet)) } + setupWelcomeState(state) { + store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateFirstWallet)) + } } - private fun setupWelcomeState(state: TwinCardsState, mainAction: VoidCallback) = + private fun setupWelcomeState(state: TwinCardsState, onMainButtonClick: VoidCallback) = with(mainBinding.onboardingActionContainer) { twinsWidget.toWelcome(false) @@ -192,7 +195,12 @@ class TwinsCardsFragment : BaseOnboardingFragment() { ) btnMainAction.setText(R.string.common_continue) - btnMainAction.setOnClickListener { mainAction() } + btnMainAction.icon = when (state.currentStep) { + is TwinCardsStep.WelcomeOnly -> null + is TwinCardsStep.Welcome -> AppCompatResources.getDrawable(requireContext(), R.drawable.ic_tangem_24) + else -> null + } + btnMainAction.setOnClickListener { onMainButtonClick() } } private fun setupWarningState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) { @@ -210,6 +218,7 @@ class TwinsCardsFragment : BaseOnboardingFragment() { } btnMainAction.isEnabled = state.userWasUnderstandIfWalletRecreate btnMainAction.setText(R.string.common_continue) + btnMainAction.icon = null btnMainAction.setOnClickListener { store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateFirstWallet)) } @@ -251,6 +260,7 @@ class TwinsCardsFragment : BaseOnboardingFragment() { tvBody.setText(R.string.onboarding_twins_interrupt_warning) btnMainAction.text = getString(R.string.twins_recreate_button_format, twinIndexNumber) + btnMainAction.setIconResource(R.drawable.ic_tangem_24) btnMainAction.setOnClickListener { Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet()) store.dispatch( @@ -274,6 +284,7 @@ class TwinsCardsFragment : BaseOnboardingFragment() { btnMainAction.text = getString(R.string.twins_recreate_button_format, twinPairIndexNumber) + btnMainAction.setIconResource(R.drawable.ic_tangem_24) btnMainAction.setOnClickListener { store.dispatch( TwinCardsAction.Wallet.LaunchSecondStep( @@ -298,6 +309,7 @@ class TwinsCardsFragment : BaseOnboardingFragment() { tvBody.setText(R.string.onboarding_twins_interrupt_warning) btnMainAction.text = getString(R.string.twins_recreate_button_format, twinIndexNumber) + btnMainAction.setIconResource(R.drawable.ic_tangem_24) btnMainAction.setOnClickListener { store.dispatch( TwinCardsAction.Wallet.LaunchThirdStep( @@ -343,6 +355,7 @@ class TwinsCardsFragment : BaseOnboardingFragment() { } } else { btnMainAction.setText(R.string.onboarding_button_receive_crypto) + btnMainAction.icon = null btnMainAction.setOnClickListener { store.dispatch(TwinCardsAction.ShowAddressInfoDialog) } @@ -367,6 +380,7 @@ class TwinsCardsFragment : BaseOnboardingFragment() { private fun setupDoneState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) { btnMainAction.setText(R.string.common_continue) + btnMainAction.icon = null btnMainAction.setOnClickListener { store.dispatch(TwinCardsAction.Confetti.Hide) store.dispatch(TwinCardsAction.Done) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt index 5b6afc478f..78f7d5bef0 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt @@ -205,6 +205,8 @@ class OnboardingWalletFragment : private fun setupCreateWalletState() = with(binding) { layoutButtonsCommon.btnWalletMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet) + layoutButtonsCommon.btnWalletMainAction.setIconResource(R.drawable.ic_tangem_24) + layoutButtonsCommon.btnWalletMainAction.setOnClickListener { Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet()) store.dispatch(OnboardingWalletAction.CreateWallet) @@ -244,6 +246,7 @@ class OnboardingWalletFragment : with(layoutButtonsCommon) { btnWalletMainAction.text = getText(R.string.onboarding_button_backup_now) + btnWalletMainAction.icon = null btnWalletMainAction.setOnClickListener { store.dispatch(BackupAction.StartBackup) } btnWalletAlternativeAction.text = getText(R.string.onboarding_button_skip_backup) @@ -262,6 +265,7 @@ class OnboardingWalletFragment : with(layoutButtonsCommon) { btnWalletMainAction.text = getString(R.string.onboarding_button_scan_origin_card) + btnWalletMainAction.setIconResource(R.drawable.ic_tangem_24) btnWalletAlternativeAction.hide() btnWalletMainAction.setOnClickListener { store.dispatch(BackupAction.ScanPrimaryCard) } } @@ -278,6 +282,7 @@ class OnboardingWalletFragment : layoutButtonsAddCards.root.show() layoutButtonsCommon.root.hide() layoutButtonsAddCards.btnAddCard.text = getText(R.string.onboarding_button_add_backup_card) + layoutButtonsAddCards.btnAddCard.setIconResource(R.drawable.ic_tangem_24) if (state.backupCardsNumber < state.maxBackupCards) { layoutButtonsAddCards.btnAddCard.setOnClickListener { store.dispatch(BackupAction.AddBackupCard) } } else { @@ -359,6 +364,7 @@ class OnboardingWalletFragment : state.primaryCardId?.let { cardIdFormatter.getFormattedCardId(it) }, ) layoutButtonsCommon.btnWalletMainAction.text = getText(R.string.onboarding_button_backup_origin) + layoutButtonsCommon.btnWalletMainAction.setIconResource(R.drawable.ic_tangem_24) layoutButtonsCommon.btnWalletMainAction.setOnClickListener { store.dispatch(BackupAction.WritePrimaryCard) } animator.showWritePrimaryCard(state) @@ -391,6 +397,7 @@ class OnboardingWalletFragment : R.string.onboarding_button_backup_card_format, cardNumber, ) + layoutButtonsCommon.btnWalletMainAction.setIconResource(R.drawable.ic_tangem_24) layoutButtonsCommon.btnWalletMainAction.setOnClickListener { store.dispatch(BackupAction.WriteBackupCard(cardNumber)) } @@ -409,6 +416,7 @@ class OnboardingWalletFragment : tvBody.text = getText(R.string.onboarding_subtitle_success_tangem_wallet_onboarding) layoutButtonsCommon.btnWalletMainAction.text = getText(R.string.onboarding_button_continue_wallet) + layoutButtonsCommon.btnWalletMainAction.icon = null layoutButtonsCommon.btnWalletAlternativeAction.hide() layoutButtonsCommon.btnWalletMainAction.setOnClickListener { showConfetti(false) diff --git a/app/src/main/res/color/selector_btn_text_primary.xml b/app/src/main/res/color/selector_btn_content_primary.xml similarity index 100% rename from app/src/main/res/color/selector_btn_text_primary.xml rename to app/src/main/res/color/selector_btn_content_primary.xml diff --git a/app/src/main/res/color/selector_btn_text_secondary.xml b/app/src/main/res/color/selector_btn_content_secondary.xml similarity index 100% rename from app/src/main/res/color/selector_btn_text_secondary.xml rename to app/src/main/res/color/selector_btn_content_secondary.xml diff --git a/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml b/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml index b098f5f0ae..32d2544981 100644 --- a/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml +++ b/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml @@ -74,7 +74,8 @@ + tools:icon="@drawable/ic_tangem_24" + tools:text="Add backup card" /> 48dp 0dp 0dp + 14dp false 14sp - normal - @font/roboto_medium + @font/roboto_regular 0 - 24sp - @dimen/btn_corner_radius_large + 500 + 16sp + + + + - \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index a6d3f7e6d4..70af1872c5 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,7 +1,8 @@ - - - + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 70af1872c5..a6d3f7e6d4 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,8 +1,7 @@ - - +