Updated on 2026-08-14
This commit is contained in:
parent
3da4771d66
commit
3baed1e815
57 changed files with 1797 additions and 370 deletions
|
|
@ -9,7 +9,6 @@ import androidx.compose.foundation.verticalScroll
|
|||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.features.commonfeatures.impl.R
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.router.stack.ChildStack
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
|
|
@ -21,6 +20,7 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorComponent
|
||||
import com.tangem.features.commonfeatures.impl.R
|
||||
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioRoutes
|
||||
|
||||
@Composable
|
||||
|
|
@ -63,7 +63,9 @@ internal fun AddToPortfolioBottomSheet(
|
|||
bottom = 16.dp,
|
||||
)
|
||||
val isScrollableContent = when (animatedStack.active.configuration) {
|
||||
AddToPortfolioRoutes.PortfolioSelector -> false
|
||||
AddToPortfolioRoutes.PortfolioSelector,
|
||||
AddToPortfolioRoutes.UserPortfolio,
|
||||
-> false
|
||||
AddToPortfolioRoutes.AddToken,
|
||||
AddToPortfolioRoutes.Empty,
|
||||
is AddToPortfolioRoutes.NetworkSelector,
|
||||
|
|
@ -95,6 +97,7 @@ private fun AddToPortfolioBottomSheetTitle(
|
|||
AddToPortfolioRoutes.Empty -> TextReference.EMPTY
|
||||
is AddToPortfolioRoutes.NetworkSelector -> resourceReference(R.string.common_choose_network)
|
||||
AddToPortfolioRoutes.TokenActions -> resourceReference(R.string.common_get_token)
|
||||
AddToPortfolioRoutes.UserPortfolio -> resourceReference(R.string.markets_portfolio_block_title)
|
||||
AddToPortfolioRoutes.PortfolioSelector -> (stack.active.instance as PortfolioSelectorComponent)
|
||||
.title.collectAsStateWithLifecycle().value
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,14 +11,17 @@ import com.tangem.core.decompose.context.child
|
|||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorComponent
|
||||
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent
|
||||
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
|
||||
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorComponent
|
||||
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioModel
|
||||
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioRoutes
|
||||
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.UserPortfolioComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class DefaultAddToPortfolioComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted private val params: AddToPortfolioComponent.Params,
|
||||
|
|
@ -26,6 +29,7 @@ internal class DefaultAddToPortfolioComponent @AssistedInject constructor(
|
|||
addTokenComponentFactory: AddTokenComponent.Factory,
|
||||
tokenActionsComponentFactory: TokenActionsComponent.Factory,
|
||||
private val chooseNetworkComponentFactory: ChooseNetworkComponent.Factory,
|
||||
private val userPortfolioComponentFactory: UserPortfolioComponent.Factory,
|
||||
) : AppComponentContext by context, AddToPortfolioComponent {
|
||||
|
||||
private val model: AddToPortfolioModel = getOrCreateModel(params)
|
||||
|
|
@ -91,6 +95,7 @@ internal class DefaultAddToPortfolioComponent @AssistedInject constructor(
|
|||
AddToPortfolioRoutes.PortfolioSelector -> portfolioSelectorComponent
|
||||
AddToPortfolioRoutes.TokenActions -> tokenActionsComponent
|
||||
AddToPortfolioRoutes.Empty -> ComposableContentComponent.EMPTY
|
||||
AddToPortfolioRoutes.UserPortfolio -> createUserPortfolioComponent(componentContext)
|
||||
is AddToPortfolioRoutes.NetworkSelector -> chooseNetworkComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = ChooseNetworkComponent.Params(
|
||||
|
|
@ -100,6 +105,19 @@ internal class DefaultAddToPortfolioComponent @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun createUserPortfolioComponent(componentContext: ComponentContext): ComposableContentComponent {
|
||||
return when (model.addToPortfolioManager.settings.launchMode) {
|
||||
AddToPortfolioManager.LaunchMode.DirectAdd -> ComposableContentComponent.EMPTY
|
||||
is AddToPortfolioManager.LaunchMode.ViaUserPortfolio -> userPortfolioComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = UserPortfolioComponent.Params(
|
||||
uiState = model.userPortfolioStateController.uiState,
|
||||
callbacks = model,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : AddToPortfolioComponent.Factory {
|
||||
override fun create(
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ internal class DefaultAddToPortfolioPreselectedDataComponent @AssistedInject con
|
|||
AddToPortfolioRoutes.TokenActions -> ComposableContentComponent.EMPTY
|
||||
AddToPortfolioRoutes.Empty -> ComposableContentComponent.EMPTY
|
||||
is AddToPortfolioRoutes.NetworkSelector -> ComposableContentComponent.EMPTY
|
||||
AddToPortfolioRoutes.UserPortfolio -> ComposableContentComponent.EMPTY
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioPrese
|
|||
import com.tangem.features.commonfeatures.impl.addtoportfolio.DefaultAddToPortfolioComponent
|
||||
import com.tangem.features.commonfeatures.impl.addtoportfolio.DefaultAddToPortfolioPreselectedDataComponent
|
||||
import com.tangem.features.commonfeatures.impl.addtoportfolio.ui.DefaultAddToPortfolioManager
|
||||
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.DefaultUserPortfolioComponent
|
||||
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.UserPortfolioComponent
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -25,4 +27,9 @@ internal interface AddToPortfolioComponentModule {
|
|||
fun bindAddToPortfolioPreselectedDataComponent(
|
||||
factory: DefaultAddToPortfolioPreselectedDataComponent.Factory,
|
||||
): AddToPortfolioPreselectedDataComponent.Factory
|
||||
|
||||
@Binds
|
||||
fun bindUserPortfolioComponentFactory(
|
||||
factory: DefaultUserPortfolioComponent.Factory,
|
||||
): UserPortfolioComponent.Factory
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfol
|
|||
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddTokenModel
|
||||
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.ChooseNetworkModel
|
||||
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.TokenActionsModel
|
||||
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model.UserPortfolioModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -41,4 +42,9 @@ internal interface AddToPortfolioModelModule {
|
|||
@IntoMap
|
||||
@ClassKey(ChooseNetworkModel::class)
|
||||
fun chooseNetworkModel(model: ChooseNetworkModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(UserPortfolioModel::class)
|
||||
fun userPortfolioModel(model: UserPortfolioModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
package com.tangem.features.commonfeatures.impl.addtoportfolio.model
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.domain.markets.GetTokenMarketCryptoCurrency
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase
|
||||
import com.tangem.features.commonfeatures.api.addtoportfolio.AvailableToAddAccount
|
||||
import com.tangem.features.commonfeatures.api.addtoportfolio.AvailableToAddData
|
||||
import com.tangem.features.commonfeatures.api.addtoportfolio.AvailableToAddWallet
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class AddToPortfolioInitialSelectionResolver @Inject constructor(
|
||||
private val getTokenMarketCryptoCurrency: GetTokenMarketCryptoCurrency,
|
||||
private val networkHasDerivationUseCase: NetworkHasDerivationUseCase,
|
||||
) {
|
||||
|
||||
suspend fun resolve(
|
||||
availableToAddData: AvailableToAddData,
|
||||
orderedNetworks: List<TokenMarketInfo.Network>,
|
||||
selectedWallet: UserWallet?,
|
||||
tokenParams: TokenMarketParams,
|
||||
accountToAdd: AvailableToAddAccount? = null,
|
||||
): InitialSelection? {
|
||||
if (availableToAddData.availableToAddWallets.isEmpty()) return null
|
||||
val fallbackNetwork = orderedNetworks.firstOrNull() ?: return null
|
||||
|
||||
val walletOrder = orderedWallets(availableToAddData, selectedWallet)
|
||||
|
||||
if (accountToAdd != null) {
|
||||
val ownerWallet = walletOrder.firstOrNull { entry ->
|
||||
entry.availableToAddAccounts.values.any { it === accountToAdd }
|
||||
} ?: walletOrder.first()
|
||||
val network = pickAddableNetwork(
|
||||
userWallet = ownerWallet.userWallet,
|
||||
account = accountToAdd,
|
||||
orderedNetworks = orderedNetworks,
|
||||
tokenParams = tokenParams,
|
||||
)
|
||||
?: fallbackNetwork
|
||||
return InitialSelection(userWallet = ownerWallet.userWallet, account = accountToAdd, network = network)
|
||||
}
|
||||
|
||||
for (walletEntry in walletOrder) {
|
||||
val account = pickAvailableAccount(walletEntry) ?: continue
|
||||
val network = pickAddableNetwork(
|
||||
userWallet = walletEntry.userWallet,
|
||||
account = account,
|
||||
orderedNetworks = orderedNetworks,
|
||||
tokenParams = tokenParams,
|
||||
) ?: continue
|
||||
return InitialSelection(walletEntry.userWallet, account, network)
|
||||
}
|
||||
|
||||
val fallbackWallet = walletOrder.first()
|
||||
val fallbackAccount = pickFallbackAccount(fallbackWallet) ?: return null
|
||||
return InitialSelection(fallbackWallet.userWallet, fallbackAccount, fallbackNetwork)
|
||||
}
|
||||
|
||||
private fun orderedWallets(data: AvailableToAddData, selectedWallet: UserWallet?): List<AvailableToAddWallet> {
|
||||
val preferred = selectedWallet?.walletId?.let { data.availableToAddWallets[it] }
|
||||
return buildList {
|
||||
preferred?.let(::add)
|
||||
data.availableToAddWallets.values.forEach { entry ->
|
||||
if (entry !== preferred) add(entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun pickAvailableAccount(walletEntry: AvailableToAddWallet): AvailableToAddAccount? {
|
||||
val mainId = AccountId.forMainCryptoPortfolio(walletEntry.userWallet.walletId)
|
||||
return walletEntry.availableToAddAccounts[mainId]?.takeIf { it.isAvailableToAdd }
|
||||
?: walletEntry.availableToAddAccounts.values.firstOrNull { it.isAvailableToAdd }
|
||||
}
|
||||
|
||||
private fun pickFallbackAccount(walletEntry: AvailableToAddWallet): AvailableToAddAccount? {
|
||||
val mainId = AccountId.forMainCryptoPortfolio(walletEntry.userWallet.walletId)
|
||||
return walletEntry.availableToAddAccounts[mainId]
|
||||
?: walletEntry.availableToAddAccounts.values.firstOrNull()
|
||||
}
|
||||
|
||||
private suspend fun pickAddableNetwork(
|
||||
userWallet: UserWallet,
|
||||
account: AvailableToAddAccount,
|
||||
orderedNetworks: List<TokenMarketInfo.Network>,
|
||||
tokenParams: TokenMarketParams,
|
||||
): TokenMarketInfo.Network? {
|
||||
val availableOrdered = orderedNetworks.filter { candidate ->
|
||||
account.availableToAddNetworks.any { it.networkId == candidate.networkId }
|
||||
}
|
||||
if (availableOrdered.isEmpty()) return null
|
||||
|
||||
val derivationIndex = account.account.account.derivationIndex
|
||||
val withDerivation = availableOrdered.filter { network ->
|
||||
val currency = getTokenMarketCryptoCurrency(
|
||||
userWalletId = userWallet.walletId,
|
||||
tokenMarketParams = tokenParams,
|
||||
network = network,
|
||||
accountIndex = derivationIndex,
|
||||
) ?: return@filter false
|
||||
networkHasDerivationUseCase(userWallet, currency.network).getOrElse { false }
|
||||
}
|
||||
|
||||
return withDerivation.firstOrNull() ?: availableOrdered.first()
|
||||
}
|
||||
|
||||
data class InitialSelection(
|
||||
val userWallet: UserWallet,
|
||||
val account: AvailableToAddAccount,
|
||||
val network: TokenMarketInfo.Network,
|
||||
)
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import com.arkivanov.decompose.router.stack.StackNavigation
|
|||
import com.arkivanov.decompose.router.stack.popToFirst
|
||||
import com.arkivanov.decompose.router.stack.pushNew
|
||||
import com.arkivanov.decompose.router.stack.replaceAll
|
||||
import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network
|
||||
import com.tangem.common.ui.markets.action.CryptoCurrencyData
|
||||
import com.tangem.common.ui.markets.action.QuickActionsConverter.toQuickActions
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
|
|
@ -11,22 +12,29 @@ import com.tangem.core.decompose.di.ModelScoped
|
|||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.DesignFeatureToggles
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.ToastMessage
|
||||
import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2
|
||||
import com.tangem.domain.markets.GetTokenMarketCryptoCurrency
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.domain.models.account.filterCryptoPortfolio
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.features.commonfeatures.api.addtoportfolio.*
|
||||
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioFetcher
|
||||
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorController
|
||||
import com.tangem.features.commonfeatures.api.addtoportfolio.*
|
||||
import com.tangem.features.commonfeatures.impl.R
|
||||
import com.tangem.features.commonfeatures.impl.addtoportfolio.AddTokenComponent
|
||||
import com.tangem.features.commonfeatures.impl.addtoportfolio.ChooseNetworkComponent
|
||||
import com.tangem.features.commonfeatures.impl.addtoportfolio.TokenActionsComponent
|
||||
import com.tangem.features.commonfeatures.impl.addtoportfolio.analytics.PortfolioAnalyticsEvent
|
||||
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.UserPortfolioComponent
|
||||
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.state.UserPortfolioStateController
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.Job
|
||||
|
|
@ -40,20 +48,25 @@ import javax.inject.Inject
|
|||
private const val TOKEN_ACTIONS_DELAY = 500L
|
||||
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
internal class AddToPortfolioModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
designFeatureToggles: DesignFeatureToggles,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
val portfolioSelectorController: PortfolioSelectorController,
|
||||
private val callbackDelegate: AddToPortfolioCallbackDelegate,
|
||||
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCaseV2,
|
||||
private val getTokenMarketCryptoCurrency: GetTokenMarketCryptoCurrency,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
val portfolioSelectorController: PortfolioSelectorController,
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val selectionResolver: AddToPortfolioInitialSelectionResolver,
|
||||
userPortfolioStateControllerFactory: UserPortfolioStateController.Factory,
|
||||
) : Model(),
|
||||
ChooseNetworkComponent.Callbacks by callbackDelegate,
|
||||
TokenActionsComponent.Callbacks by callbackDelegate,
|
||||
AddTokenComponent.Callbacks by callbackDelegate {
|
||||
AddTokenComponent.Callbacks by callbackDelegate,
|
||||
UserPortfolioComponent.Callbacks by callbackDelegate {
|
||||
|
||||
private val params = paramsContainer.require<AddToPortfolioComponent.Params>()
|
||||
val navigation = StackNavigation<AddToPortfolioRoutes>()
|
||||
|
|
@ -68,11 +81,24 @@ internal class AddToPortfolioModel @Inject constructor(
|
|||
val portfolioFetcher: PortfolioFetcher = addToPortfolioManager.portfolioFetcher
|
||||
val eventBuilder: MutableSharedFlow<PortfolioAnalyticsEvent.EventBuilder> = replayMutableSharedFlow()
|
||||
|
||||
val userPortfolioStateController = userPortfolioStateControllerFactory.create(
|
||||
modelScope = modelScope,
|
||||
onTokenSelected = { result -> addToPortfolioManager.onAddedTokenClick(result) },
|
||||
)
|
||||
|
||||
val featureData: Flow<AddToPortfolioManager.State> = combineFeatureData()
|
||||
|
||||
private val globalSelectedWallet: UserWallet?
|
||||
get() = getSelectedWalletSyncUseCase().getOrNull()
|
||||
.takeIf { it?.isMultiCurrency == true }
|
||||
|
||||
init {
|
||||
navigation.subscribe { currentStack = it.transformer.invoke(currentStack) }
|
||||
startAddToPortfolioFlow()
|
||||
if (designFeatureToggles.isRedesignEnabled) {
|
||||
startRedesignAddToPortfolioFlow()
|
||||
} else {
|
||||
startLegacyAddToPortfolioFlow()
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> replayMutableSharedFlow() = MutableSharedFlow<T>(
|
||||
|
|
@ -81,7 +107,7 @@ internal class AddToPortfolioModel @Inject constructor(
|
|||
)
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun startAddToPortfolioFlow() {
|
||||
private fun startLegacyAddToPortfolioFlow() {
|
||||
channelFlow<Unit> {
|
||||
fun finishSuccessFlow(result: AddToPortfolioManager.Result) {
|
||||
addToPortfolioManager.onSuccessAdded(result)
|
||||
|
|
@ -208,6 +234,130 @@ internal class AddToPortfolioModel @Inject constructor(
|
|||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun startRedesignAddToPortfolioFlow() {
|
||||
channelFlow<Unit> {
|
||||
fun finishSuccessFlow(result: AddToPortfolioManager.Result) {
|
||||
addToPortfolioManager.onSuccessAdded(result)
|
||||
channel.close()
|
||||
}
|
||||
|
||||
fun finishDismissFlow() {
|
||||
addToPortfolioManager.onDismiss()
|
||||
channel.close()
|
||||
}
|
||||
|
||||
val tokenMarketParams = addToPortfolioManager.paramsFlow.first().token
|
||||
val eb = PortfolioAnalyticsEvent.EventBuilder(
|
||||
tokenSymbol = tokenMarketParams.symbol,
|
||||
source = addToPortfolioManager.analyticsParams.source,
|
||||
)
|
||||
eventBuilder.tryEmit(eb)
|
||||
|
||||
val launchMode = addToPortfolioManager.settings.launchMode
|
||||
val initialData = featureData
|
||||
.filterIsInstance<AddToPortfolioManager.State.Ready>()
|
||||
.map { it.availableToAddData }
|
||||
.first()
|
||||
|
||||
if (launchMode is AddToPortfolioManager.LaunchMode.ViaUserPortfolio &&
|
||||
initialData.hasAnyAddedCurrency(launchMode.rawCurrencyId)
|
||||
) {
|
||||
// suspend, must prepare UM before navigate to UserPortfolio
|
||||
userPortfolioStateController.updateAndWaitNotNullState(initialData, launchMode.rawCurrencyId)
|
||||
navigation.replaceAll(AddToPortfolioRoutes.UserPortfolio)
|
||||
callbackDelegate.onContinueFromUserPortfolio.receiveAsFlow().first()
|
||||
}
|
||||
|
||||
val paramsSnapshot = addToPortfolioManager.paramsFlow.first()
|
||||
val selection = selectionResolver.resolve(
|
||||
availableToAddData = initialData,
|
||||
orderedNetworks = paramsSnapshot.networks,
|
||||
selectedWallet = globalSelectedWallet,
|
||||
tokenParams = tokenMarketParams,
|
||||
) ?: run {
|
||||
finishDismissFlow()
|
||||
return@channelFlow
|
||||
}
|
||||
|
||||
val isAccountMode = portfolioSelectorController.isAccountModeSync()
|
||||
portfolioSelectorController.selectAccount(selection.account.account.accountId)
|
||||
|
||||
val firstSelectedPortfolio = SelectedPortfolio(
|
||||
isAccountMode = isAccountMode,
|
||||
userWallet = selection.userWallet,
|
||||
account = selection.account,
|
||||
isAvailableMorePortfolio = !initialData.isSinglePortfolio,
|
||||
)
|
||||
val firstSelectedNetwork = selection.toSelectedNetwork() ?: run {
|
||||
finishDismissFlow()
|
||||
return@channelFlow
|
||||
}
|
||||
|
||||
selectedPortfolio.emit(firstSelectedPortfolio)
|
||||
selectedNetwork.emit(firstSelectedNetwork)
|
||||
|
||||
analyticsEventHandler.send(event = eventBuilder.first().popupToConfirm())
|
||||
navigation.replaceAll(AddToPortfolioRoutes.AddToken)
|
||||
|
||||
var middleNavigationJob: Job? = null
|
||||
callbackDelegate.onChangeNetworkClick.receiveAsFlow()
|
||||
.onEach {
|
||||
middleNavigationJob?.cancel()
|
||||
middleNavigationJob = changeNetworkNavigationFlow()
|
||||
.launchIn(this)
|
||||
val route = routeToNetworkSelector(selectedPortfolio.first())
|
||||
navigation.pushNew(route)
|
||||
}
|
||||
.launchIn(this)
|
||||
|
||||
callbackDelegate.onChangePortfolioClick.receiveAsFlow()
|
||||
.onEach {
|
||||
middleNavigationJob?.cancel()
|
||||
middleNavigationJob = changePortfolioNavigationNewFlow(
|
||||
data = initialData,
|
||||
orderedNetworks = paramsSnapshot.networks,
|
||||
tokenParams = tokenMarketParams,
|
||||
).launchIn(this)
|
||||
logAccountSelector(isAccountMode)
|
||||
navigation.pushNew(AddToPortfolioRoutes.PortfolioSelector)
|
||||
}
|
||||
.launchIn(this)
|
||||
|
||||
val addedToken = callbackDelegate.onTokenAdded.receiveAsFlow().first()
|
||||
middleNavigationJob?.cancel()
|
||||
val selectedPortfolioSnapshot = selectedPortfolio.first()
|
||||
val result = AddToPortfolioManager.Result(
|
||||
wallet = selectedPortfolioSnapshot.userWallet,
|
||||
account = selectedPortfolioSnapshot.account.account,
|
||||
addedCurrency = addedToken,
|
||||
)
|
||||
|
||||
messageSender.send(ToastMessage(message = resourceReference(R.string.markets_token_added)))
|
||||
|
||||
if (addToPortfolioManager.settings.shouldSkipTokenActionsScreen) {
|
||||
finishSuccessFlow(result)
|
||||
return@channelFlow
|
||||
}
|
||||
|
||||
setupTokenActionsFlow(selectedPortfolioSnapshot, addedToken)
|
||||
.onEach { cryptoCurrencyData ->
|
||||
tokenActionsData.emit(cryptoCurrencyData)
|
||||
navigation.replaceAll(AddToPortfolioRoutes.TokenActions)
|
||||
}
|
||||
.onEmpty { finishSuccessFlow(result) }
|
||||
.launchIn(this)
|
||||
|
||||
callbackDelegate.onLaterClick.receiveAsFlow().first()
|
||||
finishSuccessFlow(result)
|
||||
}
|
||||
.catch { throwable ->
|
||||
TangemLogger.e("Error", throwable)
|
||||
addToPortfolioManager.onDismiss()
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private suspend fun logAccountSelector(isAccountMode: Boolean) {
|
||||
if (isAccountMode) {
|
||||
analyticsEventHandler.send(eventBuilder.first().popupToChooseAccount())
|
||||
|
|
@ -249,6 +399,38 @@ internal class AddToPortfolioModel @Inject constructor(
|
|||
).collect { emit(it) }
|
||||
}
|
||||
|
||||
private fun changePortfolioNavigationNewFlow(
|
||||
data: AvailableToAddData,
|
||||
orderedNetworks: List<TokenMarketInfo.Network>,
|
||||
tokenParams: TokenMarketParams,
|
||||
): Flow<Unit> {
|
||||
return setupPortfolioFlow(data)
|
||||
// drop first selected portfolio or any selected before
|
||||
.drop(1)
|
||||
.map { newPortfolio ->
|
||||
val currentNetwork = selectedNetwork.first().selectedNetwork
|
||||
val availableToAddNetworks = newPortfolio.account.availableToAddNetworks
|
||||
val isSelectedNetworkAvailableForNewPortfolio = availableToAddNetworks
|
||||
.any { it.networkId == currentNetwork.networkId }
|
||||
|
||||
if (!isSelectedNetworkAvailableForNewPortfolio) {
|
||||
val selection = selectionResolver.resolve(
|
||||
availableToAddData = data,
|
||||
orderedNetworks = orderedNetworks,
|
||||
selectedWallet = globalSelectedWallet,
|
||||
tokenParams = tokenParams,
|
||||
accountToAdd = newPortfolio.account,
|
||||
)
|
||||
val newNetwork = selection?.toSelectedNetwork()
|
||||
if (newNetwork != null) {
|
||||
this.selectedNetwork.tryEmit(newNetwork)
|
||||
}
|
||||
}
|
||||
selectedPortfolio.tryEmit(newPortfolio)
|
||||
navigation.popToFirst()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupTokenActionsFlow(
|
||||
selectedPortfolio: SelectedPortfolio,
|
||||
addedToken: CryptoCurrencyStatus,
|
||||
|
|
@ -350,19 +532,34 @@ internal class AddToPortfolioModel @Inject constructor(
|
|||
-> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun AddToPortfolioInitialSelectionResolver.InitialSelection.toSelectedNetwork(): SelectedNetwork? {
|
||||
val crypto = createCryptoCurrency(
|
||||
userWallet = userWallet,
|
||||
network = network,
|
||||
account = account,
|
||||
) ?: return null
|
||||
return SelectedNetwork(
|
||||
cryptoCurrency = crypto,
|
||||
selectedNetwork = network,
|
||||
isAvailableMoreNetwork = !account.isSingleNetwork,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ModelScoped
|
||||
internal class AddToPortfolioCallbackDelegate @Inject constructor() :
|
||||
ChooseNetworkComponent.Callbacks,
|
||||
TokenActionsComponent.Callbacks,
|
||||
AddTokenComponent.Callbacks {
|
||||
AddTokenComponent.Callbacks,
|
||||
UserPortfolioComponent.Callbacks {
|
||||
|
||||
val onNetworkSelected = Channel<TokenMarketInfo.Network>()
|
||||
val onLaterClick = Channel<Unit>()
|
||||
val onChangeNetworkClick = Channel<Unit>()
|
||||
val onChangePortfolioClick = Channel<Unit>()
|
||||
val onTokenAdded = Channel<CryptoCurrencyStatus>()
|
||||
val onContinueFromUserPortfolio = Channel<Unit>()
|
||||
|
||||
override fun onNetworkSelected(network: TokenMarketInfo.Network) {
|
||||
onNetworkSelected.trySend(network)
|
||||
|
|
@ -383,4 +580,19 @@ internal class AddToPortfolioCallbackDelegate @Inject constructor() :
|
|||
override fun onTokenAdded(status: CryptoCurrencyStatus) {
|
||||
onTokenAdded.trySend(status)
|
||||
}
|
||||
|
||||
override fun onContinueFromUserPortfolio() {
|
||||
onContinueFromUserPortfolio.trySend(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
private fun AvailableToAddData.hasAnyAddedCurrency(rawCurrencyId: CryptoCurrency.RawID): Boolean {
|
||||
return availableToAddWallets.values.any { wallet ->
|
||||
wallet.accounts.filterCryptoPortfolio().any { accountStatus ->
|
||||
accountStatus.tokenList.flattenCurrencies().any { status ->
|
||||
val id = status.currency.id.rawCurrencyId ?: return@any false
|
||||
getTokenIdIfL2Network(id.value) == rawCurrencyId.value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,9 @@ internal sealed interface AddToPortfolioRoutes : Route {
|
|||
@Serializable
|
||||
data object AddToken : AddToPortfolioRoutes
|
||||
|
||||
@Serializable
|
||||
data object UserPortfolio : AddToPortfolioRoutes
|
||||
|
||||
@Serializable
|
||||
data object TokenActions : AddToPortfolioRoutes
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ internal class DefaultAddToPortfolioManager @AssistedInject constructor(
|
|||
|
||||
override val onDismiss: Channel<Unit> = Channel()
|
||||
override val onSuccessAdded: Channel<AddToPortfolioManager.Result> = Channel()
|
||||
override val onAddedTokenClick: Channel<AddToPortfolioManager.Result> = Channel()
|
||||
|
||||
override val portfolioFetcher: PortfolioFetcher = portfolioFetcherFactory.create(
|
||||
mode = PortfolioFetcher.Mode.All(isOnlyMultiCurrency = true),
|
||||
|
|
@ -63,6 +64,10 @@ internal class DefaultAddToPortfolioManager @AssistedInject constructor(
|
|||
onSuccessAdded.trySend(result)
|
||||
}
|
||||
|
||||
override fun onAddedTokenClick(result: AddToPortfolioManager.Result) {
|
||||
onAddedTokenClick.trySend(result)
|
||||
}
|
||||
|
||||
override fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) {
|
||||
updateInternal(networks = networks)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
package com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.common.ui.markets.tokenselector.StickyFooter
|
||||
import com.tangem.common.ui.markets.tokenselector.TokenSelectorEmbeddedContent
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.commonfeatures.impl.R
|
||||
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model.UserPortfolioModel
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultUserPortfolioComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted params: UserPortfolioComponent.Params,
|
||||
) : UserPortfolioComponent, AppComponentContext by context {
|
||||
|
||||
private val model: UserPortfolioModel = getOrCreateModel(params)
|
||||
private val onContinueClick: () -> Unit = params.callbacks::onContinueFromUserPortfolio
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val stateFlow = model.state.collectAsStateWithLifecycle()
|
||||
val state = stateFlow.value ?: return
|
||||
TokenSelectorEmbeddedContent(
|
||||
content = state.content,
|
||||
modifier = modifier,
|
||||
stickyFooter = StickyFooter(
|
||||
buttonText = resourceReference(R.string.common_add),
|
||||
isEnabled = state.isAddEnabled,
|
||||
onClick = onContinueClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : UserPortfolioComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: UserPortfolioComponent.Params,
|
||||
): DefaultUserPortfolioComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model.UserPortfolioUM
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
internal interface UserPortfolioComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val uiState: StateFlow<UserPortfolioUM?>,
|
||||
val callbacks: Callbacks,
|
||||
)
|
||||
|
||||
interface Callbacks {
|
||||
fun onContinueFromUserPortfolio()
|
||||
}
|
||||
|
||||
interface Factory : ComponentFactory<Params, UserPortfolioComponent>
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model
|
||||
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.UserPortfolioComponent
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class UserPortfolioModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<UserPortfolioComponent.Params>()
|
||||
|
||||
val state: StateFlow<UserPortfolioUM?> = params.uiState
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.markets.tokenselector.TokenSelectorContentUM
|
||||
|
||||
@Immutable
|
||||
internal data class UserPortfolioUM(
|
||||
val content: TokenSelectorContentUM,
|
||||
val isAddEnabled: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.state
|
||||
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
|
||||
import com.tangem.features.commonfeatures.api.addtoportfolio.AvailableToAddData
|
||||
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model.UserPortfolioUM
|
||||
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.transformer.UserPortfolioSectionsTransformer
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
internal class UserPortfolioStateController @AssistedInject constructor(
|
||||
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
@Assisted private val modelScope: CoroutineScope,
|
||||
@Assisted private val onTokenSelected: (AddToPortfolioManager.Result) -> Unit,
|
||||
) {
|
||||
|
||||
private val requiredDataFlow = MutableSharedFlow<Pair<AvailableToAddData, CryptoCurrency.RawID>>(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
val uiState: StateFlow<UserPortfolioUM?> = combine(
|
||||
flow = requiredDataFlow.distinctUntilChanged(),
|
||||
flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(),
|
||||
flow3 = getBalanceHidingSettingsUseCase.isBalanceHidden(),
|
||||
) { (allAvailableData, rawCurrencyId), appCurrency, isBalanceHidden ->
|
||||
UserPortfolioSectionsTransformer(
|
||||
availableData = allAvailableData,
|
||||
rawCurrencyId = rawCurrencyId,
|
||||
appCurrency = appCurrency,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
onTokenSelected = onTokenSelected,
|
||||
).transform()
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.stateIn(modelScope, SharingStarted.Lazily, null)
|
||||
|
||||
suspend fun updateAndWaitNotNullState(allAvailableData: AvailableToAddData, rawCurrencyId: CryptoCurrency.RawID) {
|
||||
requiredDataFlow.tryEmit(allAvailableData to rawCurrencyId)
|
||||
uiState.filterNotNull().firstOrNull()
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(
|
||||
modelScope: CoroutineScope,
|
||||
onTokenSelected: (AddToPortfolioManager.Result) -> Unit,
|
||||
): UserPortfolioStateController
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
package com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.transformer
|
||||
|
||||
import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.common.ui.markets.tokenselector.*
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeState
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.account.filterCryptoPortfolio
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
|
||||
import com.tangem.features.commonfeatures.api.addtoportfolio.AvailableToAddData
|
||||
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model.UserPortfolioUM
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class UserPortfolioSectionsTransformer(
|
||||
private val availableData: AvailableToAddData,
|
||||
private val rawCurrencyId: CryptoCurrency.RawID,
|
||||
private val appCurrency: AppCurrency,
|
||||
private val isBalanceHidden: Boolean,
|
||||
private val onTokenSelected: (AddToPortfolioManager.Result) -> Unit,
|
||||
) {
|
||||
|
||||
private val iconConverter = CryptoCurrencyToIconStateConverter()
|
||||
|
||||
fun transform(): UserPortfolioUM {
|
||||
return UserPortfolioUM(
|
||||
content = TokenSelectorContentUM(
|
||||
sections = buildSections(entries = generateEntries(availableData)).toImmutableList(),
|
||||
),
|
||||
isAddEnabled = availableData.isAvailableToAdd,
|
||||
)
|
||||
}
|
||||
|
||||
private fun generateEntries(data: AvailableToAddData): List<PortfolioEntry> {
|
||||
return data.availableToAddWallets.values.flatMap { wallet ->
|
||||
wallet.accounts.filterCryptoPortfolio().flatMap { accountStatus ->
|
||||
accountStatus.tokenList.flattenCurrencies()
|
||||
.filter { status -> status.currency.matchesRawId(rawCurrencyId) }
|
||||
.map { status ->
|
||||
PortfolioEntry(
|
||||
wallet = wallet.userWallet,
|
||||
account = accountStatus,
|
||||
currencyStatus = status,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildSections(entries: List<PortfolioEntry>): List<TokenSelectorSectionUM> {
|
||||
val sections = mutableListOf<TokenSelectorSectionUM>()
|
||||
val byWallet = entries.groupBy { it.wallet.walletId }
|
||||
val shouldShowWalletHeaders = byWallet.size > 1
|
||||
|
||||
for ((_, walletEntries) in byWallet) {
|
||||
if (shouldShowWalletHeaders) {
|
||||
sections.add(
|
||||
TokenSelectorSectionUM.WalletHeader(walletName = walletEntries.first().wallet.name),
|
||||
)
|
||||
}
|
||||
|
||||
val byAccount = walletEntries.groupBy { it.account.account.accountId }
|
||||
val shouldShowAccountHeaders = byAccount.size > 1
|
||||
|
||||
for ((_, accountEntries) in byAccount) {
|
||||
val singles = accountEntries.map(::entryToSingle).toImmutableList()
|
||||
val accountHeader = if (shouldShowAccountHeaders) {
|
||||
val first = accountEntries.first()
|
||||
AccountHeaderData(
|
||||
accountName = first
|
||||
.account
|
||||
.account
|
||||
.accountName
|
||||
.toUM()
|
||||
.value,
|
||||
cryptoPortfolioIcon = first.account.account.icon,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
sections.add(
|
||||
TokenSelectorSectionUM.TokenGroup(accountHeader = accountHeader, items = singles),
|
||||
)
|
||||
}
|
||||
}
|
||||
return sections
|
||||
}
|
||||
|
||||
private fun entryToSingle(entry: PortfolioEntry): UserAssetItemUM.Single {
|
||||
val currency = entry.currencyStatus.currency
|
||||
val value = entry.currencyStatus.value
|
||||
return UserAssetItemUM.Single(
|
||||
id = "${entry.wallet.walletId.stringValue}_${entry.account.account.accountId.value}_${currency.id.value}",
|
||||
icon = TangemIconUM.Currency(currencyIconState = iconConverter.convert(entry.currencyStatus)),
|
||||
tokenName = currency.name,
|
||||
tokenSymbol = currency.symbol,
|
||||
fiatRate = value.fiatRate?.format { fiat(appCurrency.code, appCurrency.symbol) },
|
||||
priceChangeState = when (value) {
|
||||
is CryptoCurrencyStatus.Loading,
|
||||
is CryptoCurrencyStatus.Unreachable,
|
||||
is CryptoCurrencyStatus.MissedDerivation,
|
||||
is CryptoCurrencyStatus.NoAmount,
|
||||
-> PriceChangeState.Unknown
|
||||
else -> PriceChangeState.Content(
|
||||
type = PriceChangeType.fromBigDecimal(value.priceChange.orZero()),
|
||||
valueInPercent = value.priceChange.format { percent() },
|
||||
)
|
||||
},
|
||||
balanceState = convertBalanceState(value, currency.symbol, currency.decimals),
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
onClick = {
|
||||
onTokenSelected(
|
||||
AddToPortfolioManager.Result(
|
||||
wallet = entry.wallet,
|
||||
account = entry.account,
|
||||
addedCurrency = entry.currencyStatus,
|
||||
),
|
||||
)
|
||||
},
|
||||
networkName = currency.network.name,
|
||||
)
|
||||
}
|
||||
|
||||
private data class PortfolioEntry(
|
||||
val wallet: UserWallet,
|
||||
val account: AccountStatus.CryptoPortfolio,
|
||||
val currencyStatus: CryptoCurrencyStatus,
|
||||
)
|
||||
|
||||
private fun convertBalanceState(
|
||||
value: CryptoCurrencyStatus.Value,
|
||||
symbol: String,
|
||||
decimals: Int,
|
||||
): BalanceDisplayState {
|
||||
return when {
|
||||
value is CryptoCurrencyStatus.Loading && value.amount != null ->
|
||||
BalanceDisplayState.Flickering(
|
||||
cryptoBalance = stringReference(formatCryptoAmount(value.amount, symbol, decimals)),
|
||||
fiatBalance = stringReference(
|
||||
value.fiatAmount?.format { fiat(appCurrency.code, appCurrency.symbol) }
|
||||
?: StringsSigns.DASH_SIGN,
|
||||
),
|
||||
)
|
||||
value is CryptoCurrencyStatus.Loading -> BalanceDisplayState.Loading
|
||||
value is CryptoCurrencyStatus.Unreachable -> BalanceDisplayState.Unreachable
|
||||
value.isError && value.amount != null ->
|
||||
BalanceDisplayState.Stale(
|
||||
cryptoBalance = stringReference(formatCryptoAmount(value.amount, symbol, decimals)),
|
||||
fiatBalance = stringReference(
|
||||
value.fiatAmount?.format { fiat(appCurrency.code, appCurrency.symbol) }
|
||||
?: StringsSigns.DASH_SIGN,
|
||||
),
|
||||
)
|
||||
value.isError -> BalanceDisplayState.Unreachable
|
||||
else -> BalanceDisplayState.Loaded(
|
||||
cryptoBalance = stringReference(formatCryptoAmount(value.amount, symbol, decimals)),
|
||||
fiatBalance = stringReference(
|
||||
value.fiatAmount?.format { fiat(appCurrency.code, appCurrency.symbol) }
|
||||
?: StringsSigns.DASH_SIGN,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatCryptoAmount(amount: BigDecimal?, symbol: String, decimals: Int): String {
|
||||
return amount?.format { crypto(symbol, decimals) } ?: StringsSigns.DASH_SIGN
|
||||
}
|
||||
|
||||
private fun CryptoCurrency.matchesRawId(target: CryptoCurrency.RawID): Boolean {
|
||||
val rawId = id.rawCurrencyId ?: return false
|
||||
return getTokenIdIfL2Network(rawId.value) == target.value
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,591 @@
|
|||
package com.tangem.features.commonfeatures.impl.addtoportfolio.model
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.domain.markets.GetTokenMarketCryptoCurrency
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase
|
||||
import com.tangem.features.commonfeatures.api.addtoportfolio.AvailableToAddAccount
|
||||
import com.tangem.features.commonfeatures.api.addtoportfolio.AvailableToAddData
|
||||
import com.tangem.features.commonfeatures.api.addtoportfolio.AvailableToAddWallet
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class AddToPortfolioInitialSelectionResolverTest {
|
||||
|
||||
private val getTokenMarketCryptoCurrency: GetTokenMarketCryptoCurrency = mockk()
|
||||
private val networkHasDerivationUseCase: NetworkHasDerivationUseCase = mockk()
|
||||
|
||||
private val tokenParams: TokenMarketParams = mockk()
|
||||
|
||||
private lateinit var resolver: AddToPortfolioInitialSelectionResolver
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
clearMocks(getTokenMarketCryptoCurrency, networkHasDerivationUseCase)
|
||||
coEvery { getTokenMarketCryptoCurrency(any(), any(), any(), any()) } returns null
|
||||
every { networkHasDerivationUseCase(any(), any()) } returns false.right()
|
||||
|
||||
resolver = AddToPortfolioInitialSelectionResolver(
|
||||
getTokenMarketCryptoCurrency = getTokenMarketCryptoCurrency,
|
||||
networkHasDerivationUseCase = networkHasDerivationUseCase,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no wallets in data WHEN resolve THEN return null`() = runTest {
|
||||
val data = availableData(wallets = emptyMap())
|
||||
|
||||
val result = resolver.resolve(
|
||||
availableToAddData = data,
|
||||
orderedNetworks = listOf(ETHEREUM),
|
||||
selectedWallet = null,
|
||||
tokenParams = tokenParams,
|
||||
)
|
||||
|
||||
Truth.assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN accountToAdd is provided WHEN resolve THEN use it instead of looking up account`() = runTest {
|
||||
val walletId = UserWalletId(WALLET_ID_A)
|
||||
val userWallet = userWallet(walletId)
|
||||
val mainAccount = availableAccount()
|
||||
val explicitAccount = availableAccount(availableToAddNetworks = setOf(BITCOIN))
|
||||
|
||||
val data = availableData(
|
||||
wallets = mapOf(
|
||||
walletId to walletEntry(
|
||||
userWallet = userWallet,
|
||||
accounts = mapOf(AccountId.forMainCryptoPortfolio(walletId) to mainAccount),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val result = resolver.resolve(
|
||||
availableToAddData = data,
|
||||
orderedNetworks = listOf(BITCOIN),
|
||||
selectedWallet = userWallet,
|
||||
tokenParams = tokenParams,
|
||||
accountToAdd = explicitAccount,
|
||||
)
|
||||
|
||||
Truth.assertThat(result).isNotNull()
|
||||
Truth.assertThat(result!!.account).isSameInstanceAs(explicitAccount)
|
||||
Truth.assertThat(result.network).isEqualTo(BITCOIN)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN accountToAdd is not available to add WHEN resolve THEN still use it`() = runTest {
|
||||
val walletId = UserWalletId(WALLET_ID_A)
|
||||
val userWallet = userWallet(walletId)
|
||||
val explicitAccount = availableAccount(isAvailableToAdd = false, availableToAddNetworks = setOf(ETHEREUM))
|
||||
|
||||
val data = availableData(
|
||||
wallets = mapOf(
|
||||
walletId to walletEntry(
|
||||
userWallet = userWallet,
|
||||
accounts = mapOf(AccountId.forMainCryptoPortfolio(walletId) to availableAccount()),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val result = resolver.resolve(
|
||||
availableToAddData = data,
|
||||
orderedNetworks = listOf(ETHEREUM),
|
||||
selectedWallet = userWallet,
|
||||
tokenParams = tokenParams,
|
||||
accountToAdd = explicitAccount,
|
||||
)
|
||||
|
||||
Truth.assertThat(result).isNotNull()
|
||||
Truth.assertThat(result!!.account).isSameInstanceAs(explicitAccount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN accountToAdd with no matching ordered networks WHEN resolve THEN fall back to first ordered network`() =
|
||||
runTest {
|
||||
val walletId = UserWalletId(WALLET_ID_A)
|
||||
val userWallet = userWallet(walletId)
|
||||
val explicitAccount = availableAccount(availableToAddNetworks = setOf(ETHEREUM))
|
||||
|
||||
val data = availableData(
|
||||
wallets = mapOf(
|
||||
walletId to walletEntry(
|
||||
userWallet = userWallet,
|
||||
accounts = mapOf(AccountId.forMainCryptoPortfolio(walletId) to availableAccount()),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val result = resolver.resolve(
|
||||
availableToAddData = data,
|
||||
orderedNetworks = listOf(BITCOIN),
|
||||
selectedWallet = userWallet,
|
||||
tokenParams = tokenParams,
|
||||
accountToAdd = explicitAccount,
|
||||
)
|
||||
|
||||
Truth.assertThat(result).isNotNull()
|
||||
Truth.assertThat(result!!.account).isSameInstanceAs(explicitAccount)
|
||||
Truth.assertThat(result.network).isEqualTo(BITCOIN)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN selected wallet is in data WHEN resolve THEN pick its entry`() = runTest {
|
||||
val selectedWalletId = UserWalletId(WALLET_ID_A)
|
||||
val otherWalletId = UserWalletId(WALLET_ID_B)
|
||||
val selectedUserWallet = userWallet(selectedWalletId)
|
||||
val otherUserWallet = userWallet(otherWalletId)
|
||||
|
||||
val selectedAccount = availableAccount()
|
||||
val otherAccount = availableAccount()
|
||||
|
||||
val data = availableData(
|
||||
wallets = linkedMapOf(
|
||||
otherWalletId to walletEntry(otherUserWallet, mapOf(AccountId.forMainCryptoPortfolio(otherWalletId) to otherAccount)),
|
||||
selectedWalletId to walletEntry(selectedUserWallet, mapOf(AccountId.forMainCryptoPortfolio(selectedWalletId) to selectedAccount)),
|
||||
),
|
||||
)
|
||||
|
||||
val result = resolver.resolve(
|
||||
availableToAddData = data,
|
||||
orderedNetworks = listOf(ETHEREUM),
|
||||
selectedWallet = selectedUserWallet,
|
||||
tokenParams = tokenParams,
|
||||
)
|
||||
|
||||
Truth.assertThat(result).isNotNull()
|
||||
Truth.assertThat(result!!.userWallet).isSameInstanceAs(selectedUserWallet)
|
||||
Truth.assertThat(result.account).isSameInstanceAs(selectedAccount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN selected wallet is not in data WHEN resolve THEN fall back to first wallet`() = runTest {
|
||||
val firstWalletId = UserWalletId(WALLET_ID_A)
|
||||
val firstUserWallet = userWallet(firstWalletId)
|
||||
val firstAccount = availableAccount()
|
||||
|
||||
val data = availableData(
|
||||
wallets = mapOf(
|
||||
firstWalletId to walletEntry(firstUserWallet, mapOf(AccountId.forMainCryptoPortfolio(firstWalletId) to firstAccount)),
|
||||
),
|
||||
)
|
||||
|
||||
val result = resolver.resolve(
|
||||
availableToAddData = data,
|
||||
orderedNetworks = listOf(ETHEREUM),
|
||||
selectedWallet = userWallet(UserWalletId(WALLET_ID_B)),
|
||||
tokenParams = tokenParams,
|
||||
)
|
||||
|
||||
Truth.assertThat(result).isNotNull()
|
||||
Truth.assertThat(result!!.userWallet).isSameInstanceAs(firstUserWallet)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN selected wallet is null WHEN resolve THEN fall back to first wallet`() = runTest {
|
||||
val firstWalletId = UserWalletId(WALLET_ID_A)
|
||||
val firstUserWallet = userWallet(firstWalletId)
|
||||
val firstAccount = availableAccount()
|
||||
|
||||
val data = availableData(
|
||||
wallets = mapOf(
|
||||
firstWalletId to walletEntry(firstUserWallet, mapOf(AccountId.forMainCryptoPortfolio(firstWalletId) to firstAccount)),
|
||||
),
|
||||
)
|
||||
|
||||
val result = resolver.resolve(
|
||||
availableToAddData = data,
|
||||
orderedNetworks = listOf(ETHEREUM),
|
||||
selectedWallet = null,
|
||||
tokenParams = tokenParams,
|
||||
)
|
||||
|
||||
Truth.assertThat(result).isNotNull()
|
||||
Truth.assertThat(result!!.userWallet).isSameInstanceAs(firstUserWallet)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN main account is available WHEN resolve THEN pick main account`() = runTest {
|
||||
val walletId = UserWalletId(WALLET_ID_A)
|
||||
val userWallet = userWallet(walletId)
|
||||
val mainAccount = availableAccount()
|
||||
val otherAccount = availableAccount()
|
||||
|
||||
val data = availableData(
|
||||
wallets = mapOf(
|
||||
walletId to walletEntry(
|
||||
userWallet = userWallet,
|
||||
accounts = linkedMapOf(
|
||||
AccountId.forPaymentAccount(walletId) to otherAccount,
|
||||
AccountId.forMainCryptoPortfolio(walletId) to mainAccount,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val result = resolver.resolve(
|
||||
availableToAddData = data,
|
||||
orderedNetworks = listOf(ETHEREUM),
|
||||
selectedWallet = userWallet,
|
||||
tokenParams = tokenParams,
|
||||
)
|
||||
|
||||
Truth.assertThat(result).isNotNull()
|
||||
Truth.assertThat(result!!.account).isSameInstanceAs(mainAccount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN main account is not available WHEN resolve THEN pick first available account`() = runTest {
|
||||
val walletId = UserWalletId(WALLET_ID_A)
|
||||
val userWallet = userWallet(walletId)
|
||||
val mainAccount = availableAccount(isAvailableToAdd = false, availableToAddNetworks = emptySet())
|
||||
val secondAvailable = availableAccount()
|
||||
|
||||
val data = availableData(
|
||||
wallets = mapOf(
|
||||
walletId to walletEntry(
|
||||
userWallet = userWallet,
|
||||
accounts = linkedMapOf(
|
||||
AccountId.forMainCryptoPortfolio(walletId) to mainAccount,
|
||||
AccountId.forPaymentAccount(walletId) to secondAvailable,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val result = resolver.resolve(
|
||||
availableToAddData = data,
|
||||
orderedNetworks = listOf(ETHEREUM),
|
||||
selectedWallet = userWallet,
|
||||
tokenParams = tokenParams,
|
||||
)
|
||||
|
||||
Truth.assertThat(result).isNotNull()
|
||||
Truth.assertThat(result!!.account).isSameInstanceAs(secondAvailable)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no available accounts WHEN resolve THEN fall back to that account with first ordered network`() =
|
||||
runTest {
|
||||
val walletId = UserWalletId(WALLET_ID_A)
|
||||
val userWallet = userWallet(walletId)
|
||||
val unavailable = availableAccount(isAvailableToAdd = false, availableToAddNetworks = emptySet())
|
||||
|
||||
val data = availableData(
|
||||
wallets = mapOf(
|
||||
walletId to walletEntry(
|
||||
userWallet = userWallet,
|
||||
accounts = mapOf(AccountId.forMainCryptoPortfolio(walletId) to unavailable),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val result = resolver.resolve(
|
||||
availableToAddData = data,
|
||||
orderedNetworks = listOf(ETHEREUM),
|
||||
selectedWallet = userWallet,
|
||||
tokenParams = tokenParams,
|
||||
)
|
||||
|
||||
Truth.assertThat(result).isNotNull()
|
||||
Truth.assertThat(result!!.userWallet).isSameInstanceAs(userWallet)
|
||||
Truth.assertThat(result.account).isSameInstanceAs(unavailable)
|
||||
Truth.assertThat(result.network).isEqualTo(ETHEREUM)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN ordered networks do not match account networks WHEN resolve THEN fall back to first ordered network`() =
|
||||
runTest {
|
||||
val walletId = UserWalletId(WALLET_ID_A)
|
||||
val userWallet = userWallet(walletId)
|
||||
val account = availableAccount(availableToAddNetworks = setOf(ETHEREUM))
|
||||
|
||||
val data = availableData(
|
||||
wallets = mapOf(
|
||||
walletId to walletEntry(
|
||||
userWallet = userWallet,
|
||||
accounts = mapOf(AccountId.forMainCryptoPortfolio(walletId) to account),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val result = resolver.resolve(
|
||||
availableToAddData = data,
|
||||
orderedNetworks = listOf(BITCOIN),
|
||||
selectedWallet = userWallet,
|
||||
tokenParams = tokenParams,
|
||||
)
|
||||
|
||||
Truth.assertThat(result).isNotNull()
|
||||
Truth.assertThat(result!!.userWallet).isSameInstanceAs(userWallet)
|
||||
Truth.assertThat(result.account).isSameInstanceAs(account)
|
||||
Truth.assertThat(result.network).isEqualTo(BITCOIN)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN preferred wallet has no available accounts but another wallet does WHEN resolve THEN pick the other`() =
|
||||
runTest {
|
||||
val preferredWalletId = UserWalletId(WALLET_ID_A)
|
||||
val otherWalletId = UserWalletId(WALLET_ID_B)
|
||||
val preferredUserWallet = userWallet(preferredWalletId)
|
||||
val otherUserWallet = userWallet(otherWalletId)
|
||||
|
||||
val preferredUnavailable = availableAccount(isAvailableToAdd = false, availableToAddNetworks = emptySet())
|
||||
val otherAvailable = availableAccount(availableToAddNetworks = setOf(ETHEREUM))
|
||||
|
||||
val data = availableData(
|
||||
wallets = linkedMapOf(
|
||||
preferredWalletId to walletEntry(
|
||||
userWallet = preferredUserWallet,
|
||||
accounts = mapOf(AccountId.forMainCryptoPortfolio(preferredWalletId) to preferredUnavailable),
|
||||
),
|
||||
otherWalletId to walletEntry(
|
||||
userWallet = otherUserWallet,
|
||||
accounts = mapOf(AccountId.forMainCryptoPortfolio(otherWalletId) to otherAvailable),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val result = resolver.resolve(
|
||||
availableToAddData = data,
|
||||
orderedNetworks = listOf(ETHEREUM),
|
||||
selectedWallet = preferredUserWallet,
|
||||
tokenParams = tokenParams,
|
||||
)
|
||||
|
||||
Truth.assertThat(result).isNotNull()
|
||||
Truth.assertThat(result!!.userWallet).isSameInstanceAs(otherUserWallet)
|
||||
Truth.assertThat(result.account).isSameInstanceAs(otherAvailable)
|
||||
Truth.assertThat(result.network).isEqualTo(ETHEREUM)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no wallet has viable combo WHEN resolve THEN fall back to preferred wallet with first ordered network`() =
|
||||
runTest {
|
||||
val preferredWalletId = UserWalletId(WALLET_ID_A)
|
||||
val otherWalletId = UserWalletId(WALLET_ID_B)
|
||||
val preferredUserWallet = userWallet(preferredWalletId)
|
||||
val otherUserWallet = userWallet(otherWalletId)
|
||||
|
||||
val preferredUnavailable = availableAccount(isAvailableToAdd = false, availableToAddNetworks = emptySet())
|
||||
val otherUnavailable = availableAccount(isAvailableToAdd = false, availableToAddNetworks = emptySet())
|
||||
|
||||
val data = availableData(
|
||||
wallets = linkedMapOf(
|
||||
otherWalletId to walletEntry(
|
||||
userWallet = otherUserWallet,
|
||||
accounts = mapOf(AccountId.forMainCryptoPortfolio(otherWalletId) to otherUnavailable),
|
||||
),
|
||||
preferredWalletId to walletEntry(
|
||||
userWallet = preferredUserWallet,
|
||||
accounts = mapOf(AccountId.forMainCryptoPortfolio(preferredWalletId) to preferredUnavailable),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val result = resolver.resolve(
|
||||
availableToAddData = data,
|
||||
orderedNetworks = listOf(ETHEREUM),
|
||||
selectedWallet = preferredUserWallet,
|
||||
tokenParams = tokenParams,
|
||||
)
|
||||
|
||||
Truth.assertThat(result).isNotNull()
|
||||
Truth.assertThat(result!!.userWallet).isSameInstanceAs(preferredUserWallet)
|
||||
Truth.assertThat(result.account).isSameInstanceAs(preferredUnavailable)
|
||||
Truth.assertThat(result.network).isEqualTo(ETHEREUM)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN first ordered network has derivation WHEN resolve THEN pick first`() = runTest {
|
||||
val walletId = UserWalletId(WALLET_ID_A)
|
||||
val userWallet = userWallet(walletId)
|
||||
val account = availableAccount(availableToAddNetworks = setOf(ETHEREUM, BITCOIN))
|
||||
|
||||
val ethereumCurrency = cryptoCurrency()
|
||||
val bitcoinCurrency = cryptoCurrency()
|
||||
coEvery { getTokenMarketCryptoCurrency(any(), any(), ETHEREUM, any()) } returns ethereumCurrency
|
||||
coEvery { getTokenMarketCryptoCurrency(any(), any(), BITCOIN, any()) } returns bitcoinCurrency
|
||||
every { networkHasDerivationUseCase(any(), ethereumCurrency.network) } returns true.right()
|
||||
every { networkHasDerivationUseCase(any(), bitcoinCurrency.network) } returns true.right()
|
||||
|
||||
val data = availableData(
|
||||
wallets = mapOf(
|
||||
walletId to walletEntry(
|
||||
userWallet = userWallet,
|
||||
accounts = mapOf(AccountId.forMainCryptoPortfolio(walletId) to account),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val result = resolver.resolve(
|
||||
availableToAddData = data,
|
||||
orderedNetworks = listOf(ETHEREUM, BITCOIN),
|
||||
selectedWallet = userWallet,
|
||||
tokenParams = tokenParams,
|
||||
)
|
||||
|
||||
Truth.assertThat(result?.network).isEqualTo(ETHEREUM)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN only second ordered network has derivation WHEN resolve THEN pick second`() = runTest {
|
||||
val walletId = UserWalletId(WALLET_ID_A)
|
||||
val userWallet = userWallet(walletId)
|
||||
val account = availableAccount(availableToAddNetworks = setOf(ETHEREUM, BITCOIN))
|
||||
|
||||
val ethereumCurrency = cryptoCurrency()
|
||||
val bitcoinCurrency = cryptoCurrency()
|
||||
coEvery { getTokenMarketCryptoCurrency(any(), any(), ETHEREUM, any()) } returns ethereumCurrency
|
||||
coEvery { getTokenMarketCryptoCurrency(any(), any(), BITCOIN, any()) } returns bitcoinCurrency
|
||||
every { networkHasDerivationUseCase(any(), ethereumCurrency.network) } returns false.right()
|
||||
every { networkHasDerivationUseCase(any(), bitcoinCurrency.network) } returns true.right()
|
||||
|
||||
val data = availableData(
|
||||
wallets = mapOf(
|
||||
walletId to walletEntry(
|
||||
userWallet = userWallet,
|
||||
accounts = mapOf(AccountId.forMainCryptoPortfolio(walletId) to account),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val result = resolver.resolve(
|
||||
availableToAddData = data,
|
||||
orderedNetworks = listOf(ETHEREUM, BITCOIN),
|
||||
selectedWallet = userWallet,
|
||||
tokenParams = tokenParams,
|
||||
)
|
||||
|
||||
Truth.assertThat(result?.network).isEqualTo(BITCOIN)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no network has derivation WHEN resolve THEN fall back to first ordered available network`() = runTest {
|
||||
val walletId = UserWalletId(WALLET_ID_A)
|
||||
val userWallet = userWallet(walletId)
|
||||
val account = availableAccount(availableToAddNetworks = setOf(ETHEREUM, BITCOIN))
|
||||
|
||||
val ethereumCurrency = cryptoCurrency()
|
||||
val bitcoinCurrency = cryptoCurrency()
|
||||
coEvery { getTokenMarketCryptoCurrency(any(), any(), ETHEREUM, any()) } returns ethereumCurrency
|
||||
coEvery { getTokenMarketCryptoCurrency(any(), any(), BITCOIN, any()) } returns bitcoinCurrency
|
||||
every { networkHasDerivationUseCase(any(), any()) } returns Throwable().left()
|
||||
|
||||
val data = availableData(
|
||||
wallets = mapOf(
|
||||
walletId to walletEntry(
|
||||
userWallet = userWallet,
|
||||
accounts = mapOf(AccountId.forMainCryptoPortfolio(walletId) to account),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val result = resolver.resolve(
|
||||
availableToAddData = data,
|
||||
orderedNetworks = listOf(BITCOIN, ETHEREUM),
|
||||
selectedWallet = userWallet,
|
||||
tokenParams = tokenParams,
|
||||
)
|
||||
|
||||
Truth.assertThat(result?.network).isEqualTo(BITCOIN)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN get token market crypto currency returns null WHEN resolve THEN fall back to first ordered available network`() = runTest {
|
||||
val walletId = UserWalletId(WALLET_ID_A)
|
||||
val userWallet = userWallet(walletId)
|
||||
val account = availableAccount(availableToAddNetworks = setOf(ETHEREUM, BITCOIN))
|
||||
|
||||
coEvery { getTokenMarketCryptoCurrency(any(), any(), any(), any()) } returns null
|
||||
|
||||
val data = availableData(
|
||||
wallets = mapOf(
|
||||
walletId to walletEntry(
|
||||
userWallet = userWallet,
|
||||
accounts = mapOf(AccountId.forMainCryptoPortfolio(walletId) to account),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val result = resolver.resolve(
|
||||
availableToAddData = data,
|
||||
orderedNetworks = listOf(BITCOIN, ETHEREUM),
|
||||
selectedWallet = userWallet,
|
||||
tokenParams = tokenParams,
|
||||
)
|
||||
|
||||
Truth.assertThat(result?.network).isEqualTo(BITCOIN)
|
||||
}
|
||||
|
||||
// region Helpers
|
||||
|
||||
private fun availableData(wallets: Map<UserWalletId, AvailableToAddWallet>): AvailableToAddData = mockk {
|
||||
every { isAvailableToAdd } returns true
|
||||
every { availableToAddWallets } returns wallets
|
||||
}
|
||||
|
||||
private fun walletEntry(
|
||||
userWallet: UserWallet,
|
||||
accounts: Map<AccountId, AvailableToAddAccount>,
|
||||
): AvailableToAddWallet = mockk {
|
||||
every { this@mockk.userWallet } returns userWallet
|
||||
every { availableToAddAccounts } returns accounts
|
||||
}
|
||||
|
||||
private fun availableAccount(
|
||||
isAvailableToAdd: Boolean = true,
|
||||
availableToAddNetworks: Set<TokenMarketInfo.Network> = setOf(ETHEREUM),
|
||||
derivationIndex: DerivationIndex = DerivationIndex.Main,
|
||||
): AvailableToAddAccount = mockk {
|
||||
every { this@mockk.isAvailableToAdd } returns isAvailableToAdd
|
||||
every { this@mockk.availableToAddNetworks } returns availableToAddNetworks
|
||||
every { account.account.derivationIndex } returns derivationIndex
|
||||
}
|
||||
|
||||
private fun userWallet(walletId: UserWalletId): UserWallet = mockk {
|
||||
every { this@mockk.walletId } returns walletId
|
||||
}
|
||||
|
||||
private fun cryptoCurrency(): CryptoCurrency = mockk {
|
||||
every { network } returns mockk<Network>()
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
private companion object {
|
||||
const val WALLET_ID_A = "011f"
|
||||
const val WALLET_ID_B = "022e"
|
||||
|
||||
val ETHEREUM = TokenMarketInfo.Network(
|
||||
networkId = "ethereum",
|
||||
isExchangeable = true,
|
||||
contractAddress = null,
|
||||
decimalCount = 18,
|
||||
)
|
||||
|
||||
val BITCOIN = TokenMarketInfo.Network(
|
||||
networkId = "bitcoin",
|
||||
isExchangeable = true,
|
||||
contractAddress = null,
|
||||
decimalCount = 8,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue