Updated on 2026-08-14
This commit is contained in:
parent
0edc7eb8a8
commit
b3138bf560
11 changed files with 707 additions and 37 deletions
|
|
@ -75,7 +75,6 @@ internal class HotCryptoCurrencyConverter(
|
|||
}
|
||||
}
|
||||
|
||||
// TODO account
|
||||
private fun createNetwork(networkId: String): Network? {
|
||||
val blockchain = Blockchain.fromNetworkId(networkId) ?: return null
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ android {
|
|||
|
||||
dependencies {
|
||||
/** Project - API */
|
||||
implementation(projects.features.account.api)
|
||||
implementation(projects.features.onramp.api)
|
||||
implementation(projects.features.swap.domain)
|
||||
implementation(projects.features.swap.domain.api)
|
||||
|
|
|
|||
|
|
@ -1,21 +1,45 @@
|
|||
package com.tangem.features.onramp.hottokens
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.slot.childSlot
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.arkivanov.decompose.router.stack.*
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
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.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.features.account.PortfolioSelectorComponent
|
||||
import com.tangem.features.onramp.hottokens.model.HotCryptoModel
|
||||
import com.tangem.features.onramp.hottokens.portfolio.OnrampAddToPortfolioComponent
|
||||
import com.tangem.features.onramp.hottokens.portfolio.OnrampAddTokenComponent
|
||||
import com.tangem.features.onramp.hottokens.portfolio.entity.OnrampAddToPortfolioBSConfig
|
||||
import com.tangem.features.onramp.hottokens.portfolio.entity.OnrampAddTokenRoute
|
||||
import com.tangem.features.onramp.hottokens.ui.HotCrypto
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
|
@ -24,10 +48,40 @@ internal class DefaultHotCryptoComponent @AssistedInject constructor(
|
|||
@Assisted context: AppComponentContext,
|
||||
@Assisted private val params: HotCryptoComponent.Params,
|
||||
private val onrampAddToPortfolioComponentFactory: OnrampAddToPortfolioComponent.Factory,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
portfolioSelectorComponentFactory: PortfolioSelectorComponent.Factory,
|
||||
addTokenComponentFactory: OnrampAddTokenComponent.Factory,
|
||||
) : HotCryptoComponent, AppComponentContext by context {
|
||||
|
||||
private val model: HotCryptoModel = getOrCreateModel(params)
|
||||
|
||||
private val portfolioSelectorComponent: PortfolioSelectorComponent? by lazy {
|
||||
if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
portfolioSelectorComponentFactory.create(
|
||||
context = child("portfolioSelectorComponent"),
|
||||
params = PortfolioSelectorComponent.Params(
|
||||
portfolioFetcher = model.portfolioFetcher!!,
|
||||
controller = model.portfolioSelectorController,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
private val addTokenComponent: OnrampAddTokenComponent? by lazy {
|
||||
if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
addTokenComponentFactory.create(
|
||||
context = child("addTokenComponent"),
|
||||
params = OnrampAddTokenComponent.Params(
|
||||
callbacks = model,
|
||||
tokenToAdd = model.hotCryptoToAddDataFlow,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = model.bottomSheetNavigation,
|
||||
serializer = null,
|
||||
|
|
@ -35,6 +89,15 @@ internal class DefaultHotCryptoComponent @AssistedInject constructor(
|
|||
childFactory = ::bottomSheetChild,
|
||||
)
|
||||
|
||||
private val childStack = childStack(
|
||||
key = "addHotCryptoToPortfolioStack",
|
||||
handleBackButton = true,
|
||||
source = model.bottomSheetNavigationV2,
|
||||
serializer = OnrampAddTokenRoute.serializer(),
|
||||
initialConfiguration = OnrampAddTokenRoute.Empty,
|
||||
childFactory = { config, _ -> contentChild(config) },
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state = model.state.collectAsStateWithLifecycle()
|
||||
|
|
@ -43,6 +106,98 @@ internal class DefaultHotCryptoComponent @AssistedInject constructor(
|
|||
HotCrypto(state, modifier)
|
||||
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
AddHotCryptoBottomSheet()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddHotCryptoBottomSheet() {
|
||||
val stack by childStack.subscribeAsState()
|
||||
val contentStack = remember { mutableStateOf(stack) }
|
||||
val currentRoute = stack.active.configuration
|
||||
val isNotEmpty = currentRoute != OnrampAddTokenRoute.Empty
|
||||
if (isNotEmpty) {
|
||||
contentStack.value = stack
|
||||
}
|
||||
|
||||
fun dismiss() = model.bottomSheetNavigationV2.replaceAll(OnrampAddTokenRoute.Empty)
|
||||
fun onBack() = if (childStack.backStack.isNotEmpty()) model.bottomSheetNavigationV2.pop() else dismiss()
|
||||
|
||||
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
|
||||
scrollableContent = false,
|
||||
onBack = ::onBack,
|
||||
config = TangemBottomSheetConfig(
|
||||
isShown = isNotEmpty,
|
||||
onDismissRequest = ::dismiss,
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
),
|
||||
containerColor = TangemTheme.colors.background.tertiary,
|
||||
title = { state ->
|
||||
AnimatedContent(targetState = contentStack.value) { stack ->
|
||||
BottomSheetTitle(
|
||||
stack = stack,
|
||||
onBackClick = ::onBack,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
},
|
||||
content = { state ->
|
||||
AnimatedContent(targetState = contentStack.value) { stack ->
|
||||
val paddingModifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
)
|
||||
val scrollableContent = when (stack.active.configuration) {
|
||||
OnrampAddTokenRoute.PortfolioSelector -> false
|
||||
OnrampAddTokenRoute.AddToken,
|
||||
OnrampAddTokenRoute.Empty,
|
||||
-> true
|
||||
}
|
||||
if (scrollableContent) {
|
||||
Column(
|
||||
modifier = paddingModifier.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
stack.active.instance.Content(modifier = Modifier)
|
||||
}
|
||||
} else {
|
||||
stack.active.instance.Content(modifier = paddingModifier)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BottomSheetTitle(
|
||||
stack: ChildStack<OnrampAddTokenRoute, ComposableContentComponent>,
|
||||
onBackClick: (() -> Unit),
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val title: TextReference = when (stack.active.configuration) {
|
||||
OnrampAddTokenRoute.AddToken -> resourceReference(R.string.common_add_token)
|
||||
OnrampAddTokenRoute.Empty -> TextReference.EMPTY
|
||||
OnrampAddTokenRoute.PortfolioSelector -> (stack.active.instance as PortfolioSelectorComponent)
|
||||
.title.collectAsStateWithLifecycle().value
|
||||
}
|
||||
val startIconRes: Int?
|
||||
val endIconRes: Int?
|
||||
if (stack.backStack.isNotEmpty()) {
|
||||
startIconRes = R.drawable.ic_back_24
|
||||
endIconRes = null
|
||||
} else {
|
||||
startIconRes = null
|
||||
endIconRes = R.drawable.ic_close_24
|
||||
}
|
||||
TangemModalBottomSheetTitle(
|
||||
modifier = modifier,
|
||||
title = title,
|
||||
startIconRes = startIconRes,
|
||||
endIconRes = endIconRes,
|
||||
onStartClick = onBackClick,
|
||||
onEndClick = onBackClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun bottomSheetChild(
|
||||
|
|
@ -65,6 +220,12 @@ internal class DefaultHotCryptoComponent @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun contentChild(config: OnrampAddTokenRoute): ComposableContentComponent = when (config) {
|
||||
OnrampAddTokenRoute.AddToken -> addTokenComponent!!
|
||||
OnrampAddTokenRoute.PortfolioSelector -> portfolioSelectorComponent!!
|
||||
OnrampAddTokenRoute.Empty -> ComposableContentComponent.EMPTY
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : HotCryptoComponent.Factory {
|
||||
override fun create(context: AppComponentContext, params: HotCryptoComponent.Params): DefaultHotCryptoComponent
|
||||
|
|
|
|||
|
|
@ -4,26 +4,43 @@ import arrow.core.getOrElse
|
|||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
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.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.onramp.GetHotCryptoUseCase
|
||||
import com.tangem.domain.onramp.model.HotCryptoCurrency
|
||||
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
|
||||
import com.tangem.features.account.PortfolioFetcher
|
||||
import com.tangem.features.account.PortfolioSelectorController
|
||||
import com.tangem.features.onramp.hottokens.HotCryptoComponent
|
||||
import com.tangem.features.onramp.hottokens.converter.HotTokenItemStateConverter
|
||||
import com.tangem.features.onramp.hottokens.entity.HotCryptoUM
|
||||
import com.tangem.features.onramp.hottokens.portfolio.OnrampAddTokenComponent
|
||||
import com.tangem.features.onramp.hottokens.portfolio.OnrampAddTokenComponent.AddHotCryptoData
|
||||
import com.tangem.features.onramp.hottokens.portfolio.entity.OnrampAddToPortfolioBSConfig
|
||||
import com.tangem.features.onramp.hottokens.portfolio.entity.OnrampAddTokenRoute
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
|
@ -40,23 +57,71 @@ import javax.inject.Inject
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
@ModelScoped
|
||||
internal class HotCryptoModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
getHotCryptoUseCase: GetHotCryptoUseCase,
|
||||
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getHotCryptoUseCase: GetHotCryptoUseCase,
|
||||
private val callbackDelegate: HotCryptoModelCallbackDelegate,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
) : Model() {
|
||||
private val hotCryptoPortfolioDataLoader: HotCryptoPortfolioDataLoader,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||
val portfolioSelectorController: PortfolioSelectorController,
|
||||
private val portfolioFetcherFactory: PortfolioFetcher.Factory,
|
||||
) : Model(), OnrampAddTokenComponent.Callbacks by callbackDelegate {
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<OnrampAddToPortfolioBSConfig> = SlotNavigation()
|
||||
|
||||
val portfolioFetcher: PortfolioFetcher?
|
||||
val bottomSheetNavigationV2 = StackNavigation<OnrampAddTokenRoute>()
|
||||
private val addHotCryptoJob = JobHolder()
|
||||
val hotCryptoToAddDataFlow: MutableSharedFlow<AddHotCryptoData> = MutableSharedFlow(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
val state: StateFlow<HotCryptoUM> get() = _state
|
||||
private val _state = MutableStateFlow(value = HotCryptoUM(items = persistentListOf()))
|
||||
|
||||
private val params: HotCryptoComponent.Params = paramsContainer.require()
|
||||
|
||||
init {
|
||||
if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
portfolioFetcher = portfolioFetcherFactory.create(
|
||||
mode = PortfolioFetcher.Mode.Wallet(params.userWalletId),
|
||||
scope = modelScope,
|
||||
)
|
||||
combineData()
|
||||
} else {
|
||||
portfolioFetcher = null
|
||||
combineDataOld()
|
||||
}
|
||||
}
|
||||
|
||||
private fun combineData() {
|
||||
combine(
|
||||
flow = hotCryptoPortfolioDataLoader.loadPortfolioData(params.userWalletId),
|
||||
flow2 = isAccountsModeEnabledUseCase.invoke(),
|
||||
transform = { data, isAccountMode ->
|
||||
HotTokenItemStateConverter(
|
||||
appCurrency = data.appCurrency,
|
||||
onItemClick = { tokenItemState, hotCryptoCurrency ->
|
||||
startAddTokenFlow(currency = hotCryptoCurrency, hotCryptoPortfolioData = data)
|
||||
},
|
||||
)
|
||||
.convertList(data.allHotCrypto)
|
||||
.map(TokensListItemUM::Token)
|
||||
},
|
||||
)
|
||||
.onEach { items -> _state.update { HotCryptoUM(items = it.buildItems(items)) } }
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun combineDataOld() {
|
||||
combine(
|
||||
flow = getSelectedAppCurrencyUseCase().map { it.getOrElse { AppCurrency.Default } },
|
||||
flow2 = getHotCryptoUseCase(params.userWalletId),
|
||||
|
|
@ -66,22 +131,19 @@ internal class HotCryptoModel @Inject constructor(
|
|||
.map(TokensListItemUM::Token)
|
||||
}
|
||||
.onEach { items ->
|
||||
_state.update {
|
||||
HotCryptoUM(
|
||||
items = buildList {
|
||||
if (items.isNotEmpty()) {
|
||||
add(it.getOrCreateGroupTitle())
|
||||
}
|
||||
|
||||
addAll(items)
|
||||
}
|
||||
.toImmutableList(),
|
||||
)
|
||||
}
|
||||
_state.update { HotCryptoUM(items = it.buildItems(items)) }
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun HotCryptoUM.buildItems(items: List<TokensListItemUM.Token>): ImmutableList<TokensListItemUM> =
|
||||
buildList {
|
||||
if (items.isNotEmpty()) {
|
||||
add(getOrCreateGroupTitle())
|
||||
}
|
||||
addAll(items)
|
||||
}.toImmutableList()
|
||||
|
||||
private fun HotCryptoUM.getOrCreateGroupTitle(): TokensListItemUM {
|
||||
return items.firstOrNull()
|
||||
.takeIf { it is TokensListItemUM.GroupTitle }
|
||||
|
|
@ -101,6 +163,69 @@ internal class HotCryptoModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun startAddTokenFlow(currency: HotCryptoCurrency, hotCryptoPortfolioData: HotCryptoPortfolioData) {
|
||||
hotCryptoToAddDataFlow.resetReplayCache()
|
||||
fun closeNavigationFlow() = bottomSheetNavigationV2.replaceAll(OnrampAddTokenRoute.Empty)
|
||||
channelFlow<Unit> {
|
||||
val userWallet = hotCryptoPortfolioData.wallet.userWallet
|
||||
val isSingleAccount = hotCryptoPortfolioData.wallet.accounts.size == 1
|
||||
|
||||
if (isSingleAccount) {
|
||||
val account = hotCryptoPortfolioData.wallet.accounts.first().account
|
||||
val tokenToAdd = AddHotCryptoData(
|
||||
cryptoCurrency = currency.cryptoCurrency,
|
||||
userWallet = userWallet,
|
||||
account = account,
|
||||
availableMorePortfolio = false,
|
||||
)
|
||||
hotCryptoToAddDataFlow.emit(tokenToAdd)
|
||||
bottomSheetNavigationV2.replaceAll(OnrampAddTokenRoute.AddToken)
|
||||
} else {
|
||||
setupPortfolioSelector(currency, hotCryptoPortfolioData)
|
||||
bottomSheetNavigationV2.replaceAll(OnrampAddTokenRoute.PortfolioSelector)
|
||||
|
||||
val tokenToAddStateFlow = portfolioSelectorController
|
||||
.selectedAccountWithData(requireNotNull(portfolioFetcher))
|
||||
.filterNotNull()
|
||||
.map { (_, selectedAccount) ->
|
||||
AddHotCryptoData(
|
||||
cryptoCurrency = currency.cryptoCurrency,
|
||||
userWallet = userWallet,
|
||||
account = selectedAccount,
|
||||
availableMorePortfolio = true,
|
||||
)
|
||||
}
|
||||
.shareIn(this, started = SharingStarted.Eagerly)
|
||||
|
||||
val firstTokenToAdd = tokenToAddStateFlow.first()
|
||||
hotCryptoToAddDataFlow.emit(firstTokenToAdd)
|
||||
bottomSheetNavigationV2.replaceAll(OnrampAddTokenRoute.AddToken)
|
||||
|
||||
callbackDelegate.onChangePortfolioClick.receiveAsFlow()
|
||||
.onEach { bottomSheetNavigationV2.pushNew(OnrampAddTokenRoute.PortfolioSelector) }
|
||||
.launchIn(this)
|
||||
|
||||
tokenToAddStateFlow
|
||||
.onEach { newTokenToAdd -> hotCryptoToAddDataFlow.emit(newTokenToAdd) }
|
||||
.onEach { bottomSheetNavigationV2.popToFirst() }
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
val addedToken = callbackDelegate.onTokenAdded
|
||||
.receiveAsFlow()
|
||||
.first()
|
||||
params.onTokenClick(addedToken)
|
||||
closeNavigationFlow()
|
||||
channel.close()
|
||||
}
|
||||
.catch {
|
||||
Timber.e(it)
|
||||
closeNavigationFlow()
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
.saveIn(addHotCryptoJob)
|
||||
}
|
||||
|
||||
private fun onSuccessAdding(id: CryptoCurrency.ID) {
|
||||
modelScope.launch {
|
||||
getSingleCryptoCurrencyStatusUseCase.invokeMultiWalletSync(
|
||||
|
|
@ -114,4 +239,31 @@ internal class HotCryptoModel @Inject constructor(
|
|||
.onLeft { Timber.d("Unable to get CryptoCurrencyStatus[$id]: $it") }
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupPortfolioSelector(hotCrypto: HotCryptoCurrency, hotCryptoPortfolioData: HotCryptoPortfolioData) {
|
||||
portfolioSelectorController.selectAccount(null)
|
||||
portfolioSelectorController.isEnabled.value = isEnabled@{ _, accountStatus ->
|
||||
val isNotAddedHotCrypto = hotCryptoPortfolioData.wallet.accounts
|
||||
.find { it.account.accountId == accountStatus.accountId }
|
||||
?.addedHotCrypto
|
||||
?.none { it.currency.id.rawCurrencyId == hotCrypto.cryptoCurrency.id.rawCurrencyId }
|
||||
?: false
|
||||
return@isEnabled isNotAddedHotCrypto
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ModelScoped
|
||||
internal class HotCryptoModelCallbackDelegate @Inject constructor() : OnrampAddTokenComponent.Callbacks {
|
||||
|
||||
val onChangePortfolioClick = Channel<Unit>()
|
||||
val onTokenAdded = Channel<CryptoCurrencyStatus>()
|
||||
|
||||
override fun onChangePortfolioClick() {
|
||||
onChangePortfolioClick.trySend(Unit)
|
||||
}
|
||||
|
||||
override fun onTokenAdded(status: CryptoCurrencyStatus) {
|
||||
onTokenAdded.trySend(status)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package com.tangem.features.onramp.hottokens.model
|
||||
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.domain.account.models.AccountStatusList
|
||||
import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatuses
|
||||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.onramp.GetHotCryptoUseCase
|
||||
import com.tangem.domain.onramp.model.HotCryptoCurrency
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class HotCryptoPortfolioDataLoader @Inject constructor(
|
||||
private val appCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getHotCryptoUseCase: GetHotCryptoUseCase,
|
||||
private val accountListSupplier: SingleAccountStatusListSupplier,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
|
||||
|
||||
) {
|
||||
|
||||
fun loadPortfolioData(userWalletId: UserWalletId): Flow<HotCryptoPortfolioData> = combine(
|
||||
flow = appCurrencyUseCase.invokeOrDefault().distinctUntilChanged(),
|
||||
flow2 = getHotCryptoUseCase(userWalletId).distinctUntilChanged(),
|
||||
flow3 = accountListSupplier.invoke(userWalletId).distinctUntilChanged(),
|
||||
flow4 = getUserWalletUseCase.invokeFlow(userWalletId).mapNotNull { it.getOrNull() },
|
||||
) { appCurrency, allHotCrypto, walletAccounts, userWallet ->
|
||||
|
||||
val hotCryptoCurrencies = allHotCrypto.map { hotCrypto -> hotCrypto.cryptoCurrency }
|
||||
val mapOfAddedCurrencies: AccountCryptoCurrencyStatuses = getAccountCurrencyStatusUseCase
|
||||
.invokeSync(userWalletId, hotCryptoCurrencies)
|
||||
.getOrNull()
|
||||
?: emptyMap()
|
||||
val accountsWithHotCrypto = walletAccounts.accountStatuses.map {
|
||||
val account: AccountStatus.CryptoPortfolio = when (it) {
|
||||
is AccountStatus.CryptoPortfolio -> it
|
||||
}
|
||||
val addedHotCrypto = mapOfAddedCurrencies[account.account] ?: listOf()
|
||||
HotCryptoPortfolioData.Account(
|
||||
account = account,
|
||||
addedHotCrypto = addedHotCrypto,
|
||||
)
|
||||
}
|
||||
|
||||
HotCryptoPortfolioData(
|
||||
allHotCrypto = allHotCrypto,
|
||||
appCurrency = appCurrency,
|
||||
wallet = HotCryptoPortfolioData.Wallet(
|
||||
userWallet = userWallet,
|
||||
statusList = walletAccounts,
|
||||
accounts = accountsWithHotCrypto,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal data class HotCryptoPortfolioData(
|
||||
val allHotCrypto: List<HotCryptoCurrency>,
|
||||
val wallet: Wallet,
|
||||
val appCurrency: AppCurrency,
|
||||
) {
|
||||
|
||||
data class Wallet(
|
||||
val userWallet: UserWallet,
|
||||
val statusList: AccountStatusList,
|
||||
val accounts: List<Account>,
|
||||
)
|
||||
|
||||
data class Account(
|
||||
val account: AccountStatus.CryptoPortfolio,
|
||||
val addedHotCrypto: List<CryptoCurrencyStatus>,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.features.onramp.hottokens.portfolio
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.common.ui.addtoken.AddTokenContent
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
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.onramp.hottokens.portfolio.model.OnrampAddTokenModel
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
internal class OnrampAddTokenComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted private val params: Params,
|
||||
) : AppComponentContext by context, ComposableContentComponent {
|
||||
|
||||
private val model: OnrampAddTokenModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state = model.uiState.collectAsStateWithLifecycle()
|
||||
val um = state.value ?: return
|
||||
AddTokenContent(
|
||||
modifier = modifier,
|
||||
state = um,
|
||||
)
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val tokenToAdd: Flow<AddHotCryptoData>,
|
||||
val callbacks: Callbacks,
|
||||
)
|
||||
|
||||
data class AddHotCryptoData(
|
||||
val cryptoCurrency: CryptoCurrency,
|
||||
val userWallet: UserWallet,
|
||||
val account: AccountStatus,
|
||||
val availableMorePortfolio: Boolean,
|
||||
)
|
||||
|
||||
interface Callbacks {
|
||||
fun onChangePortfolioClick()
|
||||
fun onTokenAdded(status: CryptoCurrencyStatus)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : ComponentFactory<Params, OnrampAddTokenComponent> {
|
||||
override fun create(context: AppComponentContext, params: Params): OnrampAddTokenComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.features.onramp.hottokens.portfolio.di
|
|||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.onramp.hottokens.portfolio.model.OnrampAddToPortfolioModel
|
||||
import com.tangem.features.onramp.hottokens.portfolio.model.OnrampAddTokenModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -17,4 +18,9 @@ internal interface OnrampAddToPortfolioModelModule {
|
|||
@IntoMap
|
||||
@ClassKey(OnrampAddToPortfolioModel::class)
|
||||
fun bindOnrampSelectCountryModel(model: OnrampAddToPortfolioModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(OnrampAddTokenModel::class)
|
||||
fun bindOnrampAddTokenModel(model: OnrampAddTokenModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.features.onramp.hottokens.portfolio.entity
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal sealed interface OnrampAddTokenRoute {
|
||||
|
||||
@Serializable
|
||||
data object Empty : OnrampAddTokenRoute
|
||||
|
||||
@Serializable
|
||||
data object PortfolioSelector : OnrampAddTokenRoute
|
||||
|
||||
@Serializable
|
||||
data object AddToken : OnrampAddTokenRoute
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
package com.tangem.features.onramp.hottokens.portfolio.entity
|
||||
|
||||
import com.tangem.common.ui.account.CryptoPortfolioIconUM
|
||||
import com.tangem.common.ui.account.PortfolioSelectUM
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.common.ui.addtoken.AddTokenUM
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.iconResId
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.features.onramp.hottokens.portfolio.OnrampAddTokenComponent
|
||||
import com.tangem.features.onramp.hottokens.portfolio.OnrampAddTokenComponent.AddHotCryptoData
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class OnrampAddTokenUiBuilder @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||
) {
|
||||
private val params = paramsContainer.require<OnrampAddTokenComponent.Params>()
|
||||
|
||||
private fun createNetwork(tokenToAdd: AddHotCryptoData): AddTokenUM.Network {
|
||||
return AddTokenUM.Network(
|
||||
icon = tokenToAdd.cryptoCurrency.network.iconResId,
|
||||
name = stringReference(tokenToAdd.cryptoCurrency.network.name),
|
||||
editable = false,
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun createPortfolio(tokenToAdd: AddHotCryptoData): PortfolioSelectUM {
|
||||
val accountIcon: CryptoPortfolioIconUM?
|
||||
val portfolioName: TextReference
|
||||
val isAccountMode = isAccountsModeEnabledUseCase.invokeSync()
|
||||
when (isAccountMode) {
|
||||
false -> {
|
||||
accountIcon = null
|
||||
portfolioName = stringReference(tokenToAdd.userWallet.name)
|
||||
}
|
||||
true -> {
|
||||
val accountStatus = tokenToAdd.account
|
||||
portfolioName = accountStatus.account.accountName.toUM().value
|
||||
accountIcon = when (accountStatus) {
|
||||
is AccountStatus.CryptoPortfolio -> accountStatus.account.icon.toUM()
|
||||
}
|
||||
}
|
||||
}
|
||||
return PortfolioSelectUM(
|
||||
icon = accountIcon,
|
||||
name = portfolioName,
|
||||
isAccountMode = isAccountMode,
|
||||
isMultiChoice = tokenToAdd.availableMorePortfolio,
|
||||
onClick = { params.callbacks.onChangePortfolioClick() },
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun updateContent(
|
||||
isTangemIconVisible: Boolean,
|
||||
onConfirmClick: () -> Unit,
|
||||
tokenToAdd: AddHotCryptoData,
|
||||
): AddTokenUM {
|
||||
val button = AddTokenUM.Button(
|
||||
showProgress = false,
|
||||
isTangemIconVisible = isTangemIconVisible,
|
||||
text = resourceReference(R.string.common_add),
|
||||
onConfirmClick = onConfirmClick,
|
||||
isEnabled = true,
|
||||
)
|
||||
val networkUM = createNetwork(tokenToAdd)
|
||||
val portfolioUM = createPortfolio(tokenToAdd)
|
||||
val currency = tokenToAdd.cryptoCurrency
|
||||
val tokenToAdd = TokenItemState.Content(
|
||||
id = currency.id.value,
|
||||
iconState = CryptoCurrencyToIconStateConverter().convert(currency),
|
||||
titleState = TokenItemState.TitleState.Content(stringReference(currency.name)),
|
||||
fiatAmountState = TokenItemState.FiatAmountState.Content(text = ""),
|
||||
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = ""),
|
||||
subtitleState = TokenItemState.SubtitleState.TextContent(stringReference(currency.symbol)),
|
||||
onItemClick = null,
|
||||
onItemLongClick = null,
|
||||
)
|
||||
return AddTokenUM(
|
||||
tokenToAdd = tokenToAdd,
|
||||
network = networkUM,
|
||||
portfolio = portfolioUM,
|
||||
button = button,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun AddTokenUM.toggleProgress(showProgress: Boolean) = this.copy(
|
||||
button = this.button.copy(showProgress = showProgress),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,9 @@
|
|||
package com.tangem.features.onramp.hottokens.portfolio.model
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.left
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase
|
||||
|
|
@ -37,7 +35,6 @@ internal class OnrampAddToPortfolioModel @Inject constructor(
|
|||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
|
||||
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -75,26 +72,16 @@ internal class OnrampAddToPortfolioModel @Inject constructor(
|
|||
private fun onAddClick() {
|
||||
modelScope.launch {
|
||||
changeAddButtonProgressStatus(isProgress = true)
|
||||
derivePublicKeysUseCase(params.userWalletId, listOfNotNull(params.cryptoCurrency)).getOrElse {
|
||||
Timber.e("Failed to derive public keys: $it")
|
||||
|
||||
if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
// saveCryptoCurrenciesUseCase(
|
||||
// accountId = params.accountId,
|
||||
// add = params.cryptoCurrency,
|
||||
// )
|
||||
// TODO account
|
||||
IllegalStateException("Not implemented yet").left()
|
||||
} else {
|
||||
derivePublicKeysUseCase(params.userWalletId, listOfNotNull(params.cryptoCurrency)).getOrElse {
|
||||
Timber.e("Failed to derive public keys: $it")
|
||||
|
||||
changeAddButtonProgressStatus(isProgress = false)
|
||||
}
|
||||
|
||||
addCryptoCurrenciesUseCase(
|
||||
userWalletId = params.userWalletId,
|
||||
currency = params.cryptoCurrency,
|
||||
)
|
||||
changeAddButtonProgressStatus(isProgress = false)
|
||||
}
|
||||
|
||||
addCryptoCurrenciesUseCase(
|
||||
userWalletId = params.userWalletId,
|
||||
currency = params.cryptoCurrency,
|
||||
)
|
||||
.onRight { params.onSuccessAdding(params.cryptoCurrency.id) }
|
||||
.onLeft { changeAddButtonProgressStatus(isProgress = false) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
package com.tangem.features.onramp.hottokens.portfolio.model
|
||||
|
||||
import com.tangem.common.ui.addtoken.AddTokenUM
|
||||
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.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.message.ToastMessage
|
||||
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
||||
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.BackendId
|
||||
import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase
|
||||
import com.tangem.features.onramp.hottokens.portfolio.OnrampAddTokenComponent
|
||||
import com.tangem.features.onramp.hottokens.portfolio.OnrampAddTokenComponent.AddHotCryptoData
|
||||
import com.tangem.features.onramp.hottokens.portfolio.entity.OnrampAddTokenUiBuilder
|
||||
import com.tangem.features.onramp.hottokens.portfolio.entity.OnrampAddTokenUiBuilder.Companion.toggleProgress
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
internal class OnrampAddTokenModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
private val uiBuilder: OnrampAddTokenUiBuilder,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
|
||||
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<OnrampAddTokenComponent.Params>()
|
||||
private val addTokenJob = JobHolder()
|
||||
|
||||
val uiState: StateFlow<AddTokenUM?>
|
||||
field = MutableStateFlow(value = null)
|
||||
|
||||
init {
|
||||
params.tokenToAdd
|
||||
.distinctUntilChanged()
|
||||
.mapLatest { tokenToAdd: AddHotCryptoData ->
|
||||
addTokenJob.cancel()
|
||||
val backendId = tokenToAdd.cryptoCurrency.network.backendId
|
||||
val userWalletId = tokenToAdd.account.accountId.userWalletId
|
||||
val isTangemIconVisible = needColdWalletInteraction(userWalletId, backendId)
|
||||
uiBuilder.updateContent(
|
||||
tokenToAdd = tokenToAdd,
|
||||
isTangemIconVisible = isTangemIconVisible,
|
||||
onConfirmClick = { onAddClick(tokenToAdd).saveIn(addTokenJob) },
|
||||
)
|
||||
}
|
||||
.onEach { newUI -> uiState.value = newUI }
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun onAddClick(tokenToAdd: AddHotCryptoData) = modelScope.launch(dispatchers.default) {
|
||||
val um = uiState.value ?: return@launch
|
||||
uiState.value = um.toggleProgress(true)
|
||||
|
||||
val cryptoCurrency = tokenToAdd.cryptoCurrency
|
||||
val accountId = tokenToAdd.account.accountId
|
||||
manageCryptoCurrenciesUseCase(accountId = accountId, add = cryptoCurrency)
|
||||
.onLeft {
|
||||
processError(error = it)
|
||||
uiState.value = um.toggleProgress(false)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val status = getAccountCurrencyStatusUseCase.invokeSync(
|
||||
userWalletId = accountId.userWalletId,
|
||||
currencyId = cryptoCurrency.id,
|
||||
network = cryptoCurrency.network,
|
||||
).getOrNull()
|
||||
if (status == null) {
|
||||
processError(error = null)
|
||||
} else {
|
||||
params.callbacks.onTokenAdded(status.status)
|
||||
}
|
||||
uiState.value = um.toggleProgress(false)
|
||||
}
|
||||
|
||||
private suspend fun needColdWalletInteraction(walletId: UserWalletId, backendId: BackendId): Boolean =
|
||||
coldWalletAndHasMissedDerivationsUseCase.invoke(
|
||||
userWalletId = walletId,
|
||||
networksWithDerivationPath = mapOf(backendId to null),
|
||||
)
|
||||
|
||||
private fun processError(error: Throwable?) {
|
||||
val message = error?.message?.let { stringReference(it) }
|
||||
?: resourceReference(R.string.common_something_went_wrong)
|
||||
messageSender.send(ToastMessage(message = message))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue