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 8e3d763c56..2d12ae7b0b 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 @@ -20,6 +20,12 @@ internal object WalletsDomainModule { return GetWalletsUseCase(walletsStateHolder = walletsStateHolder) } + @Provides + @ViewModelScoped + fun providesGetUserWalletUseCase(walletsStateHolder: WalletsStateHolder): GetUserWalletUseCase { + return GetUserWalletUseCase(walletsStateHolder = walletsStateHolder) + } + @Provides @ViewModelScoped fun providesGetSelectedWalletUseCase(walletsStateHolder: WalletsStateHolder): GetSelectedWalletUseCase { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt index eb198a07f9..74aaf39091 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt @@ -39,9 +39,13 @@ class MultiWalletMiddleware { when (action) { is WalletAction.MultiWallet.SelectWallet -> { if (action.currency != null) { + val userWalletId = userWalletsListManager.selectedUserWalletSync?.walletId + val bundle = bundleOf( + TokenDetailsRouter.USER_WALLET_ID_KEY to userWalletId?.stringValue, TokenDetailsRouter.CRYPTO_CURRENCY_KEY to cryptoCurrencyConverter.convert(action.currency), ) + store.dispatch(NavigationAction.NavigateTo(screen = AppScreen.WalletDetails, bundle = bundle)) } } diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 79b6816470..6d7e66c87e 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.txhistory.models) /** Tangem libraries */ implementation(deps.tangem.blockchain) diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/TxHistoryItemsStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/TxHistoryItemsStoreModule.kt new file mode 100644 index 0000000000..9a936f57f0 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/TxHistoryItemsStoreModule.kt @@ -0,0 +1,21 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.datasource.local.txhistory.DefaultTxHistoryItemsStore +import com.tangem.datasource.local.txhistory.TxHistoryItemsStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal object TxHistoryItemsStoreModule { + + @Provides + fun provideTxHistoryItemsStore(): TxHistoryItemsStore { + return DefaultTxHistoryItemsStore( + dataStore = RuntimeDataStore(), + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/DefaultTxHistoryItemsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/DefaultTxHistoryItemsStore.kt new file mode 100644 index 0000000000..7cbeb3f317 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/DefaultTxHistoryItemsStore.kt @@ -0,0 +1,42 @@ +package com.tangem.datasource.local.txhistory + +import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator +import com.tangem.domain.txhistory.models.PaginationWrapper +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.utils.extensions.addOrReplace + +internal class DefaultTxHistoryItemsStore( + dataStore: StringKeyDataStore>>, +) : TxHistoryItemsStore, + StringKeyDataStoreDecorator>>(dataStore) { + + override fun provideStringKey(key: TxHistoryItemsStore.Key): String = key.toString() + + override suspend fun getNextPageSyncOrNull(key: TxHistoryItemsStore.Key): Int? { + val storedValue = getSyncOrNull(key) ?: return null + val lastWrappedItems = storedValue.maxBy(PaginationWrapper<*>::page) + val lastPage = lastWrappedItems.page + + return if (lastPage <= lastWrappedItems.totalPages) { + lastPage + } else { + null + } + } + + override suspend fun getSyncOrNull(key: TxHistoryItemsStore.Key, page: Int): PaginationWrapper? { + val storedValue = getSyncOrNull(key) + + return storedValue?.firstOrNull { it.page == page } + } + + override suspend fun store(key: TxHistoryItemsStore.Key, value: PaginationWrapper) { + val oldValue = getSyncOrNull(key).orEmpty() + val newValue = oldValue.addOrReplace(value) { + it.page == value.page + } + + store(key, newValue) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/TxHistoryItemsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/TxHistoryItemsStore.kt new file mode 100644 index 0000000000..e2f288e235 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/TxHistoryItemsStore.kt @@ -0,0 +1,22 @@ +package com.tangem.datasource.local.txhistory + +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.txhistory.models.PaginationWrapper +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.wallets.models.UserWalletId + +interface TxHistoryItemsStore { + + suspend fun getNextPageSyncOrNull(key: Key): Int? + + suspend fun getSyncOrNull(key: Key, page: Int): PaginationWrapper? + + suspend fun remove(key: Key) + + suspend fun store(key: Key, value: PaginationWrapper) + + data class Key( + val userWalletId: UserWalletId, + val currency: CryptoCurrency, + ) +} \ No newline at end of file diff --git a/data/txhistory/build.gradle.kts b/data/txhistory/build.gradle.kts index 7ffda888cc..d53b060232 100644 --- a/data/txhistory/build.gradle.kts +++ b/data/txhistory/build.gradle.kts @@ -10,6 +10,8 @@ android { } dependencies { + implementation(projects.data.common) + implementation(projects.core.utils) implementation(projects.core.datasource) implementation(projects.domain.legacy) @@ -20,7 +22,8 @@ dependencies { implementation(deps.kotlin.coroutines) implementation(deps.androidx.paging.runtime) - implementation(deps.arrow.core) + implementation(deps.timber) + implementation(deps.jodatime) implementation(deps.hilt.core) kapt(deps.hilt.kapt) diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt index da8b08603c..27f3b8e7bb 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt @@ -1,6 +1,8 @@ package com.tangem.data.txhistory.di +import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.txhistory.repository.DefaultTxHistoryRepository +import com.tangem.datasource.local.txhistory.TxHistoryItemsStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.txhistory.repository.TxHistoryRepository import com.tangem.domain.walletmanager.WalletManagersFacade @@ -17,10 +19,14 @@ internal object TxHistoryDataModule { @Provides @Singleton fun provideTxHistoryRepository( + cacheRegistry: CacheRegistry, walletManagersFacade: WalletManagersFacade, userWalletsStore: UserWalletsStore, + txHistoryItemsStore: TxHistoryItemsStore, ): TxHistoryRepository = DefaultTxHistoryRepository( - walletManagersFacade = walletManagersFacade, - userWalletsStore = userWalletsStore, + cacheRegistry, + walletManagersFacade, + userWalletsStore, + txHistoryItemsStore, ) } \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt index 2f9fa852c0..bfa1cc57d1 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt @@ -3,7 +3,9 @@ package com.tangem.data.txhistory.repository import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData +import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.txhistory.repository.paging.TxHistoryPagingSource +import com.tangem.datasource.local.txhistory.TxHistoryItemsStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network @@ -13,50 +15,57 @@ import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.domain.txhistory.repository.TxHistoryRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow class DefaultTxHistoryRepository( + private val cacheRegistry: CacheRegistry, private val walletManagersFacade: WalletManagersFacade, private val userWalletsStore: UserWalletsStore, + private val txHistoryItemsStore: TxHistoryItemsStore, ) : TxHistoryRepository { - override suspend fun getTxHistoryItemsCount(network: Network): Int { - val userWallet = getUserWallet() + override suspend fun getTxHistoryItemsCount(userWalletId: UserWalletId, network: Network): Int { + val userWallet = getUserWallet(userWalletId) val state = walletManagersFacade.getTxHistoryState( userWalletId = userWallet.walletId, network = network, ) return when (state) { is TxHistoryState.Failed.FetchError -> throw TxHistoryStateError.DataError(state.exception) - TxHistoryState.NotImplemented -> throw TxHistoryStateError.TxHistoryNotImplemented - TxHistoryState.Success.Empty -> throw TxHistoryStateError.EmptyTxHistories + is TxHistoryState.NotImplemented -> throw TxHistoryStateError.TxHistoryNotImplemented + is TxHistoryState.Success.Empty -> throw TxHistoryStateError.EmptyTxHistories is TxHistoryState.Success.HasTransactions -> state.txCount } } - override fun getTxHistoryItems(currency: CryptoCurrency, pageSize: Int): Flow> { - val userWallet = getUserWallet() - return Pager( + override fun getTxHistoryItems( + userWalletId: UserWalletId, + currency: CryptoCurrency, + pageSize: Int, + refresh: Boolean, + ): Flow> { + val pager = Pager( config = PagingConfig( pageSize = pageSize, initialLoadSize = pageSize, ), pagingSourceFactory = { TxHistoryPagingSource( - loadPage = { page: Int, pageSize: Int -> - walletManagersFacade.getTxHistoryItems( - userWalletId = userWallet.walletId, - currency = currency, - page = page, - pageSize = pageSize, - ) - }, + sourceParams = TxHistoryPagingSource.Params(userWalletId, currency, pageSize, refresh), + txHistoryItemsStore = txHistoryItemsStore, + walletManagersFacade = walletManagersFacade, + cacheRegistry = cacheRegistry, ) }, - ).flow + ) + + return pager.flow } - private fun getUserWallet(): UserWallet = requireNotNull(userWalletsStore.selectedUserWalletOrNull) { - "Selected wallet must not be null" + private suspend fun getUserWallet(userWalletId: UserWalletId): UserWallet { + return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { + "Unable to find user wallet with provided ID: $userWalletId" + } } } \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt index a3ebb06194..f195b173f7 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt @@ -2,34 +2,96 @@ package com.tangem.data.txhistory.repository.paging import androidx.paging.PagingSource import androidx.paging.PagingState +import com.tangem.data.common.cache.CacheRegistry +import com.tangem.datasource.local.txhistory.TxHistoryItemsStore +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.txhistory.models.PaginationWrapper import com.tangem.domain.txhistory.models.TxHistoryItem - -private const val INITIAL_PAGE = 1 +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWalletId +import timber.log.Timber internal class TxHistoryPagingSource( - private val loadPage: suspend (page: Int, pageSize: Int) -> PaginationWrapper, + private val sourceParams: Params, + private val txHistoryItemsStore: TxHistoryItemsStore, + private val walletManagersFacade: WalletManagersFacade, + private val cacheRegistry: CacheRegistry, ) : PagingSource() { + private val storeKey = TxHistoryItemsStore.Key(sourceParams.userWalletId, sourceParams.currency) + override fun getRefreshKey(state: PagingState): Int? { return state.anchorPosition?.let { anchorPosition -> - state.closestPageToPosition(anchorPosition)?.prevKey?.plus(other = 1) - ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(other = 1) + val anchorPage = state.closestPageToPosition(anchorPosition) + anchorPage?.prevKey?.inc() ?: anchorPage?.nextKey?.dec() } } override suspend fun load(params: LoadParams): LoadResult { - val currentPage = params.key ?: INITIAL_PAGE - return try { - val result = loadPage(currentPage, params.loadSize) + val pageToLoad = params.key ?: INITIAL_PAGE - LoadResult.Page( - data = result.items, - prevKey = if (currentPage > INITIAL_PAGE) currentPage.minus(1) else null, - nextKey = if (result.page < result.totalPages) currentPage.plus(1) else null, + return try { + val wrappedItems = loadItems( + pageToLoad = pageToLoad, + pageSize = sourceParams.pageSize, + refresh = sourceParams.refresh && params is LoadParams.Refresh, ) - } catch (e: Exception) { + + val items = wrappedItems.items + val prevPage = when { + items.isEmpty() -> null + pageToLoad > INITIAL_PAGE -> pageToLoad.dec() + else -> null + } + val nextPage = when { + items.isEmpty() -> INITIAL_PAGE + pageToLoad < wrappedItems.totalPages -> pageToLoad.inc() + else -> null + } + + LoadResult.Page(items, prevKey = prevPage, nextKey = nextPage) + } catch (e: Throwable) { + Timber.e(e, "Unable to load the transaction history for the requested page: $pageToLoad") + LoadResult.Error(e) } } + + private suspend fun loadItems(pageToLoad: Int, pageSize: Int, refresh: Boolean): PaginationWrapper { + cacheRegistry.invokeOnExpire( + key = getTxHistoryPageKey(pageToLoad), + skipCache = refresh, + block = { fetch(pageToLoad, pageSize) }, + ) + + return requireNotNull(txHistoryItemsStore.getSyncOrNull(storeKey, pageToLoad)) { + "The transaction history page #$pageToLoad could not be retrieved" + } + } + + private suspend fun fetch(pageToLoad: Int, pageSize: Int) { + val wrappedItems = walletManagersFacade.getTxHistoryItems( + userWalletId = sourceParams.userWalletId, + currency = sourceParams.currency, + page = pageToLoad, + pageSize = pageSize, + ) + + txHistoryItemsStore.store(storeKey, wrappedItems) + } + + private fun getTxHistoryPageKey(page: Int): String { + return "tx_history_page_${sourceParams.currency}_${sourceParams.userWalletId}_$page" + } + + data class Params( + val userWalletId: UserWalletId, + val currency: CryptoCurrency, + val pageSize: Int, + val refresh: Boolean, + ) + + private companion object { + private const val INITIAL_PAGE = 1 + } } \ No newline at end of file diff --git a/domain/txhistory/build.gradle.kts b/domain/txhistory/build.gradle.kts index 2ba0547f59..892e55e639 100644 --- a/domain/txhistory/build.gradle.kts +++ b/domain/txhistory/build.gradle.kts @@ -9,11 +9,15 @@ android { } dependencies { - implementation(deps.arrow.core) - implementation(deps.kotlin.coroutines) - implementation(deps.androidx.paging.runtime) - - implementation(projects.core.utils) + /** Project - Domain */ + implementation(projects.domain.core) implementation(projects.domain.tokens.models) implementation(projects.domain.txhistory.models) + implementation(projects.domain.wallets.models) + + /** Project - Other */ + implementation(projects.core.utils) + + /** Android - Other */ + implementation(deps.androidx.paging.runtime) } \ No newline at end of file diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt index 0c778f81ae..a71fe087c1 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt @@ -6,13 +6,19 @@ import com.tangem.domain.tokens.model.Network 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.UserWalletId import kotlinx.coroutines.flow.Flow interface TxHistoryRepository { @Throws(TxHistoryStateError::class) - suspend fun getTxHistoryItemsCount(network: Network): Int + suspend fun getTxHistoryItemsCount(userWalletId: UserWalletId, network: Network): Int @Throws(TxHistoryListError::class) - fun getTxHistoryItems(currency: CryptoCurrency, pageSize: Int): Flow> + fun getTxHistoryItems( + userWalletId: UserWalletId, + currency: CryptoCurrency, + pageSize: Int, + refresh: Boolean, + ): Flow> } \ No newline at end of file diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt index 6ca59cb013..ab14187a02 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt @@ -6,14 +6,15 @@ import arrow.core.raise.either import com.tangem.domain.tokens.model.Network import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.domain.txhistory.repository.TxHistoryRepository +import com.tangem.domain.wallets.models.UserWalletId +// TODO: Add tests class GetTxHistoryItemsCountUseCase(private val repository: TxHistoryRepository) { - // FIXME: Provide UserWalletId - suspend operator fun invoke(network: Network): Either { + suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either { return either { catch( - block = { repository.getTxHistoryItemsCount(network) }, + block = { repository.getTxHistoryItemsCount(userWalletId, network) }, catch = { throwable -> raise( when (throwable) { diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt index 0555f17187..e5570e8077 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt @@ -7,21 +7,25 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.domain.txhistory.repository.TxHistoryRepository +import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch private const val DEFAULT_PAGE_SIZE = 50 +// TODO: Add tests class GetTxHistoryItemsUseCase(private val repository: TxHistoryRepository) { // FIXME: Provide UserWalletId operator fun invoke( + userWalletId: UserWalletId, currency: CryptoCurrency, pageSize: Int = DEFAULT_PAGE_SIZE, + refresh: Boolean = false, ): Either>> { return either { repository - .getTxHistoryItems(currency = currency, pageSize = pageSize) + .getTxHistoryItems(userWalletId, currency, pageSize, refresh) .catch { raise(TxHistoryListError.DataError(it)) } } } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/GetSelectedWalletError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/GetSelectedWalletError.kt deleted file mode 100644 index ad0867fa6e..0000000000 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/GetSelectedWalletError.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.domain.wallets.models - -sealed interface GetSelectedWalletError { - - object DataError : GetSelectedWalletError - - object NoUserWalletSelected : GetSelectedWalletError -} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/GetUserWalletError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/GetUserWalletError.kt new file mode 100644 index 0000000000..50d1bea262 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/GetUserWalletError.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.wallets.models + +sealed class GetUserWalletError { + + data class DataError(val cause: Throwable) : GetUserWalletError() + + object UserWalletNotFound : GetUserWalletError() +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt index b56de58375..a29aad4ea4 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt @@ -4,7 +4,7 @@ import arrow.core.Either import arrow.core.raise.either import arrow.core.raise.ensureNotNull import com.tangem.domain.wallets.legacy.WalletsStateHolder -import com.tangem.domain.wallets.models.GetSelectedWalletError +import com.tangem.domain.wallets.models.GetUserWalletError import com.tangem.domain.wallets.models.UserWallet /** @@ -17,16 +17,20 @@ import com.tangem.domain.wallets.models.UserWallet */ class GetSelectedWalletUseCase(private val walletsStateHolder: WalletsStateHolder) { - operator fun invoke(): Either { + operator fun invoke(): Either { return either { val userWalletsListManager = ensureNotNull( value = walletsStateHolder.userWalletsListManager, - raise = { GetSelectedWalletError.DataError }, + raise = { + val error = IllegalStateException("User wallets list manager not initialized") + + GetUserWalletError.DataError(error) + }, ) ensureNotNull( value = userWalletsListManager.selectedUserWalletSync, - raise = { GetSelectedWalletError.NoUserWalletSelected }, + raise = { GetUserWalletError.UserWalletNotFound }, ) } } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt new file mode 100644 index 0000000000..9d1fe85f5e --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt @@ -0,0 +1,30 @@ +package com.tangem.domain.wallets.usecase + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensureNotNull +import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.models.GetUserWalletError +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.firstOrNull + +class GetUserWalletUseCase(private val walletsStateHolder: WalletsStateHolder) { + + suspend operator fun invoke(userWalletId: UserWalletId): Either = either { + val userWalletsListManager = ensureNotNull( + value = walletsStateHolder.userWalletsListManager, + raise = { + val error = IllegalStateException("User wallets list manager not initialized") + + GetUserWalletError.DataError(error) + }, + ) + + val userWallets = userWalletsListManager.userWallets.firstOrNull().orEmpty() + + ensureNotNull(userWallets.firstOrNull { it.walletId == userWalletId }) { + raise(GetUserWalletError.UserWalletNotFound) + } + } +} \ No newline at end of file diff --git a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsRouter.kt b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsRouter.kt index da5bc7571f..a0f630c125 100644 --- a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsRouter.kt +++ b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsRouter.kt @@ -7,6 +7,7 @@ interface TokenDetailsRouter { fun getEntryFragment(): Fragment companion object { + const val USER_WALLET_ID_KEY = "token_details_user_wallet_id" const val CRYPTO_CURRENCY_KEY = "token_details_crypto_currency" } } \ 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 bbf6300015..1de3829916 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 @@ -23,10 +23,9 @@ import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase -import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenScreenEvent import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState @@ -48,7 +47,7 @@ import kotlin.properties.Delegates @HiltViewModel internal class TokenDetailsViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, - private val getSelectedWalletUseCase: GetSelectedWalletUseCase, + private val getUserWalletUseCase: GetUserWalletUseCase, private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, @@ -67,15 +66,18 @@ internal class TokenDetailsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents { + private val userWalletId: UserWalletId = savedStateHandle.get(TokenDetailsRouter.USER_WALLET_ID_KEY) + ?.let { stringValue -> UserWalletId(stringValue) } + ?: error("This screen can't open without `UserWalletId`") + private val cryptoCurrency: CryptoCurrency = savedStateHandle[TokenDetailsRouter.CRYPTO_CURRENCY_KEY] - ?: error("This screen can't open without CryptoCurrency") + ?: error("This screen can't open without `CryptoCurrency`") var router by Delegates.notNull() private val marketPriceJobHolder = JobHolder() private val refreshStateJobHolder = JobHolder() private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null - private var wallet by Delegates.notNull() private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() @@ -91,23 +93,14 @@ internal class TokenDetailsViewModel @Inject constructor( private set override fun onCreate(owner: LifecycleOwner) { - getWallet() - updateContent(selectedWallet = wallet) + updateContent() handleBalanceHiding(owner) } - private fun getWallet() { - getSelectedWalletUseCase() - .fold( - ifLeft = { error("Can not get selected wallet $it") }, - ifRight = { wallet = it }, - ) - } - - private fun updateContent(selectedWallet: UserWallet) { - updateMarketPrice(selectedWallet = selectedWallet) + private fun updateContent() { + updateMarketPrice() updateTxHistory(refresh = false, showItemsLoading = true) - updateWarnings(selectedWallet = selectedWallet) + updateWarnings() } private fun handleBalanceHiding(owner: LifecycleOwner) { @@ -135,10 +128,10 @@ internal class TokenDetailsViewModel @Inject constructor( .launchIn(viewModelScope) } - private fun updateWarnings(selectedWallet: UserWallet) { + private fun updateWarnings() { viewModelScope.launch(dispatchers.io) { getCurrencyWarningsUseCase.invoke( - userWalletId = selectedWallet.walletId, + userWalletId = userWalletId, currency = cryptoCurrency, derivationPath = cryptoCurrency.network.derivationPath, ) @@ -148,9 +141,9 @@ internal class TokenDetailsViewModel @Inject constructor( } } - private fun updateMarketPrice(selectedWallet: UserWallet) { + private fun updateMarketPrice() { getCurrencyStatusUpdatesUseCase( - userWalletId = selectedWallet.walletId, + userWalletId = userWalletId, currencyId = cryptoCurrency.id, derivationPath = cryptoCurrency.network.derivationPath, ) @@ -159,7 +152,7 @@ internal class TokenDetailsViewModel @Inject constructor( uiState = stateFactory.getCurrencyLoadedBalanceState(either) either.onRight { status -> cryptoCurrencyStatus = status - updateButtons(userWalletId = selectedWallet.walletId, currencyStatus = status) + updateButtons(userWalletId = userWalletId, currencyStatus = status) } } .flowOn(dispatchers.io) @@ -175,6 +168,7 @@ internal class TokenDetailsViewModel @Inject constructor( private fun updateTxHistory(refresh: Boolean, showItemsLoading: Boolean) { viewModelScope.launch(dispatchers.io) { val txHistoryItemsCountEither = txHistoryItemsCountUseCase( + userWalletId = userWalletId, network = cryptoCurrency.network, ) @@ -184,9 +178,13 @@ internal class TokenDetailsViewModel @Inject constructor( } txHistoryItemsCountEither.onRight { - val either = txHistoryItemsUseCase(currency = cryptoCurrency) - .map { it.cachedIn(viewModelScope) } - uiState = stateFactory.getLoadedTxHistoryState(txHistoryEither = either) + val maybeTxHistory = txHistoryItemsUseCase( + userWalletId = userWalletId, + currency = cryptoCurrency, + refresh = refresh, + ).map { it.cachedIn(viewModelScope) } + + uiState = stateFactory.getLoadedTxHistoryState(maybeTxHistory) } } } @@ -211,13 +209,15 @@ internal class TokenDetailsViewModel @Inject constructor( analyticsEventsHandler.send(TokenScreenEvent.ButtonBuy(cryptoCurrency.symbol)) val status = cryptoCurrencyStatus ?: return - reduxStateHolder.dispatch( - TradeCryptoAction.New.Buy( - userWallet = wallet, - cryptoCurrencyStatus = status, - appCurrencyCode = selectedAppCurrencyFlow.value.code, - ), - ) + viewModelScope.launch(dispatchers.io) { + reduxStateHolder.dispatch( + TradeCryptoAction.New.Buy( + userWallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch }, + cryptoCurrencyStatus = status, + appCurrencyCode = selectedAppCurrencyFlow.value.code, + ), + ) + } } override fun onReloadClick() { @@ -231,38 +231,38 @@ internal class TokenDetailsViewModel @Inject constructor( val cryptoCurrencyStatus = cryptoCurrencyStatus ?: return - when (cryptoCurrencyStatus.currency) { - is CryptoCurrency.Coin -> { - reduxStateHolder.dispatch( - action = TradeCryptoAction.New.SendCoin( - userWallet = wallet, - coinStatus = cryptoCurrencyStatus, - ), - ) + viewModelScope.launch(dispatchers.io) { + when (cryptoCurrencyStatus.currency) { + is CryptoCurrency.Coin -> { + reduxStateHolder.dispatch( + action = TradeCryptoAction.New.SendCoin( + userWallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch }, + coinStatus = cryptoCurrencyStatus, + ), + ) + } + is CryptoCurrency.Token -> sendToken(status = cryptoCurrencyStatus) } - is CryptoCurrency.Token -> sendToken(status = cryptoCurrencyStatus) } } private fun sendToken(status: CryptoCurrencyStatus) { viewModelScope.launch(dispatchers.io) { - getNetworkCoinStatusUseCase( - userWalletId = wallet.walletId, + val maybeCoinStatus = getNetworkCoinStatusUseCase( + userWalletId = userWalletId, networkId = status.currency.network.id, derivationPath = status.currency.network.derivationPath, - ) - .take(count = 1) - .collectLatest { - it.onRight { coinStatus -> - reduxStateHolder.dispatch( - action = TradeCryptoAction.New.SendToken( - userWallet = wallet, - tokenStatus = status, - coinFiatRate = coinStatus.value.fiatRate, - ), - ) - } - } + ).firstOrNull() + + maybeCoinStatus?.onRight { coinStatus -> + reduxStateHolder.dispatch( + action = TradeCryptoAction.New.SendToken( + userWallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch }, + tokenStatus = status, + coinFiatRate = coinStatus.value.fiatRate, + ), + ) + } } } @@ -271,7 +271,7 @@ internal class TokenDetailsViewModel @Inject constructor( viewModelScope.launch(dispatchers.io) { val addresses = walletManagersFacade.getAddress( - userWalletId = wallet.walletId, + userWalletId = userWalletId, network = cryptoCurrency.network, ) @@ -314,7 +314,7 @@ internal class TokenDetailsViewModel @Inject constructor( analyticsEventsHandler.send(TokenScreenEvent.ButtonRemoveToken(cryptoCurrency.symbol)) viewModelScope.launch { - val hasLinkedTokens = removeCurrencyUseCase.hasLinkedTokens(wallet.walletId, cryptoCurrency) + val hasLinkedTokens = removeCurrencyUseCase.hasLinkedTokens(userWalletId, cryptoCurrency) uiState = if (hasLinkedTokens) { stateFactory.getStateWithLinkedTokensDialog(cryptoCurrency) } else { @@ -325,7 +325,7 @@ internal class TokenDetailsViewModel @Inject constructor( override fun onHideConfirmed() { viewModelScope.launch { - removeCurrencyUseCase.invoke(wallet.walletId, cryptoCurrency) + removeCurrencyUseCase.invoke(userWalletId, cryptoCurrency) .onLeft { Timber.e(it) } .onRight { router.popBackStack() } } @@ -335,7 +335,7 @@ internal class TokenDetailsViewModel @Inject constructor( analyticsEventsHandler.send(TokenScreenEvent.ButtonExplore(cryptoCurrency.symbol)) viewModelScope.launch(dispatchers.io) { val addresses = walletManagersFacade.getAddress( - userWalletId = wallet.walletId, + userWalletId = userWalletId, network = cryptoCurrency.network, ) @@ -357,7 +357,7 @@ internal class TokenDetailsViewModel @Inject constructor( viewModelScope.launch { router.openUrl( url = getExploreUrlUseCase( - userWalletId = wallet.walletId, + userWalletId = userWalletId, network = cryptoCurrency.network, addressType = addressType, ), @@ -373,8 +373,8 @@ internal class TokenDetailsViewModel @Inject constructor( viewModelScope.launch(dispatchers.io) { listOf( async { - fetchCurrencyStatusUseCase.invoke( - userWalletId = wallet.walletId, + fetchCurrencyStatusUseCase( + userWalletId = userWalletId, id = cryptoCurrency.id, derivationPath = cryptoCurrency.network.derivationPath, refresh = true, @@ -386,7 +386,7 @@ internal class TokenDetailsViewModel @Inject constructor( showItemsLoading = uiState.txHistoryState !is TxHistoryState.Content, ) }, - async { updateWarnings(wallet) }, + async { updateWarnings() }, ).awaitAll() uiState = stateFactory.getRefreshedState() }.saveIn(refreshStateJobHolder) 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 2c7205d680..c481e57703 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 @@ -107,11 +107,14 @@ internal class DefaultWalletRouter(private val reduxNavController: ReduxNavContr reduxNavController.navigate(action = NavigationAction.OpenUrl(url)) } - override fun openTokenDetails(currency: CryptoCurrency) { + override fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) { reduxNavController.navigate( action = NavigationAction.NavigateTo( screen = AppScreen.WalletDetails, - bundle = bundleOf(TokenDetailsRouter.CRYPTO_CURRENCY_KEY to currency), + bundle = bundleOf( + TokenDetailsRouter.USER_WALLET_ID_KEY to userWalletId.stringValue, + TokenDetailsRouter.CRYPTO_CURRENCY_KEY to currency, + ), ), ) } 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 e48e601399..07a2e8a80b 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,7 +44,7 @@ internal interface InnerWalletRouter : WalletRouter { fun openTxHistoryWebsite(url: String) /** Open token details screen */ - fun openTokenDetails(currency: CryptoCurrency) + fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) /** Open stories screen */ fun openStoriesScreen() 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 9cb22da162..5b6e0fd44e 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 @@ -726,7 +726,7 @@ internal class WalletViewModel @Inject constructor( override fun onTokenItemClick(currency: CryptoCurrency) { analyticsEventsHandler.send(PortfolioEvent.TokenTapped) - router.openTokenDetails(currency = currency) + router.openTokenDetails(getSelectedWallet().walletId, currency) } override fun onTokenItemLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { @@ -963,7 +963,7 @@ internal class WalletViewModel @Inject constructor( private fun getSingleCurrencyContent(index: Int) { val wallet = getWallet(index) - getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = wallet.walletId) + getPrimaryCurrencyStatusUpdatesUseCase(wallet.walletId) .distinctUntilChanged() .onEach { maybeCryptoCurrencyStatus -> uiState = stateFactory.getSingleCurrencyLoadedBalanceState(maybeCryptoCurrencyStatus) @@ -976,8 +976,8 @@ internal class WalletViewModel @Inject constructor( } updateNotifications(index) - updateButtons(userWalletId = wallet.walletId, currencyStatus = status) - updateTxHistory(status.currency) + updateButtons(wallet.walletId, status) + updateTxHistory(wallet.walletId, status.currency, refresh = false) } } .flowOn(dispatchers.io) @@ -985,9 +985,12 @@ internal class WalletViewModel @Inject constructor( .saveIn(marketPriceJobHolder) } - private fun updateTxHistory(currency: CryptoCurrency) { + private fun updateTxHistory(userWalletId: UserWalletId, currency: CryptoCurrency, refresh: Boolean) { viewModelScope.launch(dispatchers.io) { - val txHistoryItemsCountEither = txHistoryItemsCountUseCase(currency.network) + val txHistoryItemsCountEither = txHistoryItemsCountUseCase( + userWalletId = userWalletId, + network = currency.network, + ) uiState = stateFactory.getLoadingTxHistoryState( itemsCountEither = txHistoryItemsCountEither, @@ -995,7 +998,11 @@ internal class WalletViewModel @Inject constructor( txHistoryItemsCountEither.onRight { uiState = stateFactory.getLoadedTxHistoryState( - txHistoryEither = txHistoryItemsUseCase(currency = currency).map { + txHistoryEither = txHistoryItemsUseCase( + userWalletId = userWalletId, + currency = currency, + refresh = refresh, + ).map { it.cachedIn(viewModelScope) }, ) @@ -1057,6 +1064,10 @@ internal class WalletViewModel @Inject constructor( uiState = stateFactory.getRefreshedState() uiState = result.fold(stateFactory::getStateByCurrencyStatusError) { uiState } + + singleWalletCryptoCurrencyStatus?.let { + updateTxHistory(wallet.walletId, it.currency, refresh = true) + } }.saveIn(refreshContentJobHolder) } @@ -1091,5 +1102,12 @@ internal class WalletViewModel @Inject constructor( ) } + private fun getSelectedWallet(): UserWallet { + val state = uiState as? WalletState.ContentState + ?: error("Unable to get selected user wallet") + + return getWallet(state.walletsListConfig.selectedWalletIndex) + } + private fun getCardTypeResolver(index: Int): CardTypesResolver = getWallet(index).scanResponse.cardTypesResolver } \ No newline at end of file