Updated on 2026-08-14

This commit is contained in:
Tangem 2025-06-05 16:18:11 +03:00
commit c8c64e882b
1244 changed files with 26701 additions and 16971 deletions

View file

@ -109,9 +109,11 @@ internal class AskBiometryModel @Inject constructor(
walletsRepository.saveShouldSaveUserWallets(item = true)
settingsRepository.setShouldSaveAccessCodes(value = true)
cardSdkConfigRepository.setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = userWallet.hasAccessCode,
)
if (userWallet is UserWallet.Cold) {
cardSdkConfigRepository.setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = userWallet.hasAccessCode,
)
}
if (_uiState.value.bottomSheetVariant) {
dismissBSFlow.emit(Unit)

View file

@ -15,6 +15,7 @@ dependencies {
/* Project - API */
implementation(projects.features.details.api)
implementation(projects.features.wallet.api)
implementation(projects.features.disclaimer.api)
implementation(projects.features.tester.api)

View file

@ -5,6 +5,7 @@ import androidx.compose.ui.Modifier
import com.tangem.core.decompose.navigation.DummyRouter
import com.tangem.core.navigation.url.DummyUrlOpener
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.details.entity.DetailsFooterUM
import com.tangem.features.details.entity.DetailsUM
@ -18,7 +19,12 @@ internal class PreviewDetailsComponent : DetailsComponent {
private val previewBlocks = runBlocking {
ItemsBuilder(
router = DummyRouter(),
).buildAll(isWalletConnectAvailable = true, onSupportClick = {}, onBuyClick = {})
).buildAll(
isWalletConnectAvailable = true,
userWalletId = UserWalletId(""),
onSupportClick = {},
onBuyClick = {},
)
}
private val previewFooter = DetailsFooterUM(

View file

@ -77,11 +77,12 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent {
}
}
private fun getInformation(cardCount: Int): TextReference {
return TextReference.PluralRes(
private fun getInformation(cardCount: Int): UserWalletItemUM.Information.Loaded {
val text = TextReference.PluralRes(
id = R.plurals.card_label_card_count,
count = cardCount,
formatArgs = wrappedList(cardCount),
)
return UserWalletItemUM.Information.Loaded(text)
}
}

View file

@ -17,6 +17,8 @@ import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.redux.LegacyAction
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.details.component.DetailsComponent
@ -80,6 +82,7 @@ internal class DetailsModel @Inject constructor(
items = MutableStateFlow(
itemsBuilder.buildAll(
isWalletConnectAvailable = isWalletConnectAvailable,
userWalletId = params.userWalletId,
onSupportClick = ::sendFeedback,
onBuyClick = ::onBuyClick,
),
@ -110,14 +113,17 @@ internal class DetailsModel @Inject constructor(
modelScope.launch {
val userWallets = getWalletsUseCase.invokeSync()
val scanResponse = getSelectedWalletSyncUseCase().getOrNull()?.scanResponse
?: error("Selected wallet is null")
val scanResponse =
getSelectedWalletSyncUseCase().getOrNull()?.requireColdWallet()?.scanResponse // TODO [REDACTED_TASK_KEY]
?: error("Selected wallet is null")
val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return@launch
val feedbackType = when {
userWallets.all { it.scanResponse.card.isVisa } -> FeedbackEmailType.Visa.DirectUserRequest(cardInfo)
userWallets.all { it.scanResponse.card.isVisa.not() } -> FeedbackEmailType.DirectUserRequest(cardInfo)
userWallets.all { it is UserWallet.Cold && it.scanResponse.card.isVisa } ->
FeedbackEmailType.Visa.DirectUserRequest(cardInfo)
userWallets.all { it !is UserWallet.Cold || it.scanResponse.card.isVisa.not() } ->
FeedbackEmailType.DirectUserRequest(cardInfo)
else -> {
showFeedbackEmailTypeOptionBS(cardInfo)
return@launch
@ -172,7 +178,9 @@ internal class DetailsModel @Inject constructor(
FeedbackEmailType.DirectUserRequest(selectedCardInfo)
} else {
val scanResponse = getWalletsUseCase.invokeSync()
.firstOrNull { it.scanResponse.card.isVisa.not() }?.scanResponse ?: return@launch
.firstOrNull { it is UserWallet.Cold && it.scanResponse.card.isVisa.not() }
?.requireColdWallet()?.scanResponse ?: return@launch
val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return@launch
FeedbackEmailType.DirectUserRequest(cardInfo)
}
@ -182,7 +190,8 @@ internal class DetailsModel @Inject constructor(
FeedbackEmailType.Visa.DirectUserRequest(selectedCardInfo)
} else {
val scanResponse = getWalletsUseCase.invokeSync()
.firstOrNull { it.scanResponse.card.isVisa }?.scanResponse ?: return@launch
.firstOrNull { it is UserWallet.Cold && it.scanResponse.card.isVisa }
?.requireColdWallet()?.scanResponse ?: return@launch
val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return@launch
FeedbackEmailType.Visa.DirectUserRequest(cardInfo)
}

View file

@ -1,15 +1,18 @@
package com.tangem.features.details.model
import com.tangem.common.routing.AppRoute
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase
import com.tangem.features.details.entity.UserWalletListUM
import com.tangem.features.details.impl.R
import com.tangem.features.details.utils.UserWalletSaver
import com.tangem.features.details.utils.UserWalletsFetcher
import com.tangem.features.wallet.utils.UserWalletsFetcher
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -21,13 +24,17 @@ import javax.inject.Inject
@ModelScoped
internal class UserWalletListModel @Inject constructor(
userWalletsFetcher: UserWalletsFetcher,
userWalletsFetcherFactory: UserWalletsFetcher.Factory,
shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase,
private val router: Router,
private val messageSender: UiMessageSender,
private val userWalletSaver: UserWalletSaver,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val isWalletSavingInProgress: MutableStateFlow<Boolean> = MutableStateFlow(value = false)
private val userWalletsFetcher = userWalletsFetcherFactory
.create(messageSender) { userWalletId -> router.push(AppRoute.WalletSettings(userWalletId)) }
val state: MutableStateFlow<UserWalletListUM> = MutableStateFlow(
value = UserWalletListUM(

View file

@ -6,6 +6,7 @@ import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.components.block.model.BlockUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.details.entity.DetailsItemUM
import com.tangem.features.details.impl.BuildConfig
import com.tangem.features.details.impl.R
@ -19,20 +20,21 @@ internal class ItemsBuilder @Inject constructor(private val router: Router) {
fun buildAll(
isWalletConnectAvailable: Boolean,
userWalletId: UserWalletId,
onSupportClick: () -> Unit,
onBuyClick: () -> Unit,
): ImmutableList<DetailsItemUM> = buildList {
buildWalletConnectBlock(isWalletConnectAvailable)?.let(::add)
buildWalletConnectBlock(isWalletConnectAvailable, userWalletId)?.let(::add)
buildUserWalletListBlock().let(::add)
buildShopBlock(onBuyClick).let(::add)
buildSettingsBlock().let(::add)
buildSupportBlock(onSupportClick).let(::add)
}.toImmutableList()
private fun buildWalletConnectBlock(isWalletConnectAvailable: Boolean): DetailsItemUM? {
private fun buildWalletConnectBlock(isWalletConnectAvailable: Boolean, userWalletId: UserWalletId): DetailsItemUM? {
return if (isWalletConnectAvailable) {
DetailsItemUM.WalletConnect(
onClick = { router.push(AppRoute.WalletConnectSessions) },
onClick = { router.push(AppRoute.WalletConnectSessions(userWalletId)) },
)
} else {
null

View file

@ -20,7 +20,7 @@ import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.models.SaveWalletError
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
@ -37,7 +37,7 @@ import kotlin.coroutines.resume
internal class UserWalletSaver @Inject constructor(
private val scanCardProcessor: ScanCardProcessor,
private val saveWalletUseCase: SaveWalletUseCase,
private val userWalletBuilderFactory: UserWalletBuilder.Factory,
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
private val shouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase,
private val reduxStateHolder: ReduxStateHolder,
private val messageSender: UiMessageSender,
@ -112,7 +112,7 @@ internal class UserWalletSaver @Inject constructor(
}
private suspend fun Raise<Error>.createUserWallet(response: ScanResponse): UserWallet {
val userWallet = userWalletBuilderFactory.create(scanResponse = response).build()
val userWallet = coldUserWalletBuilderFactory.create(scanResponse = response).build()
return ensureNotNull(userWallet) { Error.Unknown }
}

View file

@ -10,7 +10,9 @@ android {
dependencies {
/* Project - Domain */
implementation(projects.domain.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.manageTokens.models)
/* Project - Core */
implementation(projects.core.ui)

View file

@ -0,0 +1,21 @@
package com.tangem.features.managetokens.component
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
interface ChooseManagedTokensComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
val initialCurrency: CryptoCurrency,
val source: Source,
)
enum class Source {
SendViaSwap,
}
interface Factory : ComponentFactory<Params, ChooseManagedTokensComponent>
}

View file

@ -4,4 +4,5 @@ enum class ManageTokensSource(val analyticsName: String) {
STORIES(analyticsName = "Stories"),
ONBOARDING(analyticsName = "Onboarding"),
SETTINGS(analyticsName = "Settings"),
SEND_VIA_SWAP(analyticsName = "SendViaSwap"),
}

View file

@ -18,9 +18,10 @@ dependencies {
/* Project - Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
implementation(projects.common.routing)
implementation(projects.core.configToggles)
implementation(projects.core.analytics)
implementation(projects.common.routing)
implementation(projects.common.ui)
/* Project - Domain */
implementation(projects.domain.card)

View file

@ -0,0 +1,72 @@
package com.tangem.features.managetokens.choosetoken
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
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.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.features.managetokens.choosetoken.entity.ChooseManageTokensBottomSheetConfig
import com.tangem.features.managetokens.choosetoken.model.ChooseManagedTokensModel
import com.tangem.features.managetokens.choosetoken.ui.ChooseManagedTokenContent
import com.tangem.features.managetokens.component.ChooseManagedTokensComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultChooseManagedTokensComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted private val params: ChooseManagedTokensComponent.Params,
) : ChooseManagedTokensComponent, AppComponentContext by context {
private val model: ChooseManagedTokensModel = getOrCreateModel(params)
private val bottomSheetSlot = childSlot(
source = model.bottomSheetNavigation,
serializer = ChooseManageTokensBottomSheetConfig.serializer(),
handleBackButton = false,
childFactory = ::bottomSheetChild,
)
@Composable
override fun Content(modifier: Modifier) {
val uiState by model.uiState.collectAsStateWithLifecycle()
val bottomSheet by bottomSheetSlot.subscribeAsState()
ChooseManagedTokenContent(
state = uiState,
)
bottomSheet.child?.instance?.BottomSheet()
}
@Suppress("UnusedPrivateMember")
private fun bottomSheetChild(
config: ChooseManageTokensBottomSheetConfig,
componentContext: ComponentContext,
): ComposableBottomSheetComponent = when (config) {
else -> getStubComponent()
}
private fun getStubComponent() = StubComponent()
class StubComponent : ComposableBottomSheetComponent {
override fun dismiss() {}
@Composable
override fun BottomSheet() {
/* no-op */
}
}
@AssistedFactory
interface Factory : ChooseManagedTokensComponent.Factory {
override fun create(
context: AppComponentContext,
params: ChooseManagedTokensComponent.Params,
): DefaultChooseManagedTokensComponent
}
}

View file

@ -0,0 +1,35 @@
package com.tangem.features.managetokens.choosetoken.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.managetokens.choosetoken.DefaultChooseManagedTokensComponent
import com.tangem.features.managetokens.choosetoken.model.ChooseManagedTokensModel
import com.tangem.features.managetokens.component.ChooseManagedTokensComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import javax.inject.Singleton
@InstallIn(ModelComponent::class)
@Module
internal interface ChooseManagedTokensModule {
@Binds
@IntoMap
@ClassKey(ChooseManagedTokensModel::class)
fun provideChooseManagedTokensModel(impl: ChooseManagedTokensModel): Model
}
@Module
@InstallIn(SingletonComponent::class)
internal interface ChooseManagedTokensModuleBinds {
@Binds
@Singleton
fun provideChooseManagedTokensComponent(
impl: DefaultChooseManagedTokensComponent.Factory,
): ChooseManagedTokensComponent.Factory
}

View file

@ -0,0 +1,17 @@
package com.tangem.features.managetokens.choosetoken.entity
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.serialization.Serializable
@Serializable
internal sealed class ChooseManageTokensBottomSheetConfig {
@Serializable
data class SwapTokensBottomSheetConfig(
val userWalletId: UserWalletId,
val initialCurrency: CryptoCurrency,
val token: ManagedCryptoCurrency.Token,
) : ChooseManageTokensBottomSheetConfig()
}

View file

@ -0,0 +1,9 @@
package com.tangem.features.managetokens.choosetoken.entity
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.features.managetokens.entity.managetokens.ManageTokensUM
internal data class ChooseManagedTokenUM(
val notificationUM: NotificationUM?,
val readContent: ManageTokensUM.ReadContent,
)

View file

@ -0,0 +1,270 @@
package com.tangem.features.managetokens.choosetoken.model
import androidx.annotation.StringRes
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.tangem.common.ui.notifications.NotificationUM
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.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.event.triggeredEvent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.features.managetokens.choosetoken.entity.ChooseManageTokensBottomSheetConfig
import com.tangem.features.managetokens.choosetoken.entity.ChooseManagedTokenUM
import com.tangem.features.managetokens.component.ChooseManagedTokensComponent
import com.tangem.features.managetokens.component.ChooseManagedTokensComponent.Source
import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import com.tangem.features.managetokens.entity.managetokens.ManageTokensTopBarUM
import com.tangem.features.managetokens.entity.managetokens.ManageTokensUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.utils.list.ManageTokensListManager
import com.tangem.features.managetokens.utils.list.getLoadingItems
import com.tangem.pagination.BatchFetchResult
import com.tangem.pagination.PaginationStatus
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import kotlin.collections.isNotEmpty
@ModelScoped
internal class ChooseManagedTokensModel @Inject constructor(
private val router: Router,
override val dispatchers: CoroutineDispatcherProvider,
private val uiMessageSender: UiMessageSender,
paramsContainer: ParamsContainer,
manageTokensListManagerFactory: ManageTokensListManager.Factory,
) : Model() {
private val params: ChooseManagedTokensComponent.Params = paramsContainer.require()
private val manageTokensListManager = manageTokensListManagerFactory.create(
onCurrencySelect = { token ->
bottomSheetNavigation.activate(
ChooseManageTokensBottomSheetConfig.SwapTokensBottomSheetConfig(
userWalletId = params.userWalletId,
initialCurrency = params.initialCurrency,
token = token,
),
)
},
)
val bottomSheetNavigation: SlotNavigation<ChooseManageTokensBottomSheetConfig> = SlotNavigation()
val uiState: StateFlow<ChooseManagedTokenUM>
field = MutableStateFlow<ChooseManagedTokenUM>(createReadContentModel())
init {
manageTokensListManager.uiItems
.onEach { items -> updateItems(items) }
.launchIn(modelScope)
manageTokensListManager.paginationStatus
.onEach { status -> updatePaginationStatus(status) }
.launchIn(modelScope)
observeSearchQueryChanges()
modelScope.launch {
manageTokensListManager.launchPagination(source = ManageTokensSource.SEND_VIA_SWAP, userWalletId = null)
}
}
private fun createReadContentModel(): ChooseManagedTokenUM {
return ChooseManagedTokenUM(
notificationUM = getNotification(),
readContent = ManageTokensUM.ReadContent(
popBack = router::pop,
isInitialBatchLoading = true,
isNextBatchLoading = false,
items = getLoadingItems(),
topBar = ManageTokensTopBarUM.ReadContent(
title = resourceReference(R.string.common_choose_token),
onBackButtonClick = router::pop,
),
search = SearchBarUM(
placeholderText = resourceReference(R.string.common_search),
query = "",
onQueryChange = ::searchCurrencies,
isActive = false,
onActiveChange = ::toggleSearchBar,
),
loadMore = ::loadMoreItems,
),
)
}
private fun getNotification(): NotificationUM? {
return when (params.source) {
Source.SendViaSwap -> ChooseManagedTokensNotificationUM.SendViaSwap(
onCloseClick = ::removeNotification,
)
}
}
private fun removeNotification() {
uiState.update {
it.copy(
notificationUM = null,
)
}
}
@OptIn(FlowPreview::class)
private fun observeSearchQueryChanges() {
uiState
.distinctUntilChanged { old, new ->
// It's also used to skip search activation to avoid searching an empty query
old.readContent.search.query == new.readContent.search.query && new.readContent.search.isActive
}
.transform { state ->
val query = state.readContent.search.query
if (state.readContent.search.isActive) {
emit(query)
}
}
.sample(periodMillis = 1_000)
.onEach { query -> manageTokensListManager.search(userWalletId = null, query = query) }
.launchIn(modelScope)
}
private fun updateItems(items: ImmutableList<CurrencyItemUM>) {
uiState.update { state ->
state.copy(
readContent = state.readContent.copy(items = items),
)
}
}
private fun consumeScrollToTopEvent() {
uiState.update { state ->
state.copy(
readContent = state.readContent.copy(scrollToTop = consumedEvent()),
)
}
}
private fun updatePaginationStatus(status: PaginationStatus<*>) {
uiState.update { state ->
val readContent = state.readContent
when (status) {
is PaginationStatus.None,
is PaginationStatus.InitialLoading,
-> {
if (readContent.search.isActive) {
state.copy(
readContent = readContent.copy(items = getLoadingItems()),
)
} else {
state.copy(
readContent = readContent.copy(
items = getLoadingItems(),
isInitialBatchLoading = true,
),
)
}
}
is PaginationStatus.NextBatchLoading -> state.copy(
readContent = readContent.copy(isNextBatchLoading = true),
)
is PaginationStatus.InitialLoadingError -> {
val message = SnackbarMessage(
message = status.throwable.localizedMessage
?.let(::stringReference)
?: resourceReference(R.string.common_error),
)
uiMessageSender.send(message)
state.copy(
readContent = readContent.copy(
isInitialBatchLoading = false,
isNextBatchLoading = false,
),
)
}
is PaginationStatus.Paginating -> {
(status.lastResult as? BatchFetchResult.Error)?.let { fetchError ->
Timber.e(fetchError.throwable)
}
state.copy(
readContent = readContent.copy(
isInitialBatchLoading = false,
isNextBatchLoading = false,
scrollToTop = if (readContent.isInitialBatchLoading && readContent.items.isNotEmpty()) {
triggeredEvent(
data = Unit,
onConsume = ::consumeScrollToTopEvent,
)
} else {
readContent.scrollToTop
},
),
)
}
is PaginationStatus.EndOfPagination -> {
state.copy(
readContent = readContent.copy(
isInitialBatchLoading = false,
isNextBatchLoading = false,
),
)
}
}
}
}
private fun searchCurrencies(query: String) {
uiState.update { state ->
state.copy(
readContent = state.readContent.copy(
search = state.readContent.search.copy(
query = query,
isActive = true,
),
),
)
}
}
private fun toggleSearchBar(isActive: Boolean) {
uiState.update { state ->
@StringRes val placeholderTextRes = if (isActive) {
R.string.manage_tokens_search_placeholder
} else {
R.string.common_search
}
state.copy(
readContent = state.readContent.copy(
search = state.readContent.search.copy(
placeholderText = resourceReference(placeholderTextRes),
isActive = isActive,
),
),
)
}
}
private fun loadMoreItems(): Boolean {
val state = uiState.value
if (state.readContent.isInitialBatchLoading || state.readContent.isNextBatchLoading) return false
modelScope.launch {
manageTokensListManager.loadMore(userWalletId = null, query = state.readContent.search.query)
}
return true
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.features.managetokens.choosetoken.model
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.managetokens.impl.R
internal object ChooseManagedTokensNotificationUM {
data class SendViaSwap(
val onCloseClick: () -> Unit,
) : NotificationUM.Info(
title = resourceReference(R.string.send_with_swap_title),
subtitle = resourceReference(R.string.send_with_swap_notification_text),
iconResId = R.drawable.ic_exchange_horizontal_24,
onCloseClick = onCloseClick,
)
}

View file

@ -0,0 +1,234 @@
package com.tangem.features.managetokens.choosetoken.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.list.InfiniteListHandler
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.rows.ChainRow
import com.tangem.core.ui.components.rows.model.ChainRowUM
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.features.managetokens.choosetoken.entity.ChooseManagedTokenUM
import com.tangem.features.managetokens.choosetoken.model.ChooseManagedTokensNotificationUM
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import com.tangem.features.managetokens.entity.managetokens.ManageTokensTopBarUM
import com.tangem.features.managetokens.entity.managetokens.ManageTokensUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.ui.*
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
@Composable
internal fun ChooseManagedTokenContent(state: ChooseManagedTokenUM, modifier: Modifier = Modifier) {
Scaffold(
modifier = modifier,
containerColor = TangemTheme.colors.background.tertiary,
contentWindowInsets = WindowInsetsZero,
topBar = {
ManageTokensTopBar(
modifier = Modifier.statusBarsPadding(),
topBar = state.readContent.topBar,
search = state.readContent.search,
)
},
content = { innerPadding ->
Content(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
state = state,
)
},
)
}
@Composable
private fun Content(state: ChooseManagedTokenUM, modifier: Modifier = Modifier) {
val listState = rememberLazyListState()
Box(modifier = modifier) {
Currencies(
modifier = Modifier.fillMaxSize(),
listState = listState,
notificationUM = state.notificationUM,
items = state.readContent.items,
showLoadingItem = state.readContent.isNextBatchLoading,
onLoadMore = state.readContent.loadMore,
)
}
EventEffect(event = state.readContent.scrollToTop) {
listState.animateScrollToItem(index = 0)
}
}
@Composable
private fun Currencies(
listState: LazyListState,
notificationUM: NotificationUM?,
items: ImmutableList<CurrencyItemUM>,
showLoadingItem: Boolean,
onLoadMore: () -> Boolean,
modifier: Modifier = Modifier,
) {
val bottomBarHeight = with(LocalDensity.current) {
WindowInsets.systemBars.getBottom(density = this).toDp()
}
LazyColumn(
modifier = modifier.padding(horizontal = 16.dp),
state = listState,
contentPadding = PaddingValues(
bottom = TangemTheme.dimens.spacing76 + bottomBarHeight,
),
) {
if (notificationUM?.config != null) {
item(key = "notification_key") {
Notification(
config = notificationUM.config,
iconTint = TangemTheme.colors.icon.accent,
modifier = Modifier
.animateItem()
.padding(bottom = 12.dp),
)
}
}
contentItems(items)
if (showLoadingItem) {
item(key = "loading_item") {
ProgressIndicator(
modifier = Modifier
.padding(vertical = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
)
}
}
}
InfiniteListHandler(
listState = listState,
buffer = LOAD_ITEMS_BUFFER,
onLoadMore = onLoadMore,
)
}
private fun LazyListScope.contentItems(items: ImmutableList<CurrencyItemUM>) {
itemsIndexed(
items = items,
key = { index, item -> item.id.value },
) { index, item ->
when (item) {
is CurrencyItemUM.Basic -> {
ChainRow(
model = with(item) {
ChainRowUM(
name = name,
type = symbol,
icon = icon,
showCustom = false,
)
},
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = index,
addDefaultPadding = false,
lastIndex = items.lastIndex,
)
.clickable(onClick = item.onExpandClick)
.background(TangemTheme.colors.background.action),
)
}
is CurrencyItemUM.Loading -> {
LoadingItem(
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = index,
addDefaultPadding = false,
lastIndex = items.lastIndex,
)
.fillMaxWidth()
.background(TangemTheme.colors.background.action),
)
}
is CurrencyItemUM.SearchNothingFound -> {
SearchNothingFoundText(
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = index,
addDefaultPadding = false,
lastIndex = items.lastIndex,
)
.background(TangemTheme.colors.background.action)
.fillParentMaxSize(),
)
}
else -> Unit
}
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun ChooseManagedTokenContent_Preview() {
TangemThemePreview {
ChooseManagedTokenContent(
state = ChooseManagedTokenUM(
notificationUM = ChooseManagedTokensNotificationUM.SendViaSwap({}),
readContent = ManageTokensUM.ReadContent(
popBack = {},
isInitialBatchLoading = false,
isNextBatchLoading = false,
items = buildList {
repeat(10) {
add(
CurrencyItemUM.Basic(
id = ManagedCryptoCurrency.ID(
value = "ID+$it",
),
name = "Bitcoin",
symbol = "BTC",
icon = CurrencyIconState.Loading,
networks = CurrencyItemUM.Basic.NetworksUM.Collapsed,
onExpandClick = {},
),
)
}
}.toPersistentList(),
topBar = ManageTokensTopBarUM.ReadContent(
title = resourceReference(R.string.common_choose_token),
onBackButtonClick = {},
),
search = SearchBarUM(
placeholderText = resourceReference(R.string.common_search),
query = "",
onQueryChange = {},
isActive = false,
onActiveChange = {},
),
loadMore = { true },
),
),
)
}
}
// endregion

View file

@ -14,7 +14,7 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.network.Network
import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent
import com.tangem.features.managetokens.component.AddCustomTokenComponent
import com.tangem.features.managetokens.component.CustomTokenFormComponent

View file

@ -9,7 +9,7 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.managetokens.ValidateDerivationPathUseCase
import com.tangem.domain.managetokens.model.exceptoin.DerivationPathValidationException
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.network.Network
import com.tangem.features.managetokens.component.CustomTokenDerivationInputComponent
import com.tangem.features.managetokens.entity.customtoken.CustomDerivationInputUM
import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath

View file

@ -3,7 +3,7 @@ package com.tangem.features.managetokens.component.preview
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.network.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent.Params
@ -26,17 +26,19 @@ internal class PreviewCustomTokenSelectorComponent(
) : CustomTokenSelectorComponent {
private val previewItems = List(size = itemsSize) { index ->
val derivationPath = Network.DerivationPath.Card("m/44'/0'/0'/0/$index")
when (params) {
is Params.DerivationPathSelector -> {
val d = SelectedDerivationPath(
id = Network.ID(index.toString()),
value = Network.DerivationPath.Card("m/44'/0'/0'/0/$index"),
id = Network.ID(value = index.toString(), derivationPath = derivationPath),
value = derivationPath,
name = "Network $index",
isDefault = false,
)
DerivationPathUM(
id = d.id?.value ?: "",
id = d.id?.rawId?.value ?: "",
value = d.value.value.orEmpty(),
networkName = stringReference(d.name),
isSelected = d.value == params.selectedDerivationPath?.value,
@ -45,16 +47,16 @@ internal class PreviewCustomTokenSelectorComponent(
}
is Params.NetworkSelector -> {
val n = SelectedNetwork(
id = Network.ID(index.toString()),
id = Network.ID(value = index.toString(), derivationPath = derivationPath),
name = "Network $index",
derivationPath = Network.DerivationPath.Card("m/44'/0'/0'/0/$index"),
derivationPath = derivationPath,
canHandleTokens = false,
)
CurrencyNetworkUM(
network = Network(
id = n.id,
backendId = n.id.value,
backendId = n.id.rawId.value,
name = "Network $index",
currencySymbol = "N$index",
derivationPath = Network.DerivationPath.Card(""),
@ -76,7 +78,7 @@ internal class PreviewCustomTokenSelectorComponent(
}
}.toImmutableList()
val previewState = CustomTokenSelectorUM(
private val previewState = CustomTokenSelectorUM(
header = when (params) {
is Params.DerivationPathSelector -> CustomTokenSelectorUM.HeaderUM.CustomDerivationButton(
value = null,

View file

@ -11,7 +11,7 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.network.Network
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM
@ -27,7 +27,7 @@ import kotlinx.coroutines.flow.update
internal class PreviewManageTokensComponent(
private val isLoading: Boolean,
private val showTangemIcon: Boolean,
showTangemIcon: Boolean,
params: ManageTokensComponent.Params,
) : ManageTokensComponent {
@ -150,13 +150,14 @@ internal class PreviewManageTokensComponent(
)
private fun getCurrencyNetworks(currencyIndex: Int) = List(size = 3) { networkIndex ->
val derivationPath = Network.DerivationPath.Card("")
CurrencyNetworkUM(
network = Network(
id = Network.ID(networkIndex.toString()),
id = Network.ID(value = networkIndex.toString(), derivationPath = derivationPath),
backendId = networkIndex.toString(),
name = "Network $networkIndex",
currencySymbol = "N$networkIndex",
derivationPath = Network.DerivationPath.Card(""),
derivationPath = derivationPath,
isTestnet = false,
standardType = Network.StandardType.ERC20,
hasFiatFeeRate = false,

View file

@ -7,7 +7,7 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.network.Network
import com.tangem.features.managetokens.component.OnboardingManageTokensComponent
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM
@ -90,13 +90,15 @@ internal class PreviewOnboardingManageTokensComponent(
)
private fun getCurrencyNetworks() = List(size = 3) { networkIndex ->
val derivationPath = Network.DerivationPath.Card("")
CurrencyNetworkUM(
network = Network(
id = Network.ID(networkIndex.toString()),
id = Network.ID(value = networkIndex.toString(), derivationPath = derivationPath),
backendId = networkIndex.toString(),
name = "Network $networkIndex",
currencySymbol = "N$networkIndex",
derivationPath = Network.DerivationPath.Card(""),
derivationPath = derivationPath,
isTestnet = false,
standardType = Network.StandardType.ERC20,
hasFiatFeeRate = false,

View file

@ -1,6 +1,6 @@
package com.tangem.features.managetokens.entity.customtoken
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.network.Network
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.serialization.Serializable

View file

@ -1,6 +1,6 @@
package com.tangem.features.managetokens.entity.item
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.network.Network
internal data class CurrencyNetworkUM(
val network: Network,
@ -13,5 +13,5 @@ internal data class CurrencyNetworkUM(
override val onSelectedStateChange: (Boolean) -> Unit,
) : SelectableItemUM {
override val id: String = network.id.value
override val id: String = network.rawId
}

View file

@ -12,9 +12,9 @@ import com.tangem.core.ui.message.DialogMessage
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.card.HasMissedDerivationsUseCase
import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent
import com.tangem.features.managetokens.component.CustomTokenFormComponent
import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM

View file

@ -10,7 +10,7 @@ import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.managetokens.GetSupportedNetworksUseCase
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.network.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent.Params.DerivationPathSelector

View file

@ -46,16 +46,18 @@ import javax.inject.Inject
internal class ManageTokensModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val manageTokensListManager: ManageTokensListManager,
private val messageSender: UiMessageSender,
private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase,
private val saveManagedTokensUseCase: SaveManagedTokensUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
manageTokensListManagerFactory: ManageTokensListManager.Factory,
paramsContainer: ParamsContainer,
) : Model() {
private val params: ManageTokensComponent.Params = paramsContainer.require()
private val manageTokensListManager = manageTokensListManagerFactory.create()
val state: MutableStateFlow<ManageTokensUM> = MutableStateFlow(getInitialState(params.userWalletId))
val bottomSheetNavigation: SlotNavigation<ManageTokensBottomSheetConfig> = SlotNavigation()

View file

@ -40,16 +40,18 @@ import javax.inject.Inject
@ModelScoped
internal class OnboardingManageTokensModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val manageTokensListManager: ManageTokensListManager,
private val messageSender: UiMessageSender,
private val reduxStateHolder: ReduxStateHolder,
private val saveManagedTokensUseCase: SaveManagedTokensUseCase,
private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
manageTokensListManagerFactory: ManageTokensListManager.Factory,
paramsContainer: ParamsContainer,
) : Model() {
private val params: OnboardingManageTokensComponent.Params = paramsContainer.require()
private val manageTokensListManager = manageTokensListManagerFactory.create()
val state: MutableStateFlow<OnboardingManageTokensUM> = MutableStateFlow(getInitialState())
val returnToParentComponentFlow = MutableSharedFlow<Unit>()

View file

@ -12,11 +12,11 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
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.sheet.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.network.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.managetokens.component.AddCustomTokenComponent
import com.tangem.features.managetokens.component.preview.PreviewAddCustomTokenComponent
@ -76,7 +76,7 @@ private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider<
userWalletId = UserWalletId(stringValue = "321"),
step = AddCustomTokenConfig.Step.FORM,
selectedNetwork = SelectedNetwork(
id = Network.ID(value = "1"),
id = Network.ID(value = "1", derivationPath = Network.DerivationPath.None),
name = "Ethereum",
derivationPath = Network.DerivationPath.None,
canHandleTokens = false,
@ -88,7 +88,7 @@ private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider<
userWalletId = UserWalletId(stringValue = "321"),
step = AddCustomTokenConfig.Step.NETWORK_SELECTOR,
selectedNetwork = SelectedNetwork(
id = Network.ID(value = "0"),
id = Network.ID(value = "0", derivationPath = Network.DerivationPath.None),
name = "Ethereum",
derivationPath = Network.DerivationPath.None,
canHandleTokens = false,
@ -100,7 +100,7 @@ private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider<
userWalletId = UserWalletId(stringValue = "321"),
step = AddCustomTokenConfig.Step.DERIVATION_PATH_SELECTOR,
selectedDerivationPath = SelectedDerivationPath(
id = Network.ID(value = "0"),
id = Network.ID(value = "0", derivationPath = Network.DerivationPath.None),
value = Network.DerivationPath.None,
name = "Ethereum",
isDefault = false,

View file

@ -30,7 +30,7 @@ import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.network.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent
import com.tangem.features.managetokens.component.preview.PreviewCustomTokenSelectorComponent
@ -266,20 +266,23 @@ private fun Preview_CustomTokenNetworkSelectorContent(
private class CustomTokenNetworkSelectorComponentPreviewProvider :
PreviewParameterProvider<CustomTokenSelectorComponent> {
private val derivationPath = Network.DerivationPath.Card("m/44'/0'/0'/0/0")
override val values: Sequence<CustomTokenSelectorComponent>
get() = sequenceOf(
PreviewCustomTokenSelectorComponent(
params = CustomTokenSelectorComponent.Params.DerivationPathSelector(
userWalletId = UserWalletId(stringValue = "321"),
selectedNetwork = SelectedNetwork(
id = Network.ID(value = "0"),
id = Network.ID(value = "0", derivationPath = derivationPath),
name = "Ethereum",
derivationPath = Network.DerivationPath.Card("m/44'/0'/0'/0/0"),
derivationPath = derivationPath,
canHandleTokens = true,
),
selectedDerivationPath = SelectedDerivationPath(
id = Network.ID(value = "0"),
value = Network.DerivationPath.Card("m/44'/0'/0'/0/0"),
id = Network.ID(value = "0", derivationPath = derivationPath),
value = derivationPath,
name = "",
isDefault = false,
),
@ -290,9 +293,9 @@ private class CustomTokenNetworkSelectorComponentPreviewProvider :
params = CustomTokenSelectorComponent.Params.NetworkSelector(
userWalletId = UserWalletId(stringValue = "321"),
selectedNetwork = SelectedNetwork(
id = Network.ID(value = "0"),
id = Network.ID(value = "0", derivationPath = derivationPath),
name = "Ethereum",
derivationPath = Network.DerivationPath.Card("m/44'/0'/0'/0/0"),
derivationPath = derivationPath,
canHandleTokens = true,
),
onNetworkSelected = {},

View file

@ -32,8 +32,6 @@ import com.tangem.core.ui.components.BottomFade
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.TangemSwitch
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.components.buttons.common.TangemButton
@ -41,8 +39,6 @@ import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.fields.SearchBar
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.list.InfiniteListHandler
import com.tangem.core.ui.components.rows.ArrowRow
import com.tangem.core.ui.components.rows.BlockchainRow
@ -51,7 +47,6 @@ import com.tangem.core.ui.components.rows.ChainRowContainer
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
import com.tangem.core.ui.components.rows.model.ChainRowUM
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.haptic.TangemHapticEffect
@ -66,14 +61,13 @@ import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.managetokens.component.preview.PreviewManageTokensComponent
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import com.tangem.features.managetokens.entity.item.CurrencyItemUM.Basic.NetworksUM
import com.tangem.features.managetokens.entity.managetokens.ManageTokensTopBarUM
import com.tangem.features.managetokens.entity.managetokens.ManageTokensUM
import com.tangem.features.managetokens.impl.R
import kotlinx.collections.immutable.ImmutableList
private const val CHEVRON_ROTATION_EXPANDED = 180f
private const val CHEVRON_ROTATION_COLLAPSED = 0f
private const val LOAD_ITEMS_BUFFER = 10
internal const val LOAD_ITEMS_BUFFER = 10
@Composable
internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modifier) {
@ -85,7 +79,9 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi
contentWindowInsets = WindowInsetsZero,
topBar = {
ManageTokensTopBar(
modifier = Modifier.statusBarsPadding(),
modifier = Modifier
.statusBarsPadding()
.background(TangemTheme.colors.background.primary),
topBar = state.topBar,
search = state.search,
)
@ -116,31 +112,6 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi
)
}
@Composable
private fun ManageTokensTopBar(topBar: ManageTokensTopBarUM?, search: SearchBarUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier.background(TangemTheme.colors.background.primary),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
) {
if (topBar != null) {
TangemTopAppBar(
title = topBar.title.resolveReference(),
startButton = TopAppBarButtonUM.Back(topBar.onBackButtonClick),
endButton = when (topBar) {
is ManageTokensTopBarUM.ManageContent -> topBar.endButton
is ManageTokensTopBarUM.ReadContent -> null
},
)
}
SearchBar(
modifier = Modifier
.padding(bottom = TangemTheme.dimens.spacing12)
.padding(horizontal = TangemTheme.dimens.spacing16),
state = search,
)
}
}
@Composable
private fun SaveChangesButton(
isVisible: Boolean,
@ -263,7 +234,7 @@ internal fun Currencies(
}
@Composable
private fun SearchNothingFoundText(modifier: Modifier = Modifier) {
fun SearchNothingFoundText(modifier: Modifier = Modifier) {
Box(
modifier = modifier,
contentAlignment = Alignment.Center,
@ -277,7 +248,7 @@ private fun SearchNothingFoundText(modifier: Modifier = Modifier) {
}
@Composable
private fun ProgressIndicator(modifier: Modifier = Modifier) {
fun ProgressIndicator(modifier: Modifier = Modifier) {
Box(
modifier = modifier.background(color = TangemTheme.colors.background.primary),
contentAlignment = Alignment.Center,
@ -287,7 +258,7 @@ private fun ProgressIndicator(modifier: Modifier = Modifier) {
}
@Composable
private fun LoadingItem(modifier: Modifier = Modifier) {
fun LoadingItem(modifier: Modifier = Modifier) {
ChainRowContainer(
modifier = modifier,
icon = {

View file

@ -0,0 +1,39 @@
package com.tangem.features.managetokens.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.fields.SearchBar
import com.tangem.core.ui.components.fields.TangemSearchBarDefaults
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.managetokens.entity.managetokens.ManageTokensTopBarUM
@Composable
internal fun ManageTokensTopBar(topBar: ManageTokensTopBarUM?, search: SearchBarUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
) {
if (topBar != null) {
TangemTopAppBar(
title = topBar.title.resolveReference(),
startButton = TopAppBarButtonUM.Back(topBar.onBackButtonClick),
endButton = when (topBar) {
is ManageTokensTopBarUM.ManageContent -> topBar.endButton
is ManageTokensTopBarUM.ReadContent -> null
},
)
}
SearchBar(
colors = TangemSearchBarDefaults.secondaryTextFieldColors,
state = search,
modifier = Modifier
.padding(bottom = TangemTheme.dimens.spacing12)
.padding(horizontal = TangemTheme.dimens.spacing16),
)
}
}

View file

@ -9,8 +9,8 @@ import com.tangem.domain.managetokens.ValidateTokenFormUseCase
import com.tangem.domain.managetokens.model.AddCustomTokenForm
import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException
import com.tangem.domain.managetokens.model.exceptoin.FindTokenException
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveInAndJoin

View file

@ -1,7 +1,7 @@
package com.tangem.features.managetokens.utils.list
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.network.Network
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update

View file

@ -2,20 +2,24 @@ package com.tangem.features.managetokens.utils.list
import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.managetokens.*
import com.tangem.domain.managetokens.model.*
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.network.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent
import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.utils.ui.toggleExpanded
import com.tangem.pagination.Batch
import com.tangem.pagination.BatchAction
import com.tangem.pagination.BatchListState
@ -24,6 +28,9 @@ import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -32,11 +39,9 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped
internal class ManageTokensListManager @Inject constructor(
@Suppress("LongParameterList", "LargeClass")
internal class ManageTokensListManager @AssistedInject constructor(
private val getManagedTokensUseCase: GetManagedTokensUseCase,
private val getDistinctManagedTokensUseCase: GetDistinctManagedCurrenciesUseCase,
private val checkHasLinkedTokensUseCase: CheckHasLinkedTokensUseCase,
@ -45,7 +50,8 @@ internal class ManageTokensListManager @Inject constructor(
private val messageSender: UiMessageSender,
private val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
clipboardManager: ClipboardManager,
private val clipboardManager: ClipboardManager,
@Assisted private val onCurrencySelect: (ManagedCryptoCurrency.Token) -> Unit = {},
) : ManageTokensUiActions {
private lateinit var scope: CoroutineScope
@ -67,7 +73,6 @@ internal class ManageTokensListManager @Inject constructor(
actions = this,
scopeProvider = Provider { scope },
sourceProvider = Provider { source },
clipboardManager = clipboardManager,
)
val currenciesToAdd: StateFlow<ChangedCurrencies> = changedCurrenciesManager.currenciesToAdd.asStateFlow()
@ -186,6 +191,14 @@ internal class ManageTokensListManager @Inject constructor(
}
}
override fun onTokenClick(currency: ManagedCryptoCurrency.Token) {
if (source == ManageTokensSource.SEND_VIA_SWAP) {
onCurrencySelect(currency)
} else {
toggleCurrencyNetworksVisibility(currency)
}
}
override fun addCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network) {
changedCurrenciesManager.addCurrency(currency, network)
@ -298,4 +311,168 @@ internal class ManageTokensListManager @Inject constructor(
null
}
}
private fun toggleCurrencyNetworksVisibility(currency: ManagedCryptoCurrency.Token) = scope.launch(
dispatchers.default,
) {
state.update { batches ->
val batchIndex = batches.batchIndexByCurrencyId(currency.id)
val currencyBatch = batches.currencyBatches[batchIndex]
val currencyIndex = currencyBatch.currencyIndexById(currency.id)
val uiBatch = batches.uiBatches[batchIndex]
val updatedUiItem = uiBatch.data[currencyIndex].toggleExpanded(
currency = currencyBatch.data[currencyIndex],
isEditable = batches.canEditItems,
onSelectCurrencyNetwork = { networkId, isSelected ->
selectNetwork(currencyBatch.key, currency, networkId, isSelected)
},
onLongTap = ::copyContractAddress,
)
batches.updateUiBatchesItem(
indexToBatch = batchIndex to uiBatch,
indexToItem = currencyIndex to updatedUiItem,
)
}
}
private fun copyContractAddress(source: ManagedCryptoCurrency.SourceNetwork) {
if (source is ManagedCryptoCurrency.SourceNetwork.Default) {
clipboardManager.setText(text = source.contractAddress, isSensitive = false)
showSnackbarMessage(resourceReference(R.string.contract_address_copied_message))
}
}
private fun showSnackbarMessage(messageText: TextReference) {
val message = SnackbarMessage(message = messageText)
messageSender.send(message)
}
private fun selectNetwork(
batchKey: Int,
currency: ManagedCryptoCurrency,
source: ManagedCryptoCurrency.SourceNetwork,
isSelected: Boolean,
) = scope.launch(dispatchers.default) {
if (currency !is ManagedCryptoCurrency.Token) return@launch
if (isSelected) {
val userWalletId = state.value.userWalletId
val unsupportedState = userWalletId?.let { checkCurrencyUnsupportedState(it, source) }
if (unsupportedState != null) {
showUnsupportedWarning(unsupportedState)
} else {
addCurrency(batchKey, currency, source.network)
}
} else {
if (checkNeedToShowRemoveNetworkWarning(currency, source.network)) {
showRemoveNetworkWarning(
currency = currency,
network = source.network,
isCoin = source is ManagedCryptoCurrency.SourceNetwork.Main,
onConfirm = {
removeCurrency(batchKey, currency, source.network)
},
)
} else {
removeCurrency(batchKey, currency, source.network)
}
}
}
private fun showUnsupportedWarning(unsupportedState: CurrencyUnsupportedState) {
val message = DialogMessage(
title = resourceReference(R.string.common_warning),
message = when (unsupportedState) {
is CurrencyUnsupportedState.Token.NetworkTokensUnsupported -> resourceReference(
id = R.string.alert_manage_tokens_unsupported_message,
formatArgs = wrappedList(unsupportedState.networkName),
)
is CurrencyUnsupportedState.Token.UnsupportedCurve -> resourceReference(
id = R.string.alert_manage_tokens_unsupported_curve_message,
formatArgs = wrappedList(unsupportedState.networkName),
)
is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference(
id = R.string.alert_manage_tokens_unsupported_curve_message,
formatArgs = wrappedList(unsupportedState.networkName),
)
},
)
messageSender.send(message)
}
private suspend fun showRemoveNetworkWarning(
currency: ManagedCryptoCurrency,
network: Network,
isCoin: Boolean,
onConfirm: () -> Unit,
) {
val userWalletId = state.value.userWalletId
val hasLinkedTokens = if (userWalletId == null || !isCoin) {
false
} else {
checkHasLinkedTokens(userWalletId, network)
}
val canHideWithoutConfirming = source == ManageTokensSource.ONBOARDING
if (hasLinkedTokens) {
showLinkedTokensWarning(currency, network)
} else if (canHideWithoutConfirming) {
onConfirm()
} else {
showHideTokenWarning(currency, onConfirm)
}
}
private fun showLinkedTokensWarning(currency: ManagedCryptoCurrency, network: Network) {
val message = DialogMessage(
title = resourceReference(
id = R.string.token_details_unable_hide_alert_title,
formatArgs = wrappedList(currency.name),
),
message = resourceReference(
id = R.string.token_details_unable_hide_alert_message,
formatArgs = wrappedList(
currency.name,
currency.symbol,
network.name,
),
),
)
messageSender.send(message)
}
private fun showHideTokenWarning(currency: ManagedCryptoCurrency, onConfirm: () -> Unit) {
val message = DialogMessage(
title = resourceReference(
id = R.string.token_details_hide_alert_title,
formatArgs = wrappedList(currency.name),
),
message = resourceReference(R.string.token_details_hide_alert_message),
firstActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.token_details_hide_alert_hide),
warning = true,
onClick = onConfirm,
)
},
secondActionBuilder = { cancelAction() },
)
messageSender.send(message)
}
private fun Batch<Int, List<ManagedCryptoCurrency>>.currencyIndexById(id: ManagedCryptoCurrency.ID): Int {
return data
.indexOfFirst { it.id == id }
.takeIf { it != -1 }
?: error("Currency with currency '$id' not found in batch #$key")
}
@AssistedFactory
interface Factory {
fun create(onCurrencySelect: (ManagedCryptoCurrency.Token) -> Unit = {}): ManageTokensListManager
}
}

View file

@ -2,11 +2,13 @@ package com.tangem.features.managetokens.utils.list
import com.tangem.domain.managetokens.model.CurrencyUnsupportedState
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.network.Network
import com.tangem.domain.wallets.models.UserWalletId
internal interface ManageTokensUiActions {
fun onTokenClick(currency: ManagedCryptoCurrency.Token)
fun addCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network)
fun removeCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network)

View file

@ -1,21 +1,16 @@
package com.tangem.features.managetokens.utils.list
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.managetokens.model.CurrencyUnsupportedState
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.network.Network
import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.utils.mapper.toUiModel
import com.tangem.features.managetokens.utils.ui.toggleExpanded
import com.tangem.features.managetokens.utils.ui.update
import com.tangem.pagination.Batch
import com.tangem.utils.Provider
@ -25,7 +20,10 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.launch
@Suppress("LongParameterList")
@ -36,7 +34,6 @@ internal class ManageTokensUiManager(
private val scopeProvider: Provider<CoroutineScope>,
private val sourceProvider: Provider<ManageTokensSource>,
private val actions: ManageTokensUiActions,
private val clipboardManager: ClipboardManager,
) {
private val scope: CoroutineScope
@ -72,7 +69,7 @@ internal class ManageTokensUiManager(
item.toUiModel(
isEditable = canEditItems,
onRemoveCustomCurrencyClick = ::removeCustomCurrency,
onExpandNetworksClick = ::toggleCurrencyNetworksVisibility,
onTokenClick = actions::onTokenClick,
)
},
)
@ -96,7 +93,7 @@ internal class ManageTokensUiManager(
item.toUiModel(
isEditable = canEditItems,
onRemoveCustomCurrencyClick = ::removeCustomCurrency,
onExpandNetworksClick = ::toggleCurrencyNetworksVisibility,
onTokenClick = actions::onTokenClick,
)
} else {
previousUiItem.update(item)
@ -123,97 +120,6 @@ internal class ManageTokensUiManager(
)
}
private fun toggleCurrencyNetworksVisibility(currency: ManagedCryptoCurrency.Token) = scope.launch(
dispatchers.default,
) {
state.update { batches ->
val batchIndex = batches.batchIndexByCurrencyId(currency.id)
val currencyBatch = batches.currencyBatches[batchIndex]
val currencyIndex = currencyBatch.currencyIndexById(currency.id)
val uiBatch = batches.uiBatches[batchIndex]
val updatedUiItem = uiBatch.data[currencyIndex].toggleExpanded(
currency = currencyBatch.data[currencyIndex],
isEditable = batches.canEditItems,
onSelectCurrencyNetwork = { networkId, isSelected ->
selectNetwork(currencyBatch.key, currency, networkId, isSelected)
},
onLongTap = ::copyContractAddress,
)
batches.updateUiBatchesItem(
indexToBatch = batchIndex to uiBatch,
indexToItem = currencyIndex to updatedUiItem,
)
}
}
private fun copyContractAddress(source: ManagedCryptoCurrency.SourceNetwork) {
if (source is ManagedCryptoCurrency.SourceNetwork.Default) {
clipboardManager.setText(text = source.contractAddress, isSensitive = false)
showSnackbarMessage(resourceReference(R.string.contract_address_copied_message))
}
}
private fun showSnackbarMessage(messageText: TextReference) {
val message = SnackbarMessage(message = messageText)
messageSender.send(message)
}
private fun selectNetwork(
batchKey: Int,
currency: ManagedCryptoCurrency,
source: ManagedCryptoCurrency.SourceNetwork,
isSelected: Boolean,
) = scope.launch(dispatchers.default) {
if (currency !is ManagedCryptoCurrency.Token) return@launch
if (isSelected) {
val userWalletId = state.value.userWalletId
val unsupportedState = userWalletId?.let { actions.checkCurrencyUnsupportedState(it, source) }
if (unsupportedState != null) {
showUnsupportedWarning(unsupportedState)
} else {
actions.addCurrency(batchKey, currency, source.network)
}
} else {
if (actions.checkNeedToShowRemoveNetworkWarning(currency, source.network)) {
showRemoveNetworkWarning(
currency = currency,
network = source.network,
isCoin = source is ManagedCryptoCurrency.SourceNetwork.Main,
onConfirm = {
actions.removeCurrency(batchKey, currency, source.network)
},
)
} else {
actions.removeCurrency(batchKey, currency, source.network)
}
}
}
private fun showUnsupportedWarning(unsupportedState: CurrencyUnsupportedState) {
val message = DialogMessage(
title = resourceReference(R.string.common_warning),
message = when (unsupportedState) {
is CurrencyUnsupportedState.Token.NetworkTokensUnsupported -> resourceReference(
id = R.string.alert_manage_tokens_unsupported_message,
formatArgs = wrappedList(unsupportedState.networkName),
)
is CurrencyUnsupportedState.Token.UnsupportedCurve -> resourceReference(
id = R.string.alert_manage_tokens_unsupported_curve_message,
formatArgs = wrappedList(unsupportedState.networkName),
)
is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference(
id = R.string.alert_manage_tokens_unsupported_curve_message,
formatArgs = wrappedList(unsupportedState.networkName),
)
},
)
messageSender.send(message)
}
private suspend fun showRemoveNetworkWarning(
currency: ManagedCryptoCurrency,
network: Network,
@ -274,11 +180,4 @@ internal class ManageTokensUiManager(
messageSender.send(message)
}
private fun Batch<Int, List<ManagedCryptoCurrency>>.currencyIndexById(id: ManagedCryptoCurrency.ID): Int {
return data
.indexOfFirst { it.id == id }
.takeIf { it != -1 }
?: error("Currency with currency '$id' not found in batch #$key")
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.features.managetokens.utils.list
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
internal fun getLoadingItems(): ImmutableList<CurrencyItemUM> {
return List(size = 10) { index ->
CurrencyItemUM.Loading(index)
}.toPersistentList()
}

View file

@ -11,11 +11,11 @@ import com.tangem.features.managetokens.utils.ui.getIconRes
internal fun ManagedCryptoCurrency.toUiModel(
isEditable: Boolean,
onExpandNetworksClick: (ManagedCryptoCurrency.Token) -> Unit,
onTokenClick: (ManagedCryptoCurrency.Token) -> Unit,
onRemoveCustomCurrencyClick: (ManagedCryptoCurrency.Custom) -> Unit,
): CurrencyItemUM = when (this) {
is ManagedCryptoCurrency.Custom -> toUiModel(onRemoveCustomCurrencyClick)
is ManagedCryptoCurrency.Token -> toUiModel(isEditable, onExpandNetworksClick)
is ManagedCryptoCurrency.Token -> toUiModel(isEditable, onTokenClick)
}
private fun ManagedCryptoCurrency.Custom.toUiModel(
@ -53,7 +53,7 @@ private fun ManagedCryptoCurrency.Custom.toUiModel(
private fun ManagedCryptoCurrency.Token.toUiModel(
isEditable: Boolean,
onExpandNetworksClick: (ManagedCryptoCurrency.Token) -> Unit,
onTokenClick: (ManagedCryptoCurrency.Token) -> Unit,
): CurrencyItemUM {
val background = TangemColorPalette.Black
@ -71,7 +71,7 @@ private fun ManagedCryptoCurrency.Token.toUiModel(
),
networks = NetworksUM.Collapsed,
onExpandClick = {
onExpandNetworksClick(this)
onTokenClick(this)
},
)
}

View file

@ -2,7 +2,7 @@ package com.tangem.features.managetokens.utils.mapper
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.network.Network
import com.tangem.features.managetokens.entity.item.CurrencyItemUM.Basic.NetworksUM
import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM
import com.tangem.features.managetokens.utils.ui.getIconRes

View file

@ -2,7 +2,7 @@ package com.tangem.features.managetokens.utils.mapper
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.network.Network
import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork
import com.tangem.features.managetokens.entity.item.DerivationPathUM
import com.tangem.features.managetokens.impl.R
@ -12,7 +12,7 @@ internal fun Network.toDerivationPathModel(
onSelectedStateChange: (Boolean) -> Unit,
): DerivationPathUM? {
return DerivationPathUM(
id = id.value,
id = rawId,
value = derivationPath.value ?: return null,
networkName = stringReference(name),
isSelected = isSelected,
@ -25,7 +25,7 @@ internal fun SelectedNetwork.toDerivationPathModel(
onSelectedStateChange: (Boolean) -> Unit,
): DerivationPathUM? {
return DerivationPathUM(
id = id.value,
id = id.rawId.value,
value = derivationPath.value ?: return null,
networkName = resourceReference(R.string.custom_token_derivation_path_default),
isSelected = isSelected,

View file

@ -3,7 +3,7 @@ package com.tangem.features.managetokens.utils.ui
import androidx.annotation.DrawableRes
import com.tangem.core.ui.extensions.getActiveIconRes
import com.tangem.core.ui.extensions.getGreyedOutIconRes
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.network.Network
import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM
internal fun CurrencyNetworkUM.select(isSelected: Boolean): CurrencyNetworkUM {
@ -15,7 +15,7 @@ internal fun CurrencyNetworkUM.select(isSelected: Boolean): CurrencyNetworkUM {
@DrawableRes
internal fun Network.ID.getIconRes(isColored: Boolean): Int = if (isColored) {
getActiveIconRes(value)
getActiveIconRes(rawId.value)
} else {
getGreyedOutIconRes(value)
getGreyedOutIconRes(rawId.value)
}

View file

@ -5,7 +5,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.managetokens.ValidateTokenFormUseCase
import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM.TokenFormUM.Field
import com.tangem.features.managetokens.impl.R

View file

@ -0,0 +1,8 @@
package com.tangem.features.markets.deeplink
interface MarketsDeepLinkHandler {
interface Factory {
fun create(): MarketsDeepLinkHandler
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.features.markets.deeplink
import kotlinx.coroutines.CoroutineScope
interface MarketsTokenDetailDeepLinkHandler {
interface Factory {
fun create(coroutineScope: CoroutineScope, params: Map<String, String>): MarketsTokenDetailDeepLinkHandler
}
}

View file

@ -3,7 +3,7 @@ package com.tangem.features.markets.token.block
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import kotlinx.serialization.Serializable
@Stable

View file

@ -0,0 +1,23 @@
package com.tangem.features.markets.tokenlist
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.markets.entry.BottomSheetState
@Stable
interface MarketsTokenListComponent : ComposableContentComponent {
@Composable
fun BottomSheetContent(
bottomSheetState: State<BottomSheetState>,
onHeaderSizeChange: (Dp) -> Unit,
modifier: Modifier,
)
interface Factory : ComponentFactory<Unit, MarketsTokenListComponent>
}

View file

@ -17,6 +17,9 @@ dependencies {
api(projects.features.onramp.api)
implementation(projects.core.navigation)
/* Data */
implementation(projects.data.common)
/* Domain */
implementation(projects.domain.appCurrency)
implementation(projects.domain.appCurrency.models)

View file

@ -0,0 +1,20 @@
package com.tangem.features.markets.deeplink
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultMarketsDeepLinkHandler @AssistedInject constructor(
appRouter: AppRouter,
) : MarketsDeepLinkHandler {
init {
appRouter.push(AppRoute.Markets)
}
@AssistedFactory
interface Factory : MarketsDeepLinkHandler.Factory {
override fun create(): DefaultMarketsDeepLinkHandler
}
}

View file

@ -0,0 +1,79 @@
package com.tangem.features.markets.deeplink
import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.GetTokenMarketInfoUseCase
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.currency.CryptoCurrency
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
internal class DefaultMarketsTokenDetailDeepLinkHandler @AssistedInject constructor(
@Assisted scope: CoroutineScope,
@Assisted queryParams: Map<String, String>,
appRouter: AppRouter,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase,
) : MarketsTokenDetailDeepLinkHandler {
init {
val tokenId = queryParams[TOKEN_ID_KEY]
val rawTokenId = CryptoCurrency.RawID(tokenId.orEmpty())
scope.launch {
val appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse {
AppCurrency.Default
}
val tokenInfo = getTokenMarketInfoUseCase(
appCurrency = appCurrency,
tokenId = rawTokenId,
tokenSymbol = TOKEN_SYMBOL_KEY,
).getOrElse {
Timber.e("Failed to get market token info")
return@launch
}
appRouter.push(
AppRoute.MarketsTokenDetails(
token = TokenMarketParams(
id = rawTokenId,
name = tokenInfo.name,
symbol = tokenInfo.symbol,
tokenQuotes = TokenMarketParams.Quotes(
currentPrice = tokenInfo.quotes.currentPrice,
h24Percent = tokenInfo.quotes.h24ChangePercent,
weekPercent = tokenInfo.quotes.weekChangePercent,
monthPercent = tokenInfo.quotes.monthChangePercent,
),
imageUrl = getTokenIconUrlFromDefaultHost(rawTokenId),
),
appCurrency = appCurrency,
showPortfolio = true,
analyticsParams = null,
),
)
}
}
@AssistedFactory
interface Factory : MarketsTokenDetailDeepLinkHandler.Factory {
override fun create(
coroutineScope: CoroutineScope,
queryParams: Map<String, String>,
): DefaultMarketsTokenDetailDeepLinkHandler
}
private companion object {
const val TOKEN_ID_KEY = "token_id"
const val TOKEN_SYMBOL_KEY = "token_symbol"
}
}

View file

@ -0,0 +1,26 @@
package com.tangem.features.markets.deeplink.di
import com.tangem.features.markets.deeplink.DefaultMarketsDeepLinkHandler
import com.tangem.features.markets.deeplink.DefaultMarketsTokenDetailDeepLinkHandler
import com.tangem.features.markets.deeplink.MarketsDeepLinkHandler
import com.tangem.features.markets.deeplink.MarketsTokenDetailDeepLinkHandler
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface MarketsDeepLinkModule {
@Binds
@Singleton
fun bindMarketsDeepLinkHandlerFactory(impl: DefaultMarketsDeepLinkHandler.Factory): MarketsDeepLinkHandler.Factory
@Binds
@Singleton
fun bindMarketsTokenDetailDeepLinkHandlerFactory(
impl: DefaultMarketsTokenDetailDeepLinkHandler.Factory,
): MarketsTokenDetailDeepLinkHandler.Factory
}

View file

@ -13,7 +13,7 @@ import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.markets.details.MarketsTokenDetailsComponent.Params
import com.tangem.features.markets.details.impl.analytics.MarketDetailsAnalyticsEvent

View file

@ -1,117 +0,0 @@
package com.tangem.features.markets.details.impl.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.PreviewShimmerContainer
import com.tangem.features.markets.impl.R
import com.tangem.utils.StringsSigns
@Composable
internal fun Description(
description: TextReference,
hasFullDescription: Boolean,
onReadMoreClick: () -> Unit,
modifier: Modifier = Modifier,
) {
if (hasFullDescription) {
val text = buildAnnotatedString {
withStyle(SpanStyle(color = TangemTheme.colors.text.secondary)) {
append(description.resolveReference())
}
withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) {
append(
" " + stringResourceSafe(R.string.common_read_more).replace(
' ',
StringsSigns.NON_BREAKING_SPACE,
),
)
}
}
Text(
modifier = modifier
.clickable(
interactionSource = null,
indication = null,
onClick = onReadMoreClick,
),
text = text,
style = TangemTheme.typography.body2,
)
} else {
Text(
modifier = modifier,
text = description.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
}
}
@Composable
internal fun DescriptionPlaceholder(modifier: Modifier = Modifier) {
Column(
modifier = modifier,
) {
TextShimmer(
modifier = Modifier.fillMaxWidth(),
style = TangemTheme.typography.body2,
textSizeHeight = true,
)
TextShimmer(
modifier = Modifier.fillMaxWidth(),
style = TangemTheme.typography.body2,
textSizeHeight = true,
)
TextShimmer(
modifier = Modifier.fillMaxWidth(fraction = 0.8f),
style = TangemTheme.typography.body2,
textSizeHeight = true,
)
}
}
@Preview
@Composable
private fun ContentPreview() {
TangemThemePreview {
Description(
description = stringReference(
"XRP (XRP) is a cryptocurrency launched in January 2009, where the first " +
"genesis block was mined on 9th January 2009",
),
hasFullDescription = true,
onReadMoreClick = {},
)
}
}
@Preview
@Composable
private fun PreviewPlaceholder() {
TangemThemePreview {
PreviewShimmerContainer(
actualContent = {
ContentPreview()
},
shimmerContent = {
DescriptionPlaceholder()
},
)
}
}

View file

@ -10,6 +10,8 @@ import androidx.compose.ui.Modifier
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
import com.tangem.core.ui.components.UnableToLoadData
import com.tangem.core.ui.components.items.DescriptionItem
import com.tangem.core.ui.components.items.DescriptionPlaceholder
internal fun LazyListScope.tokenMarketDetailsBody(
state: MarketsTokenDetailsUM.Body,
@ -69,7 +71,7 @@ private fun LazyListScope.error(state: MarketsTokenDetailsUM.Body.Error) {
private fun LazyListScope.description(description: MarketsTokenDetailsUM.Description) {
item("description") {
Description(
DescriptionItem(
modifier = Modifier.blockPaddings(),
description = description.shortDescription,
hasFullDescription = description.fullDescription != null,

View file

@ -5,20 +5,15 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.stack.ChildStack
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.router.stack.pushNew
import com.arkivanov.decompose.router.stack.popWhile
import com.arkivanov.decompose.value.Value
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.navigation.Router
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarket
import com.tangem.domain.markets.toSerializableParam
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.core.decompose.navigation.inner.InnerRouter
import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.entry.MarketsEntryComponent
import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory.Child
@ -35,7 +30,12 @@ internal class DefaultMarketsEntryComponent @AssistedInject constructor(
private val stackNavigation = StackNavigation<Child>()
val stack: Value<ChildStack<Child, Any>> = childStack(
private val innerRouter = InnerRouter<Child>(
stackNavigation = stackNavigation,
popCallback = { onChildBack() },
)
private val stack: Value<ChildStack<Child, Any>> = childStack(
key = "main",
source = stackNavigation,
serializer = Child.serializer(),
@ -46,9 +46,8 @@ internal class DefaultMarketsEntryComponent @AssistedInject constructor(
child = configuration,
appComponentContext = childByContext(
componentContext = factoryContext,
router = createRouter(configuration),
router = innerRouter,
),
onTokenSelected = ::marketsListTokenSelected,
)
},
)
@ -68,32 +67,9 @@ internal class DefaultMarketsEntryComponent @AssistedInject constructor(
)
}
@OptIn(ExperimentalDecomposeApi::class)
private fun marketsListTokenSelected(token: TokenMarket, appCurrency: AppCurrency) {
stackNavigation.pushNew(
configuration = Child.TokenDetails(
params = MarketsTokenDetailsComponent.Params(
token = token.toSerializableParam(),
appCurrency = appCurrency,
showPortfolio = true,
analyticsParams = MarketsTokenDetailsComponent.AnalyticsParams(
blockchain = null,
source = "Market",
),
),
),
)
}
private fun AppComponentContext.createRouter(child: Child): Router {
return when (child) {
is Child.TokenDetails -> {
MarketTokenDetailsRouter(
contextRouter = this.router,
stackNavigation = stackNavigation,
)
}
else -> this.router
private fun onChildBack() {
if (stack.value.active.configuration !is Child.TokenList) {
stackNavigation.popWhile { it != Child.TokenList }
}
}

View file

@ -1,25 +0,0 @@
package com.tangem.features.markets.entry.impl
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.popWhile
import com.tangem.core.decompose.navigation.Route
import com.tangem.core.decompose.navigation.Router
import kotlin.reflect.KClass
internal class MarketTokenDetailsRouter(
private val contextRouter: Router,
private val stackNavigation: StackNavigation<MarketsEntryChildFactory.Child>,
) : Router by contextRouter {
override fun pop(onComplete: (isSuccess: Boolean) -> Unit) {
stackNavigation.popWhile({ it != MarketsEntryChildFactory.Child.TokenList }, onComplete)
}
override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) {
/** Not allowed */
}
override fun popTo(routeClass: KClass<out Route>, onComplete: (isSuccess: Boolean) -> Unit) {
/** Not allowed */
}
}

View file

@ -2,11 +2,9 @@ package com.tangem.features.markets.entry.impl
import androidx.compose.runtime.Immutable
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarket
import com.tangem.core.decompose.navigation.Route
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory.Child
import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent
import com.tangem.features.markets.tokenlist.MarketsTokenListComponent
import kotlinx.serialization.Serializable
import javax.inject.Inject
@ -17,7 +15,7 @@ internal class MarketsEntryChildFactory @Inject constructor(
@Serializable
@Immutable
sealed interface Child {
sealed interface Child : Route {
@Serializable
@Immutable
@ -28,11 +26,7 @@ internal class MarketsEntryChildFactory @Inject constructor(
data class TokenDetails(val params: MarketsTokenDetailsComponent.Params) : Child
}
fun createChild(
child: Child,
appComponentContext: AppComponentContext,
onTokenSelected: (TokenMarket, AppCurrency) -> Unit,
): Any {
fun createChild(child: Child, appComponentContext: AppComponentContext): Any {
return when (child) {
is Child.TokenDetails -> {
tokenDetailsComponentFactory.create(
@ -43,7 +37,7 @@ internal class MarketsEntryChildFactory @Inject constructor(
is Child.TokenList -> {
tokenListComponentFactory.create(
context = appComponentContext,
onTokenSelected = onTokenSelected,
params = Unit,
)
}
}

View file

@ -20,7 +20,7 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory
import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent
import com.tangem.features.markets.tokenlist.MarketsTokenListComponent
@Composable
internal fun EntryBottomSheetContent(

View file

@ -7,11 +7,11 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.markets.FilterAvailableNetworksForWalletUseCase
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.GetAllWalletsCryptoCurrencyStatusesUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId

View file

@ -11,6 +11,7 @@ import com.tangem.domain.models.ArtworkModel
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.isMultiCurrency
import com.tangem.features.markets.portfolio.impl.loader.PortfolioData
import com.tangem.features.markets.portfolio.impl.ui.state.AddToPortfolioBSContentUM
import com.tangem.features.markets.portfolio.impl.ui.state.WalletSelectorBSContentUM

View file

@ -21,9 +21,11 @@ import com.tangem.domain.managetokens.model.CurrencyUnsupportedState
import com.tangem.domain.markets.SaveMarketTokensUseCase
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.models.ArtworkModel
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.isMultiCurrency
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.features.markets.impl.R
@ -195,6 +197,7 @@ internal class MarketsPortfolioModel @Inject constructor(
modelScope.launch {
loadArtworksMutex.withLock {
wallets.forEach { wallet ->
wallet.requireColdWallet() // TODO [REDACTED_TASK_KEY]
if (!loadedArtworks.containsKey(wallet.walletId)) {
val artwork = getCardImageUseCase(
cardId = wallet.cardId,

View file

@ -6,6 +6,7 @@ import com.tangem.domain.models.ArtworkModel
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.isMultiCurrency
import com.tangem.features.markets.portfolio.impl.loader.PortfolioData
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState

View file

@ -14,10 +14,10 @@ import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.models.network.Network
import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.markets.impl.R
@ -59,7 +59,8 @@ internal class TokenActionsHandler @AssistedInject constructor(
cryptoCurrencyData = cryptoCurrencyData,
),
)
if (handleDemoMode(action, cryptoCurrencyData.userWallet)) return
val userWallet = cryptoCurrencyData.userWallet
if (userWallet is UserWallet.Cold && handleDemoMode(action, userWallet)) return
when (action) {
TokenActionsBSContentUM.Action.Buy -> onBuyClick(cryptoCurrencyData)
@ -72,7 +73,7 @@ internal class TokenActionsHandler @AssistedInject constructor(
}
}
private fun handleDemoMode(action: TokenActionsBSContentUM.Action, userWallet: UserWallet): Boolean {
private fun handleDemoMode(action: TokenActionsBSContentUM.Action, userWallet: UserWallet.Cold): Boolean {
val demoCard = isDemoCardUseCase.invoke(userWallet.cardId)
val needShowDemoWarning = demoCard && disabledActionsInDemoMode.contains(action)

View file

@ -4,6 +4,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.markets.impl.R
@ -25,7 +26,7 @@ internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider
val userWallet = UserWalletItemUM(
id = UserWalletId("1"),
name = stringReference("Wallet 1"),
information = stringReference("3 cards"),
information = UserWalletItemUM.Information.Loaded(TextReference.Str("3 cards")),
balance = UserWalletItemUM.Balance.Loading,
isEnabled = true,
endIcon = UserWalletItemUM.EndIcon.Arrow,

View file

@ -1,29 +0,0 @@
package com.tangem.features.markets.tokenlist.api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarket
import com.tangem.features.markets.entry.BottomSheetState
@Stable
interface MarketsTokenListComponent {
@Composable
fun BottomSheetContent(
bottomSheetState: State<BottomSheetState>,
onHeaderSizeChange: (Dp) -> Unit,
modifier: Modifier,
)
interface Factory {
fun create(
context: AppComponentContext,
onTokenSelected: (TokenMarket, AppCurrency) -> Unit,
): MarketsTokenListComponent
}
}

View file

@ -1,34 +1,58 @@
package com.tangem.features.markets.tokenlist.impl
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRoute.MarketsTokenDetails.AnalyticsParams
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarket
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.domain.markets.toSerializableParam
import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent
import com.tangem.features.markets.tokenlist.MarketsTokenListComponent
import com.tangem.features.markets.tokenlist.impl.model.MarketsListModel
import com.tangem.features.markets.tokenlist.impl.ui.MarketsList
import com.tangem.features.markets.tokenlist.impl.ui.MarketsListWithBack
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@Suppress("UnusedPrivateMember")
class DefaultMarketsTokenListComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val onTokenSelected: (TokenMarket, AppCurrency) -> Unit,
@Assisted params: Unit,
) : AppComponentContext by appComponentContext, MarketsTokenListComponent {
private val model: MarketsListModel = getOrCreateModel()
init {
model.tokenSelected
.onEach { onTokenSelected(it.first, it.second) }
.onEach { (token, appCurrency) ->
router.push(
AppRoute.MarketsTokenDetails(
token = token.toSerializableParam(),
appCurrency = appCurrency,
showPortfolio = true,
analyticsParams = AnalyticsParams(
blockchain = null,
source = "Market",
),
),
)
}
.launchIn(componentScope)
}
@ -60,11 +84,35 @@ class DefaultMarketsTokenListComponent @AssistedInject constructor(
)
}
@Composable
override fun Content(modifier: Modifier) {
LifecycleStartEffect(Unit) {
model.isVisibleOnScreen.value = true
onStopOrDispose {
model.isVisibleOnScreen.value = false
}
}
val state by model.state.collectAsStateWithLifecycle()
Scaffold(
contentWindowInsets = WindowInsetsZero,
containerColor = TangemTheme.colors.background.primary,
) {
MarketsListWithBack(
modifier = Modifier
.statusBarsPadding()
.imePadding()
.padding(it),
state = state,
bottomSheetState = BottomSheetState.EXPANDED,
onBackClick = router::pop,
)
}
}
@AssistedFactory
interface Factory : MarketsTokenListComponent.Factory {
override fun create(
context: AppComponentContext,
onTokenSelected: (TokenMarket, AppCurrency) -> Unit,
): DefaultMarketsTokenListComponent
override fun create(context: AppComponentContext, params: Unit): DefaultMarketsTokenListComponent
}
}

View file

@ -1,6 +1,6 @@
package com.tangem.features.markets.tokenlist.impl.di
import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent
import com.tangem.features.markets.tokenlist.MarketsTokenListComponent
import com.tangem.features.markets.tokenlist.impl.DefaultMarketsTokenListComponent
import dagger.Binds
import dagger.Module

View file

@ -11,12 +11,12 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
import com.tangem.domain.markets.GetStakingNotificationMaxApyUseCase
import com.tangem.domain.markets.TokenMarket
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.domain.settings.usercountry.models.UserCountryError
import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.tokenlist.impl.analytics.MarketsListAnalyticsEvent
import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListBatchFlowManager

View file

@ -2,7 +2,7 @@ package com.tangem.features.markets.tokenlist.impl.model.statemanager
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.*
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.markets.tokenlist.impl.model.converters.MarketsTokenItemConverter
import com.tangem.features.markets.tokenlist.impl.model.utils.logAction
import com.tangem.features.markets.tokenlist.impl.model.utils.logStatus

View file

@ -6,7 +6,7 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.event.triggeredEvent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.tokenlist.impl.ui.state.*
import com.tangem.utils.Provider

View file

@ -3,22 +3,29 @@ package com.tangem.features.markets.tokenlist.impl.ui
import android.content.res.Configuration
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.text.buildAnnotatedString
import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH8
@ -38,7 +45,7 @@ import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListLazyColumn
@ -62,26 +69,8 @@ internal fun MarketsList(
bottomSheetState: BottomSheetState,
modifier: Modifier = Modifier,
) {
Content(
modifier = modifier,
state = state,
onHeaderSizeChange = onHeaderSizeChange,
)
MarketsListSortByBottomSheet(config = state.sortByBottomSheet)
KeyboardEvents(
isSortByBottomSheetShown = state.sortByBottomSheet.isShown,
bottomSheetState = bottomSheetState,
)
}
@Suppress("LongMethod")
@Composable
private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier = Modifier) {
val density = LocalDensity.current
val background = LocalMainBottomSheetColor.current.value
val strokeColor = TangemTheme.colors.stroke.primary
val scrolledState = remember { mutableStateOf(false) }
Column(
modifier = modifier
.fillMaxSize()
@ -106,85 +95,148 @@ private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modi
.padding(bottom = 4.dp),
state = state.searchBar,
)
Column(Modifier.padding(horizontal = TangemTheme.dimens.size16)) {
AnimatedVisibility(
visible = scrolledState.value.not(),
) {
Column {
SpacerH8()
Title(isInSearchMode = state.isInSearchMode)
SpacerH12()
}
}
Content(state = state)
}
MarketsListSortByBottomSheet(config = state.sortByBottomSheet)
KeyboardEvents(
isSortByBottomSheetShown = state.sortByBottomSheet.isShown,
bottomSheetState = bottomSheetState,
)
}
@Composable
internal fun MarketsListWithBack(
state: MarketsListUM,
bottomSheetState: BottomSheetState,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val background = LocalMainBottomSheetColor.current.value
Column(
modifier = modifier
.fillMaxSize()
.imePadding()
.drawBehind { drawRect(background) },
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = rememberVectorPainter(
ImageVector.vectorResource(R.drawable.ic_close_24),
),
contentDescription = null,
tint = TangemTheme.colors.icon.primary1,
modifier = Modifier
.padding(16.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(bounded = false),
onClick = onBackClick,
),
)
SearchBar(
modifier = Modifier
.drawBehind { drawRect(background) }
.padding(
end = 16.dp,
),
state = state.searchBar,
)
}
Content(state = state)
}
MarketsListSortByBottomSheet(config = state.sortByBottomSheet)
KeyboardEvents(
isSortByBottomSheetShown = state.sortByBottomSheet.isShown,
bottomSheetState = bottomSheetState,
)
}
@Suppress("LongMethod")
@Composable
private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modifier) {
val strokeColor = TangemTheme.colors.stroke.primary
val scrolledState = remember { mutableStateOf(false) }
Column(modifier.padding(horizontal = TangemTheme.dimens.size16)) {
AnimatedVisibility(
visible = scrolledState.value.not(),
) {
Column {
AnimatedVisibility(state.isInSearchMode.not()) {
Options(
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
sortByTypeUM = state.selectedSortBy,
trendInterval = state.selectedInterval,
onIntervalClick = state.onIntervalClick,
onSortByClick = state.onSortByButtonClick,
)
}
AnimatedVisibility(
state.isInSearchMode.not() &&
state.stakingNotificationMaxApy != null &&
state.selectedSortBy != SortByTypeUM.Staking,
) {
val showMore = stringResourceSafe(R.string.common_show_more)
val description = stringResourceSafe(
R.string.markets_staking_banner_description_placeholder,
showMore,
)
val clickableDescription = buildAnnotatedString {
append(description.substringBefore(showMore))
pushStringAnnotation(SHOW_MORE_KEY, "")
appendColored(showMore, TangemTheme.colors.text.accent)
pop()
}
StakingInMarketsPromoNotification(
config = NotificationConfig(
iconResId = R.drawable.img_staking_in_market_notification,
title = resourceReference(
R.string.markets_staking_banner_title,
wrappedList(state.stakingNotificationMaxApy.format { percent() }),
),
subtitle = annotatedReference(clickableDescription),
onClick = state.onStakingNotificationClick,
onCloseClick = state.onStakingNotificationCloseClick,
),
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
)
}
SpacerH8()
Title(isInSearchMode = state.isInSearchMode)
SpacerH12()
}
}
Column {
AnimatedVisibility(state.isInSearchMode.not()) {
Options(
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
sortByTypeUM = state.selectedSortBy,
trendInterval = state.selectedInterval,
onIntervalClick = state.onIntervalClick,
onSortByClick = state.onSortByButtonClick,
)
}
AnimatedVisibility(
state.isInSearchMode.not() &&
state.stakingNotificationMaxApy != null &&
state.selectedSortBy != SortByTypeUM.Staking,
) {
val showMore = stringResourceSafe(R.string.common_show_more)
val description = stringResourceSafe(
R.string.markets_staking_banner_description_placeholder,
showMore,
)
val clickableDescription = buildAnnotatedString {
append(description.substringBefore(showMore))
pushStringAnnotation(SHOW_MORE_KEY, "")
appendColored(showMore, TangemTheme.colors.text.accent)
pop()
}
StakingInMarketsPromoNotification(
config = NotificationConfig(
iconResId = R.drawable.img_staking_in_market_notification,
title = resourceReference(
R.string.markets_staking_banner_title,
wrappedList(state.stakingNotificationMaxApy.format { percent() }),
),
subtitle = annotatedReference(clickableDescription),
onClick = state.onStakingNotificationClick,
onCloseClick = state.onStakingNotificationCloseClick,
),
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
)
}
}
val strokeWidth = TangemTheme.dimens.size0_5
Box(
Modifier
.fillMaxWidth()
.height(strokeWidth)
.drawBehind {
// draw horizontal line
if (scrolledState.value) {
drawLine(
color = strokeColor,
start = Offset(0f, size.height),
end = Offset(size.width, size.height),
strokeWidth = strokeWidth.toPx(),
)
}
},
)
ItemsList(
scrolledState = scrolledState,
isInSearchMode = state.isInSearchMode,
state = state.list,
)
}
val strokeWidth = TangemTheme.dimens.size0_5
Box(
Modifier
.fillMaxWidth()
.height(strokeWidth)
.drawBehind {
// draw horizontal line
if (scrolledState.value) {
drawLine(
color = strokeColor,
start = Offset(0f, size.height),
end = Offset(size.width, size.height),
strokeWidth = strokeWidth.toPx(),
)
}
},
)
ItemsList(
scrolledState = scrolledState,
isInSearchMode = state.isInSearchMode,
state = state.list,
)
}
@Composable

View file

@ -18,7 +18,7 @@ import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM
import kotlinx.coroutines.launch

View file

@ -1,11 +1,12 @@
@file:Suppress("MagicNumber")
package com.tangem.features.markets.tokenlist.impl.ui.preview
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.common.ui.charts.state.MarketChartRawData
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
import kotlinx.collections.immutable.persistentListOf

View file

@ -5,7 +5,7 @@ import com.tangem.common.ui.charts.state.MarketChartLook
import com.tangem.common.ui.charts.state.MarketChartRawData
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
@Immutable
data class MarketsListItemUM(

View file

@ -6,7 +6,7 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.markets.impl.R
import kotlinx.collections.immutable.ImmutableList
import java.math.BigDecimal

View file

@ -4,4 +4,5 @@ interface NFTFeatureToggles {
val isNFTEnabled: Boolean
val isNFTEVMEnabled: Boolean
val isNFTSolanaEnabled: Boolean
val isNFTMediaContentEnabled: Boolean
}

View file

@ -0,0 +1,8 @@
package com.tangem.features.nft.entity
/**
* Trigger on success nft send
*/
interface NFTSendSuccessTrigger {
suspend fun triggerSuccessNFTSend()
}

View file

@ -28,6 +28,7 @@ dependencies {
/** Domain modules */
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.appCurrency)
implementation(projects.domain.models)
implementation(projects.domain.nft)
implementation(projects.domain.nft.models)

View file

@ -13,4 +13,7 @@ internal class DefaultNFTFeatureToggles(
override val isNFTSolanaEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "NFT_SOLANA_ENABLED")
override val isNFTMediaContentEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "NFT_MEDIA_CONTENT_ENABLED")
}

View file

@ -5,7 +5,7 @@ import com.tangem.core.ui.extensions.TextReference
internal data class NFTCollectionUM(
val id: String,
val name: String?,
val name: String,
@DrawableRes val networkIconId: Int,
val logoUrl: String?,
val description: TextReference,

View file

@ -1,10 +1,11 @@
package com.tangem.features.nft.collections.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
@Immutable
internal sealed class NFTSalePriceUM {
data object Loading : NFTSalePriceUM()
data object Failed : NFTSalePriceUM()
data class Content(val price: String) : NFTSalePriceUM()
data class Content(val price: TextReference) : NFTSalePriceUM()
}

View file

@ -2,13 +2,13 @@ package com.tangem.features.nft.collections.entity.transformer
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.getActiveIconRes
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.nft.models.*
import com.tangem.features.nft.collections.entity.*
import com.tangem.features.nft.impl.R
import com.tangem.utils.StringsSigns.DASH_SIGN
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
@ -16,7 +16,6 @@ import kotlinx.collections.immutable.toPersistentList
@Suppress("LongParameterList")
internal class UpdateDataStateTransformer(
private val nftCollections: List<NFTCollections>,
private val searchQuery: String,
private val onReceiveClick: () -> Unit,
private val onRetryClick: () -> Unit,
private val onExpandCollectionClick: (NFTCollection) -> Unit,
@ -27,42 +26,17 @@ internal class UpdateDataStateTransformer(
) : Transformer<NFTCollectionsStateUM> {
override fun transform(prevState: NFTCollectionsStateUM): NFTCollectionsStateUM {
val hasQuery = !(prevState.content as? NFTCollectionsUM.Content)?.search?.query.isNullOrEmpty()
val content = when {
nftCollections.allCollectionsFailed() ->
!hasQuery && nftCollections.allCollectionsFailed() ->
NFTCollectionsUM.Failed(onRetryClick, onReceiveClick)
nftCollections.anyCollectionFailed() && nftCollections.allLoadedCollectionsEmpty() ->
!hasQuery && nftCollections.anyCollectionFailed() && nftCollections.allLoadedCollectionsEmpty() ->
NFTCollectionsUM.Failed(onRetryClick, onReceiveClick)
nftCollections.allCollectionsLoaded() && nftCollections.allCollectionsEmpty() ->
!hasQuery && nftCollections.allCollectionsLoaded() && nftCollections.allCollectionsEmpty() ->
NFTCollectionsUM.Empty(onReceiveClick)
!nftCollections.allCollectionsLoaded() && nftCollections.allCollectionsEmpty() ->
NFTCollectionsUM.Loading(
onReceiveClick = onReceiveClick,
search = SearchBarUM(
placeholderText = resourceReference(R.string.common_search),
query = "",
isActive = false,
onQueryChange = { },
onActiveChange = { },
),
)
else -> {
NFTCollectionsUM.Content(
search = if (prevState.content is NFTCollectionsUM.Content) {
prevState.content.search
} else {
initialSearchBarFactory()
},
collections = nftCollections
.map { it.content }
.asSequence()
.filterIsInstance<NFTCollections.Content.Collections>()
.map { it.collections.orEmpty().transform(prevState, searchQuery) }
.flatten()
.toPersistentList(),
warnings = transformNotifications(),
onReceiveClick = onReceiveClick,
)
}
!nftCollections.allCollectionsLoaded() && nftCollections.allCollectionsEmpty() -> prevState.createLoading()
else -> prevState.createContent()
}
return prevState.copy(
content = content,
@ -75,48 +49,51 @@ internal class UpdateDataStateTransformer(
)
}
private fun List<NFTCollection>.transform(
state: NFTCollectionsStateUM,
query: String,
): ImmutableList<NFTCollectionUM> = mapNotNull {
val assetsFulfillQuery = if (query.isEmpty()) {
true
} else {
when (val assets = it.assets) {
is NFTCollection.Assets.Empty,
is NFTCollection.Assets.Failed,
is NFTCollection.Assets.Loading,
-> false
is NFTCollection.Assets.Value -> {
assets.items.any { asset ->
asset.name?.lowercase()?.contains(query.lowercase()) == true
}
}
}
}
private fun NFTCollectionsStateUM.createLoading(): NFTCollectionsUM.Loading = NFTCollectionsUM.Loading(
onReceiveClick = onReceiveClick,
search = SearchBarUM(
placeholderText = resourceReference(R.string.common_search),
query = "",
isActive = false,
onQueryChange = { },
onActiveChange = { },
),
)
val collectionFulfillQuery = query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true
if (collectionFulfillQuery || assetsFulfillQuery) {
NFTCollectionUM(
id = it.collectionIdProvider(),
networkIconId = getActiveIconRes(it.network.id.value),
name = it.name,
description = TextReference.PluralRes(
R.plurals.nft_collections_count,
it.count,
wrappedList(it.count),
),
logoUrl = it.logoUrl,
assets = it.transformAssets(),
onExpandClick = {
onExpandCollectionClick(it)
},
isExpanded = it.isExpanded(state),
)
private fun NFTCollectionsStateUM.createContent(): NFTCollectionsUM.Content = NFTCollectionsUM.Content(
search = if (content is NFTCollectionsUM.Content) {
content.search
} else {
null
}
initialSearchBarFactory()
},
collections = nftCollections
.map { it.content }
.asSequence()
.filterIsInstance<NFTCollections.Content.Collections>()
.map { it.collections.orEmpty().transform(this) }
.flatten()
.toPersistentList(),
warnings = transformNotifications(),
onReceiveClick = onReceiveClick,
)
private fun List<NFTCollection>.transform(state: NFTCollectionsStateUM): ImmutableList<NFTCollectionUM> = map {
NFTCollectionUM(
id = it.collectionIdProvider(),
networkIconId = getActiveIconRes(it.network.rawId),
name = it.name.orEmpty(),
description = TextReference.PluralRes(
R.plurals.nft_collections_count,
it.count,
wrappedList(it.count),
),
logoUrl = it.logoUrl,
assets = it.transformAssets(),
onExpandClick = {
onExpandCollectionClick(it)
},
isExpanded = it.isExpanded(state),
)
}.toPersistentList()
private fun transformNotifications(): ImmutableList<NFTCollectionsWarningUM> = buildList {
@ -146,20 +123,31 @@ internal class UpdateDataStateTransformer(
)
}
private fun NFTAsset.transform(collectionName: String): NFTCollectionAssetUM = NFTCollectionAssetUM(
id = id.toString(),
name = name.orEmpty(),
imageUrl = media?.url,
price = when (val salePrice = salePrice) {
is NFTSalePrice.Empty -> NFTSalePriceUM.Failed
is NFTSalePrice.Loading -> NFTSalePriceUM.Loading
is NFTSalePrice.Error -> NFTSalePriceUM.Failed
is NFTSalePrice.Value -> NFTSalePriceUM.Content(salePrice.value.toString())
},
onItemClick = {
onAssetClick(this, collectionName)
},
)
private fun NFTAsset.transform(collectionName: String): NFTCollectionAssetUM {
return NFTCollectionAssetUM(
id = id.toString(),
name = name ?: DASH_SIGN,
imageUrl = media?.imageUrl,
price = when (val salePrice = salePrice) {
is NFTSalePrice.Empty -> NFTSalePriceUM.Failed
is NFTSalePrice.Loading -> NFTSalePriceUM.Loading
is NFTSalePrice.Error -> NFTSalePriceUM.Failed
is NFTSalePrice.Value -> NFTSalePriceUM.Content(
price = stringReference(
salePrice.value.format {
crypto(
symbol = salePrice.symbol,
decimals = salePrice.decimals,
)
},
),
)
},
onItemClick = {
onAssetClick(this, collectionName)
},
)
}
private fun NFTCollection.isExpanded(state: NFTCollectionsStateUM): Boolean =
(state.content as? NFTCollectionsUM.Content)

View file

@ -11,14 +11,11 @@ import com.tangem.domain.nft.FetchNFTCollectionAssetsUseCase
import com.tangem.domain.nft.GetNFTCollectionsUseCase
import com.tangem.domain.nft.RefreshAllNFTUseCase
import com.tangem.domain.nft.models.NFTCollection
import com.tangem.domain.nft.models.NFTCollections
import com.tangem.features.nft.collections.NFTCollectionsComponent
import com.tangem.features.nft.collections.entity.NFTCollectionsStateUM
import com.tangem.features.nft.collections.entity.NFTCollectionsUM
import com.tangem.features.nft.collections.entity.transformer.*
import com.tangem.features.nft.collections.entity.transformer.ChangeCollectionExpandedStateTransformer
import com.tangem.features.nft.collections.entity.transformer.ToggleSearchBarTransformer
import com.tangem.features.nft.collections.entity.transformer.UpdateDataStateTransformer
import com.tangem.features.nft.collections.entity.transformer.UpdateSearchQueryTransformer
import com.tangem.features.nft.impl.R
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
@ -75,8 +72,7 @@ internal class NFTCollectionsModel @Inject constructor(
) { nftCollections, query ->
_state.update {
UpdateDataStateTransformer(
nftCollections = nftCollections,
searchQuery = query,
nftCollections = nftCollections.filter(query),
onReceiveClick = {
params.onReceiveClick()
},
@ -95,6 +91,38 @@ internal class NFTCollectionsModel @Inject constructor(
.launchIn(modelScope)
}
private fun List<NFTCollections>.filter(query: String): List<NFTCollections> = map {
it.copy(
content = when (val content = it.content) {
is NFTCollections.Content.Collections -> content.copy(
collections = content.collections.orEmpty().filter {
val assetsFulfillQuery = if (query.isEmpty()) {
true
} else {
when (val assets = it.assets) {
is NFTCollection.Assets.Empty,
is NFTCollection.Assets.Failed,
is NFTCollection.Assets.Loading,
-> false
is NFTCollection.Assets.Value -> {
assets.items.any { asset ->
asset.name?.lowercase()?.contains(query.lowercase()) == true
}
}
}
}
val collectionFulfillQuery =
query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true
collectionFulfillQuery || assetsFulfillQuery
},
)
is NFTCollections.Content.Error -> it.content
},
)
}
private fun onRefresh() {
modelScope.launch {
_state.update { ChangeRefreshingStateTransformer(true).transform(it) }

View file

@ -18,7 +18,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.nft.collections.entity.NFTCollectionAssetsListUM
@ -90,7 +89,7 @@ private fun RowScope.Text(state: NFTCollectionUM) {
) {
Text(
modifier = Modifier,
text = state.name.takeUnless { it.isNullOrEmpty() } ?: stringResourceSafe(R.string.nft_no_collection),
text = state.name,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
maxLines = 1,

View file

@ -24,6 +24,7 @@ import coil.request.ImageRequest
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH2
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.nft.collections.entity.NFTCollectionAssetUM
@ -77,7 +78,7 @@ private fun Placeholder(modifier: Modifier = Modifier) {
) {
Image(
modifier = Modifier.size(TangemTheme.dimens.size52),
painter = painterResource(R.drawable.ic_nft_placeholder_76),
painter = painterResource(R.drawable.ic_nft_placeholder_120),
contentDescription = null,
)
}
@ -116,7 +117,9 @@ private class NFTCollectionAssetProvider : CollectionPreviewParameterProvider<NF
id = "item3",
name = "Nethers #0855",
imageUrl = "img",
price = NFTSalePriceUM.Content("0.05 ETH"),
price = NFTSalePriceUM.Content(
price = stringReference("0.05 ETH"),
),
onItemClick = { },
),
),

View file

@ -25,6 +25,7 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -338,7 +339,9 @@ private fun Preview_NFTCollectionsContent() {
id = "item1",
name = "Nethers #0854",
imageUrl = "img",
price = NFTSalePriceUM.Content("0.05 ETH"),
price = NFTSalePriceUM.Content(
price = stringReference("0.05 ETH"),
),
onItemClick = { },
),
NFTCollectionAssetUM(

View file

@ -10,7 +10,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.PrimaryButton
@ -35,19 +34,19 @@ internal fun NFTCollectionsEmpty(state: NFTCollectionsUM.Empty, modifier: Modifi
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
painter = painterResource(R.drawable.ic_nft_placeholder_76),
painter = painterResource(R.drawable.ic_nft_placeholder_add_76),
contentDescription = null,
)
Text(
modifier = Modifier.padding(top = TangemTheme.dimens.spacing24),
text = stringResource(R.string.nft_collections_empty_title),
text = stringResourceSafe(R.string.nft_collections_empty_title),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
Text(
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
text = stringResource(R.string.nft_collections_empty_description),
text = stringResourceSafe(R.string.nft_collections_empty_description),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
@ -55,7 +54,7 @@ internal fun NFTCollectionsEmpty(state: NFTCollectionsUM.Empty, modifier: Modifi
PrimaryButton(
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing48)
.wrapContentWidth(),
.widthIn(min = TangemTheme.dimens.size158),
text = stringResourceSafe(R.string.nft_collections_receive),
onClick = state.onReceiveClick,
)

View file

@ -6,6 +6,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.nft.collections.entity.NFTSalePriceUM
@ -15,7 +16,7 @@ internal fun NFTSalePrice(state: NFTSalePriceUM, modifier: Modifier = Modifier)
is NFTSalePriceUM.Content -> {
Text(
modifier = modifier,
text = state.price,
text = state.price.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,

View file

@ -19,18 +19,22 @@ import com.tangem.features.nft.common.ui.NFTContent
import com.tangem.features.nft.component.NFTComponent
import com.tangem.features.nft.details.NFTDetailsComponent
import com.tangem.features.nft.details.info.NFTDetailsInfoComponent
import com.tangem.features.nft.entity.NFTSendSuccessListener
import com.tangem.features.nft.receive.NFTReceiveComponent
import com.tangem.features.nft.traits.NFTAssetTraitsComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
internal class DefaultNFTComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: NFTComponent.Params,
private val nftDetailsInfoComponentFactory: NFTDetailsInfoComponent.Factory,
nftSendSuccessListener: NFTSendSuccessListener,
) : NFTComponent, AppComponentContext by appComponentContext {
private val stackNavigation = StackNavigation<NFTRoute>()
@ -69,6 +73,11 @@ internal class DefaultNFTComponent @AssistedInject constructor(
currentRoute.emit(stack.active.configuration)
}
}
nftSendSuccessListener.nftSendSuccessFlow
.onEach {
innerRouter.popTo(NFTRoute.Collections(userWalletId = params.userWalletId))
}
.launchIn(componentScope)
}
@Composable

View file

@ -21,8 +21,8 @@ class DefaultNFTDetailsBlockComponent @AssistedInject constructor(
NFTDetailsBlock(
assetName = stringReference(params.nftAsset.name.orEmpty()),
collectionName = stringReference(params.nftCollectionName),
assetImage = params.nftAsset.media?.url,
networkIconRes = getActiveIconRes(params.nftAsset.network.id.value),
assetImage = params.nftAsset.media?.imageUrl,
networkIconRes = getActiveIconRes(params.nftAsset.network.rawId),
)
}

View file

@ -3,9 +3,7 @@ package com.tangem.features.nft.details.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency
import kotlinx.collections.immutable.ImmutableList
import java.math.BigDecimal
data class NFTAssetUM(
val name: String,
@ -31,7 +29,6 @@ data class NFTAssetUM(
sealed class Media {
data object Empty : Media()
data class Content(
val mimetype: String?,
val url: String,
) : Media()
}
@ -41,11 +38,9 @@ data class NFTAssetUM(
data object Loading : SalePrice()
data object Empty : SalePrice()
data class Content(
val value: BigDecimal,
val symbol: String,
val decimals: Int,
val rate: BigDecimal?,
val appCurrency: AppCurrency,
val isFlickering: Boolean,
val cryptoPrice: TextReference,
val fiatPrice: TextReference,
) : SalePrice()
}

View file

@ -1,7 +1,10 @@
package com.tangem.features.nft.details.entity
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
internal data class NFTDetailsUM(
val nftAsset: NFTAssetUM,
val pullToRefreshConfig: PullToRefreshConfig,
val onBackClick: () -> Unit,
val onReadMoreClick: () -> Unit,
val onSeeAllTraitsClick: () -> Unit,

View file

@ -1,9 +1,14 @@
package com.tangem.features.nft.details.entity.factory
import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
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.domain.appcurrency.model.AppCurrency
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.nft.models.NFTSalePrice
import com.tangem.features.nft.details.entity.NFTAssetUM
@ -12,17 +17,24 @@ import com.tangem.features.nft.impl.R
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@Suppress("LongParameterList")
internal class NFTDetailsUMFactory(
private val appCurrency: AppCurrency,
private val onBackClick: () -> Unit,
private val onReadMoreClick: () -> Unit,
private val onSeeAllTraitsClick: () -> Unit,
private val onExploreClick: () -> Unit,
private val onSendClick: () -> Unit,
private val onRefresh: () -> Unit,
private val onInfoBlockClick: (title: TextReference, text: TextReference) -> Unit,
) {
fun getInitialState(nftAsset: NFTAsset): NFTDetailsUM = NFTDetailsUM(
nftAsset = nftAsset.transform(),
pullToRefreshConfig = PullToRefreshConfig(
isRefreshing = false,
onRefresh = { onRefresh() },
),
onBackClick = onBackClick,
onReadMoreClick = onReadMoreClick,
onSeeAllTraitsClick = onSeeAllTraitsClick,
@ -32,10 +44,9 @@ internal class NFTDetailsUMFactory(
private fun NFTAsset.transform(): NFTAssetUM = NFTAssetUM(
name = name.orEmpty(),
media = media?.let {
media = media?.imageUrl?.let {
NFTAssetUM.Media.Content(
mimetype = it.mimetype,
url = it.url,
url = it,
)
} ?: NFTAssetUM.Media.Empty,
topInfo = when {
@ -52,7 +63,7 @@ internal class NFTDetailsUMFactory(
} else {
null
},
salePrice = NFTAssetUM.SalePrice.Empty,
salePrice = toSalePrice(),
description = description,
rarity = if (rarity != null) {
NFTAssetUM.Rarity.Content(
@ -91,6 +102,32 @@ internal class NFTDetailsUMFactory(
private fun NFTAsset.hasSalePrice() = salePrice !is NFTSalePrice.Empty && salePrice !is NFTSalePrice.Error
private fun NFTAsset.toSalePrice() = when (val price = salePrice) {
is NFTSalePrice.Empty,
is NFTSalePrice.Error,
-> NFTAssetUM.SalePrice.Empty
is NFTSalePrice.Loading -> NFTAssetUM.SalePrice.Loading
is NFTSalePrice.Value -> NFTAssetUM.SalePrice.Content(
isFlickering = false,
cryptoPrice = stringReference(
price.value.format {
crypto(
symbol = price.symbol,
decimals = price.decimals,
)
},
),
fiatPrice = stringReference(
price.fiatValue.format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
},
),
)
}
@Suppress("LongMethod")
private fun NFTAsset.buildBaseInfoItems() = when (val id = id) {
is NFTAsset.Identifier.EVM -> persistentListOf(
@ -120,6 +157,7 @@ internal class NFTDetailsUMFactory(
NFTAssetUM.BlockItem(
title = resourceReference(R.string.nft_details_token_id),
value = id.tokenId.toString(),
valueTextEllipsis = TextEllipsis.Middle,
showInfoButton = true,
onClick = {
onInfoBlockClick(

View file

@ -0,0 +1,53 @@
package com.tangem.features.nft.details.entity.transformer
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.domain.appcurrency.model.AppCurrency
import com.tangem.domain.nft.models.NFTSalePrice
import com.tangem.features.nft.details.entity.NFTAssetUM
import com.tangem.features.nft.details.entity.NFTDetailsUM
import com.tangem.utils.transformer.Transformer
internal class NFTPriceChangeTransformer(
private val appCurrency: AppCurrency,
private val nftSalePrice: NFTSalePrice,
) : Transformer<NFTDetailsUM> {
override fun transform(prevState: NFTDetailsUM): NFTDetailsUM {
val topInfo = prevState.nftAsset.topInfo as? NFTAssetUM.TopInfo.Content ?: return prevState
return prevState.copy(
nftAsset = prevState.nftAsset.copy(
topInfo = topInfo.copy(
salePrice = when (nftSalePrice) {
is NFTSalePrice.Empty,
is NFTSalePrice.Error,
-> NFTAssetUM.SalePrice.Empty
is NFTSalePrice.Loading -> NFTAssetUM.SalePrice.Loading
is NFTSalePrice.Value -> NFTAssetUM.SalePrice.Content(
isFlickering = false,
cryptoPrice = stringReference(
nftSalePrice.value.format {
crypto(
symbol = nftSalePrice.symbol,
decimals = nftSalePrice.decimals,
)
},
),
fiatPrice = stringReference(
nftSalePrice.fiatValue.format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
},
),
)
},
),
),
)
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.features.nft.details.entity.transformer
import com.tangem.features.nft.details.entity.NFTAssetUM
import com.tangem.features.nft.details.entity.NFTDetailsUM
import com.tangem.utils.transformer.Transformer
internal object NFTPriceUpdatingTransformer : Transformer<NFTDetailsUM> {
override fun transform(prevState: NFTDetailsUM): NFTDetailsUM {
val topInfo = prevState.nftAsset.topInfo as? NFTAssetUM.TopInfo.Content ?: return prevState
val salePrice = topInfo.salePrice as? NFTAssetUM.SalePrice.Content
return prevState.copy(
nftAsset = prevState.nftAsset.copy(
topInfo = topInfo.copy(
salePrice = salePrice?.copy(
isFlickering = true,
) ?: topInfo.salePrice,
),
),
)
}
}

View file

@ -39,7 +39,7 @@ internal fun NFTInfoBottomSheetContent(text: TextReference, modifier: Modifier =
) {
Text(
text = text.resolveReference(),
color = TangemTheme.colors.text.secondary,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.body2,
modifier = Modifier.padding(
start = TangemTheme.dimens.spacing16,
@ -52,7 +52,7 @@ internal fun NFTInfoBottomSheetContent(text: TextReference, modifier: Modifier =
@Composable
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(showBackground = false, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_NFTInfoBottomSheetContent() {
TangemThemePreview {
NFTInfoBottomSheetContent(

View file

@ -1,5 +1,6 @@
package com.tangem.features.nft.details.model
import arrow.core.getOrElse
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.tangem.common.routing.AppRoute
@ -12,20 +13,31 @@ import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.nft.FetchNFTCollectionAssetsUseCase
import com.tangem.domain.nft.FetchNFTPriceUseCase
import com.tangem.domain.nft.GetNFTExploreUrlUseCase
import com.tangem.domain.nft.GetNFTPriceUseCase
import com.tangem.domain.nft.analytics.NFTAnalyticsEvent
import com.tangem.features.nft.details.NFTDetailsComponent
import com.tangem.features.nft.details.entity.NFTAssetUM
import com.tangem.features.nft.details.entity.NFTDetailsBottomSheetConfig
import com.tangem.features.nft.details.entity.NFTDetailsUM
import com.tangem.features.nft.details.entity.factory.NFTDetailsUMFactory
import com.tangem.features.nft.details.entity.transformer.NFTPriceChangeTransformer
import com.tangem.features.nft.details.entity.transformer.NFTPriceUpdatingTransformer
import com.tangem.features.nft.impl.R
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import com.tangem.utils.transformer.update
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped
internal class NFTDetailsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
@ -33,6 +45,10 @@ internal class NFTDetailsModel @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
private val urlOpener: UrlOpener,
private val getNFTExploreUrlUseCase: GetNFTExploreUrlUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getNFTPriceUseCase: GetNFTPriceUseCase,
private val fetchNFTCollectionAssetsUseCase: FetchNFTCollectionAssetsUseCase,
private val fetchNFTPriceUseCase: FetchNFTPriceUseCase,
paramsContainer: ParamsContainer,
) : Model() {
@ -40,23 +56,91 @@ internal class NFTDetailsModel @Inject constructor(
val state: StateFlow<NFTDetailsUM> get() = _state
private var appCurrency: AppCurrency = AppCurrency.Default
private val stateFactory: NFTDetailsUMFactory = NFTDetailsUMFactory(
appCurrency = appCurrency,
onBackClick = { params.onBackClick() },
onReadMoreClick = ::onReadMoreClick,
onSeeAllTraitsClick = { params.onAllTraitsClick() },
onSeeAllTraitsClick = {
analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonSeeAll)
params.onAllTraitsClick()
},
onExploreClick = ::onExploreClick,
onSendClick = ::onSendClick,
onInfoBlockClick = ::onInfoBlockClick,
onRefresh = ::onRefresh,
)
private val _state = MutableStateFlow(
value = stateFactory.getInitialState(params.nftAsset),
)
private val _state by lazy {
MutableStateFlow(
value = stateFactory.getInitialState(params.nftAsset),
)
}
val bottomSheetNavigation: SlotNavigation<NFTDetailsBottomSheetConfig> = SlotNavigation()
init {
analyticsEventHandler.send(NFTAnalyticsEvent.Details.ScreenOpened(params.nftAsset.network.name))
initAppCurrency()
subscribeToPriceChanges()
}
private fun initAppCurrency() {
modelScope.launch {
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
}
}
private fun subscribeToPriceChanges() {
_state.update(NFTPriceUpdatingTransformer)
modelScope.launch {
getNFTPriceUseCase(params.userWalletId, params.nftAsset)
.fold(
ifLeft = {
Timber.w(it)
},
ifRight = { quoteFlow ->
quoteFlow
.distinctUntilChanged()
.onEach { salePrice ->
_state.update(
NFTPriceChangeTransformer(
appCurrency = appCurrency,
nftSalePrice = salePrice,
),
)
}.launchIn(modelScope)
},
)
}
}
private fun onRefresh() {
_state.update {
it.copy(pullToRefreshConfig = it.pullToRefreshConfig.copy(isRefreshing = true))
}
_state.update(NFTPriceUpdatingTransformer)
modelScope.launch {
awaitAll(
async {
fetchNFTCollectionAssetsUseCase(
userWalletId = params.userWalletId,
network = params.nftAsset.network,
collectionId = params.nftAsset.collectionId,
)
},
async {
fetchNFTPriceUseCase(
network = params.nftAsset.network,
appCurrencyId = null,
)
},
)
_state.update {
it.copy(pullToRefreshConfig = it.pullToRefreshConfig.copy(isRefreshing = false))
}
}
}
private fun onInfoBlockClick(title: TextReference, text: TextReference) {

View file

@ -1,6 +1,7 @@
package com.tangem.features.nft.details.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
@ -11,6 +12,7 @@ import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.nft.details.entity.NFTDetailsUM
@ -34,14 +36,19 @@ internal fun NFTDetails(state: NFTDetailsUM, modifier: Modifier = Modifier) {
)
},
content = { innerPadding ->
NFTDetailsAsset(
state = state.nftAsset,
onReadMoreClick = state.onReadMoreClick,
onSeeAllTraitsClick = state.onSeeAllTraitsClick,
onExploreClick = state.onExploreClick,
TangemPullToRefreshContainer(
config = state.pullToRefreshConfig,
modifier = Modifier
.padding(innerPadding),
)
.padding(innerPadding)
.fillMaxSize(),
) {
NFTDetailsAsset(
state = state.nftAsset,
onReadMoreClick = state.onReadMoreClick,
onSeeAllTraitsClick = state.onSeeAllTraitsClick,
onExploreClick = state.onExploreClick,
)
}
},
floatingActionButtonPosition = FabPosition.Center,
floatingActionButton = {

Some files were not shown because too many files have changed in this diff Show more