Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-05 14:46:29 +04:00
parent b3b7a6feac
commit 509d1ffd3c
15 changed files with 136 additions and 16 deletions

View file

@ -9,6 +9,7 @@ import com.tangem.features.onramp.impl.R
/**
* Onramp add to portfolio UM
*
* @property walletName wallet name
* @property currencyName currency name
* @property networkName network name
* @property currencyIconState currency icon state
@ -17,6 +18,7 @@ import com.tangem.features.onramp.impl.R
[REDACTED_AUTHOR]
*/
data class OnrampAddToPortfolioUM(
val walletName: String,
val currencyName: String,
val networkName: String,
val currencyIconState: CurrencyIconState,
@ -25,6 +27,11 @@ data class OnrampAddToPortfolioUM(
val bsTitle: TextReference = resourceReference(id = R.string.common_add_token)
val bsSubtitle: TextReference = resourceReference(
id = R.string.hot_crypto_add_token_subtitle,
formatArgs = wrappedList(walletName),
)
val subtitle: TextReference = resourceReference(
id = R.string.hot_crypto_token_network,
formatArgs = wrappedList(networkName),

View file

@ -6,6 +6,8 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.onramp.hottokens.portfolio.OnrampAddToPortfolioComponent
import com.tangem.features.onramp.hottokens.portfolio.entity.OnrampAddToPortfolioUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -22,6 +24,7 @@ import javax.inject.Inject
* @property dispatchers dispatchers
* @property derivePublicKeysUseCase use case for deriving public key
* @property addCryptoCurrenciesUseCase use case for adding crypto currency
* @property getUserWalletUseCase use case for getting user wallet by id
*
[REDACTED_AUTHOR]
*/
@ -31,6 +34,7 @@ internal class OnrampAddToPortfolioModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
) : Model() {
private val params: OnrampAddToPortfolioComponent.Params = paramsContainer.require()
@ -40,6 +44,7 @@ internal class OnrampAddToPortfolioModel @Inject constructor(
private fun getInitialState(): OnrampAddToPortfolioUM {
return OnrampAddToPortfolioUM(
walletName = getUserWalletName(),
currencyName = params.cryptoCurrency.name,
networkName = params.cryptoCurrency.network.name,
currencyIconState = params.currencyIconState,
@ -47,6 +52,12 @@ internal class OnrampAddToPortfolioModel @Inject constructor(
)
}
private fun getUserWalletName(): String {
return getUserWalletUseCase(params.userWalletId)
.onLeft { Timber.e("Unable to get wallet name by id [${params.userWalletId}]: $it") }
.fold(ifLeft = { "" }, ifRight = UserWallet::name)
}
private fun onAddClick() {
modelScope.launch {
derivePublicKeysUseCase(params.userWalletId, listOfNotNull(params.cryptoCurrency)).getOrElse {

View file

@ -16,7 +16,6 @@ import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.TangemTopAppBarHeight
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.buttons.common.TangemButton
@ -44,7 +43,7 @@ fun OnrampAddToPortfolioContent(state: OnrampAddToPortfolioUM) {
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
TopAppBar(text = state.bsTitle)
TopAppBar(text = state.bsTitle, subtitle = state.bsSubtitle)
SpacerH(height = 19.dp)
@ -77,13 +76,10 @@ fun OnrampAddToPortfolioContent(state: OnrampAddToPortfolioUM) {
}
@Composable
private fun TopAppBar(text: TextReference) {
private fun TopAppBar(text: TextReference, subtitle: TextReference) {
TangemTopAppBar(
title = text.resolveReference(),
endButton = TopAppBarButtonUM(
iconRes = R.drawable.ic_information_24,
onIconClicked = {},
),
subtitle = subtitle.resolveReference(),
iconTint = TangemTheme.colors.icon.informative,
titleAlignment = Alignment.CenterHorizontally,
height = TangemTopAppBarHeight.BOTTOM_SHEET,
@ -143,6 +139,7 @@ private fun PreviewOnrampAddToPortfolioContent() {
content = {
OnrampAddToPortfolioContent(
state = OnrampAddToPortfolioUM(
walletName = "Wallet 1",
currencyName = "Tether",
networkName = "Ethereum",
currencyIconState = CurrencyIconState.TokenIcon(

View file

@ -6,6 +6,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.onramp.OnrampFeatureToggles
import com.tangem.features.onramp.hottokens.HotCryptoComponent
import com.tangem.features.onramp.selecttoken.model.OnrampOperationModel
import com.tangem.features.onramp.selecttoken.ui.OnrampSelectToken
import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
@ -18,6 +20,8 @@ internal class DefaultOnrampOperationComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
onrampTokenListComponentFactory: OnrampTokenListComponent.Factory,
@Assisted private val params: OnrampOperationComponent.Params,
private val onrampFeatureToggles: OnrampFeatureToggles,
private val hotCryptoComponentFactory: HotCryptoComponent.Factory,
) : AppComponentContext by appComponentContext, OnrampOperationComponent {
private val model: OnrampOperationModel = getOrCreateModel(params)
@ -31,9 +35,12 @@ internal class DefaultOnrampOperationComponent @AssistedInject constructor(
},
userWalletId = params.userWalletId,
onTokenClick = { _, status -> model.onTokenClick(status) },
onTokenListInitialized = model::onTokenListInitialized,
),
)
private val hotCryptoComponent: HotCryptoComponent? = createHotCryptoComponent()
@Composable
override fun Content(modifier: Modifier) {
val state = model.state.collectAsStateWithLifecycle()
@ -41,10 +48,25 @@ internal class DefaultOnrampOperationComponent @AssistedInject constructor(
OnrampSelectToken(
state = state.value,
onrampTokenListComponent = onrampTokenListComponent,
hotCryptoComponent = hotCryptoComponent,
modifier = modifier,
)
}
private fun createHotCryptoComponent(): HotCryptoComponent? {
return if (onrampFeatureToggles.isHotTokensEnabled && params is OnrampOperationComponent.Params.Buy) {
hotCryptoComponentFactory.create(
context = child(key = "hot_crypto"),
params = HotCryptoComponent.Params(
userWalletId = params.userWalletId,
onTokenClick = model::onTokenClick,
),
)
} else {
null
}
}
@AssistedFactory
interface Factory : OnrampOperationComponent.Factory {

View file

@ -5,4 +5,5 @@ import androidx.annotation.StringRes
internal data class OnrampOperationUM(
@StringRes val titleResId: Int,
val onBackClick: () -> Unit,
val isHotCryptoVisible: Boolean,
)

View file

@ -27,6 +27,7 @@ import com.tangem.features.onramp.selecttoken.entity.OnrampOperationUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -98,6 +99,7 @@ internal class OnrampOperationModel @Inject constructor(
is Params.Sell -> R.string.common_sell
},
onBackClick = ::onBackClick,
isHotCryptoVisible = false,
)
}
@ -144,4 +146,11 @@ internal class OnrampOperationModel @Inject constructor(
action()
}
}
fun onTokenListInitialized() {
// Makes HotCrypto tokens visible when token list is initialized to synchronize UI
_state.update {
it.copy(isHotCryptoVisible = true)
}
}
}

View file

@ -15,6 +15,7 @@ import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.rememberHideKeyboardNestedScrollConnection
import com.tangem.features.onramp.hottokens.HotCryptoComponent
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.selecttoken.entity.OnrampOperationUM
import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
@ -24,6 +25,7 @@ import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
internal fun OnrampSelectToken(
state: OnrampOperationUM,
onrampTokenListComponent: OnrampTokenListComponent,
hotCryptoComponent: HotCryptoComponent?,
modifier: Modifier = Modifier,
) {
BackHandler(onBack = state.onBackClick)
@ -47,8 +49,27 @@ internal fun OnrampSelectToken(
item(key = "token_list", contentType = "token_list") {
onrampTokenListComponent.Content(
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
modifier = Modifier
.padding(top = 8.dp)
.padding(horizontal = 16.dp)
.then(
if (hotCryptoComponent != null) {
Modifier
} else {
Modifier.padding(bottom = 8.dp)
},
),
)
}
if (hotCryptoComponent != null && state.isHotCryptoVisible) {
item(key = "hot_crypto", contentType = "hot_crypto") {
hotCryptoComponent.Content(
modifier = Modifier
.padding(top = 12.dp, bottom = 8.dp)
.padding(horizontal = 16.dp),
)
}
}
}
}

View file

@ -18,13 +18,15 @@ internal interface OnrampTokenListComponent : ComposableContentComponent {
/**
* Params
*
* @property filterOperation operation that is used to filter tokens by availability
* @property userWalletId id of multi-currency wallet
* @property onTokenClick callback for token click
* @property filterOperation operation that is used to filter tokens by availability
* @property userWalletId user wallet id of multi-currency wallet
* @property onTokenClick callback for token click
* @property onTokenListInitialized callback for token list initialization
*/
data class Params(
val filterOperation: OnrampOperation,
val userWalletId: UserWalletId,
val onTokenClick: (TokenItemState, CryptoCurrencyStatus) -> Unit,
val onTokenListInitialized: () -> Unit = {},
)
}

View file

@ -24,6 +24,7 @@ import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
import com.tangem.features.onramp.tokenlist.entity.OnrampOperation
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMController
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
import com.tangem.features.onramp.tokenlist.entity.transformer.SetNothingToFoundStateTransformer
import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateTokenItemsTransformer
import com.tangem.features.onramp.utils.InputManager
@ -125,7 +126,7 @@ internal class OnrampTokenListModel @Inject constructor(
)
}
}
.onEach(tokenListUMController::update)
.onEach(::updateTokenListUM)
.flowOn(dispatchers.main)
.launchIn(modelScope)
}
@ -156,6 +157,21 @@ internal class OnrampTokenListModel @Inject constructor(
OnrampOperation.SWAP -> R.string.tokens_list_unavailable_to_swap_source_header
}.let(::resourceReference)
private fun updateTokenListUM(transformer: TokenListUMTransformer) {
tokenListUMController.update { prevState ->
transformer.transform(prevState).apply {
if (isFirstInitialization(prevState = prevState, newState = this)) {
params.onTokenListInitialized()
}
}
}
}
private fun isFirstInitialization(prevState: TokenListUM, newState: TokenListUM): Boolean {
return prevState.availableItems.isEmpty() && prevState.unavailableItems.isEmpty() &&
(newState.availableItems.isNotEmpty() || newState.unavailableItems.isNotEmpty())
}
private fun onSearchQueryChange(newQuery: String) {
val searchBar = state.value.searchBarUM
if (searchBar.query == newQuery) return