Updated on 2026-08-14
This commit is contained in:
parent
7d9da40464
commit
519f5332f1
23 changed files with 379 additions and 132 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Set<PaginationWrapper<TxHistoryItem>>>,
|
||||
) : TxHistoryItemsStore,
|
||||
StringKeyDataStoreDecorator<TxHistoryItemsStore.Key, Set<PaginationWrapper<TxHistoryItem>>>(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<TxHistoryItem>? {
|
||||
val storedValue = getSyncOrNull(key)
|
||||
|
||||
return storedValue?.firstOrNull { it.page == page }
|
||||
}
|
||||
|
||||
override suspend fun store(key: TxHistoryItemsStore.Key, value: PaginationWrapper<TxHistoryItem>) {
|
||||
val oldValue = getSyncOrNull(key).orEmpty()
|
||||
val newValue = oldValue.addOrReplace(value) {
|
||||
it.page == value.page
|
||||
}
|
||||
|
||||
store(key, newValue)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TxHistoryItem>?
|
||||
|
||||
suspend fun remove(key: Key)
|
||||
|
||||
suspend fun store(key: Key, value: PaginationWrapper<TxHistoryItem>)
|
||||
|
||||
data class Key(
|
||||
val userWalletId: UserWalletId,
|
||||
val currency: CryptoCurrency,
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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<PagingData<TxHistoryItem>> {
|
||||
val userWallet = getUserWallet()
|
||||
return Pager(
|
||||
override fun getTxHistoryItems(
|
||||
userWalletId: UserWalletId,
|
||||
currency: CryptoCurrency,
|
||||
pageSize: Int,
|
||||
refresh: Boolean,
|
||||
): Flow<PagingData<TxHistoryItem>> {
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TxHistoryItem>,
|
||||
private val sourceParams: Params,
|
||||
private val txHistoryItemsStore: TxHistoryItemsStore,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val cacheRegistry: CacheRegistry,
|
||||
) : PagingSource<Int, TxHistoryItem>() {
|
||||
|
||||
private val storeKey = TxHistoryItemsStore.Key(sourceParams.userWalletId, sourceParams.currency)
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, TxHistoryItem>): 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<Int>): LoadResult<Int, TxHistoryItem> {
|
||||
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<TxHistoryItem> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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<PagingData<TxHistoryItem>>
|
||||
fun getTxHistoryItems(
|
||||
userWalletId: UserWalletId,
|
||||
currency: CryptoCurrency,
|
||||
pageSize: Int,
|
||||
refresh: Boolean,
|
||||
): Flow<PagingData<TxHistoryItem>>
|
||||
}
|
||||
|
|
@ -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<TxHistoryStateError, Int> {
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either<TxHistoryStateError, Int> {
|
||||
return either {
|
||||
catch(
|
||||
block = { repository.getTxHistoryItemsCount(network) },
|
||||
block = { repository.getTxHistoryItemsCount(userWalletId, network) },
|
||||
catch = { throwable ->
|
||||
raise(
|
||||
when (throwable) {
|
||||
|
|
|
|||
|
|
@ -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<TxHistoryListError, Flow<PagingData<TxHistoryItem>>> {
|
||||
return either {
|
||||
repository
|
||||
.getTxHistoryItems(currency = currency, pageSize = pageSize)
|
||||
.getTxHistoryItems(userWalletId, currency, pageSize, refresh)
|
||||
.catch { raise(TxHistoryListError.DataError(it)) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
package com.tangem.domain.wallets.models
|
||||
|
||||
sealed interface GetSelectedWalletError {
|
||||
|
||||
object DataError : GetSelectedWalletError
|
||||
|
||||
object NoUserWalletSelected : GetSelectedWalletError
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.domain.wallets.models
|
||||
|
||||
sealed class GetUserWalletError {
|
||||
|
||||
data class DataError(val cause: Throwable) : GetUserWalletError()
|
||||
|
||||
object UserWalletNotFound : GetUserWalletError()
|
||||
}
|
||||
|
|
@ -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<GetSelectedWalletError, UserWallet> {
|
||||
operator fun invoke(): Either<GetUserWalletError, UserWallet> {
|
||||
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 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Any, UserWallet> = 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String>(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<InnerTokenDetailsRouter>()
|
||||
|
||||
private val marketPriceJobHolder = JobHolder()
|
||||
private val refreshStateJobHolder = JobHolder()
|
||||
private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null
|
||||
private var wallet by Delegates.notNull<UserWallet>()
|
||||
|
||||
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = 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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue