Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-02 10:24:41 +03:00
commit af09ce9ca4
877 changed files with 16091 additions and 6613 deletions

View file

@ -66,7 +66,6 @@ dependencies {
implementation(deps.arrow.core)
implementation(deps.kotlin.immutable.collections)
implementation(deps.kotlin.serialization)
implementation(deps.timber)
implementation(deps.firebase.crashlytics)
/** DI */

View file

@ -25,10 +25,10 @@ import com.tangem.features.account.createedit.error.AccountFeatureError
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@ -147,7 +147,7 @@ internal class ArchivedAccountListModel @Inject constructor(
private fun logError(error: AccountFeatureError, params: Map<String, String> = emptyMap()) {
val exception = IllegalStateException(error.toString())
Timber.e(exception)
TangemLogger.e("Error", exception)
analyticsExceptionHandler.sendException(
event = ExceptionAnalyticsEvent(exception = exception, params = params),

View file

@ -8,8 +8,8 @@ import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.account.models.ArchivedAccount
import com.tangem.domain.account.usecase.ArchivedAccountList
import com.tangem.domain.models.account.AccountId
import com.tangem.utils.logging.TangemLogger
import kotlinx.collections.immutable.toImmutableList
import timber.log.Timber
import javax.inject.Inject
internal class AccountArchivedUMBuilder @Inject constructor() {
@ -49,7 +49,7 @@ internal class AccountArchivedUMBuilder @Inject constructor() {
onCloseClick: () -> Unit,
getArchivedAccounts: () -> Unit,
): AccountArchivedUM.Error {
Timber.e(throwable)
TangemLogger.e("Error", throwable)
return AccountArchivedUM.Error(
onCloseClick = onCloseClick,
onRetryClick = { getArchivedAccounts() },

View file

@ -42,11 +42,11 @@ import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.
import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateName
import com.tangem.features.account.createedit.error.AccountFeatureError
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ModelScoped
@ -242,7 +242,7 @@ internal class AccountCreateEditModel @Inject constructor(
private fun onNameChange(name: AccountNameUM) {
val isNotEmptyCustomName = (name as? AccountNameUM.Custom)?.raw?.isNotEmpty() == true
if (!name.isValidName() && isNotEmptyCustomName) {
Timber.d("Invalid account name: $name")
TangemLogger.d("Invalid account name: $name")
return
}
@ -310,7 +310,7 @@ internal class AccountCreateEditModel @Inject constructor(
private fun logError(error: AccountFeatureError, params: Map<String, String> = emptyMap()) {
val exception = IllegalStateException(error.toString())
Timber.e(exception)
TangemLogger.e("Error", exception)
analyticsExceptionHandler.sendException(
event = ExceptionAnalyticsEvent(exception = exception, params = params),

View file

@ -13,6 +13,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
@ -31,6 +32,7 @@ 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.test.AccountDetailsScreenTestTags
import com.tangem.features.account.details.entity.AccountDetailsUM
@Composable
@ -130,7 +132,8 @@ private fun ManageTokensRow(state: AccountDetailsUM) {
.clip(RoundedCornerShape(TangemTheme.dimens.radius12))
.background(TangemTheme.colors.background.primary)
.clickable(onClick = state.onManageTokensClick)
.padding(all = TangemTheme.dimens.spacing12),
.padding(all = TangemTheme.dimens.spacing12)
.testTag(AccountDetailsScreenTestTags.MANAGE_TOKENS_BUTTON),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {

View file

@ -12,6 +12,7 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.filterCryptoPortfolio
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.account.PortfolioFetcher
@ -179,13 +180,10 @@ internal class PortfolioSelectorModel @Inject constructor(
}
}
portfolio.accountsBalance.accountStatuses.forEach { accountStatus ->
portfolio.accountsBalance.accountStatuses.filterCryptoPortfolio().forEach { accountStatus ->
val isEnabledByFeature = isEnabled(wallet, accountStatus)
val account = accountStatus.account
val accountBalance = when (accountStatus) {
is AccountStatus.CryptoPortfolio -> accountStatus.tokenList.totalFiatBalance
is AccountStatus.Payment -> accountStatus.totalFiatBalance
}
val accountBalance = accountStatus.tokenList.totalFiatBalance
val accountItemUM = AccountPortfolioItemUMConverter(
onClick = { selectorController.selectAccount(account.accountId) },
appCurrency = appCurrency,

View file

@ -15,6 +15,7 @@ interface GiveApprovalComponent : ComposableBottomSheetComponent {
val amount: String,
val spenderAddress: String,
val subtitle: TextReference,
val isHoldToConfirm: Boolean = false,
val callback: Callback,
)

View file

@ -50,7 +50,6 @@ dependencies {
/** Other */
implementation(deps.decompose)
implementation(deps.decompose.ext.compose)
implementation(deps.timber)
implementation(deps.kotlin.immutable.collections)
implementation(deps.arrow.core)

View file

@ -86,6 +86,7 @@ internal class DefaultGiveApprovalComponent @AssistedInject constructor(
walletInteractionIcon = uiState.walletInteractionIcon,
isApproveEnabled = uiState.isApproveButtonEnabled,
isApproveLoading = uiState.isApproveLoading,
isHoldToConfirm = uiState.isHoldToConfirm,
onApproveClick = model::onApproveClick,
onCancelClick = model::onCancelClick,
onOpenLearnMoreAboutApproveClick = model::onOpenLearnMoreAboutApproveClick,

View file

@ -8,6 +8,7 @@ import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
import com.tangem.common.ui.userwallet.ext.walletInterationIcon
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
@ -15,6 +16,7 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.domain.models.currency.CryptoCurrency
@ -27,19 +29,17 @@ import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransactionUseCase
import com.tangem.domain.transaction.usecase.gasless.GetFeeForGaslessUseCase
import com.tangem.domain.transaction.usecase.gasless.GetFeeForTokenUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.approval.api.GiveApprovalComponent
import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.common.ui.userwallet.ext.walletInterationIcon
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
@ -76,6 +76,7 @@ internal class GiveApprovalModel @Inject constructor(
walletInteractionIcon = walletInterationIcon(userWallet),
isApproveButtonEnabled = false,
isApproveLoading = false,
isHoldToConfirm = params.isHoldToConfirm,
),
)
@ -183,7 +184,7 @@ internal class GiveApprovalModel @Inject constructor(
contractAddress = tokenCurrency.contractAddress,
spenderAddress = params.spenderAddress,
).getOrElse { error ->
Timber.e(error, "Failed to create approval transaction")
TangemLogger.e("Failed to create approval transaction", error)
return false
}
@ -201,7 +202,7 @@ internal class GiveApprovalModel @Inject constructor(
)
}.fold(
ifLeft = { error ->
Timber.e("Failed to send approval transaction: $error")
TangemLogger.e("Failed to send approval transaction: $error")
false
},
ifRight = {

View file

@ -11,4 +11,5 @@ internal data class GiveApprovalUM(
@DrawableRes val walletInteractionIcon: Int?,
val isApproveButtonEnabled: Boolean,
val isApproveLoading: Boolean,
val isHoldToConfirm: Boolean = false,
)

View file

@ -48,6 +48,7 @@ internal fun GiveApprovalContent(
walletInteractionIcon: Int?,
isApproveEnabled: Boolean,
isApproveLoading: Boolean,
isHoldToConfirm: Boolean,
onApproveClick: () -> Unit,
onCancelClick: () -> Unit,
onOpenLearnMoreAboutApproveClick: () -> Unit,
@ -81,16 +82,28 @@ internal fun GiveApprovalContent(
SpacerH(height = TangemTheme.dimens.spacing20)
PrimaryButtonIconEnd(
text = stringResourceSafe(id = CommonUiR.string.common_approve),
iconResId = walletInteractionIcon,
showProgress = isApproveLoading,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16),
onClick = onApproveClick,
enabled = isApproveEnabled,
)
if (isHoldToConfirm) {
HoldToConfirmButton(
text = stringResourceSafe(id = CommonUiR.string.common_approve),
enabled = isApproveEnabled,
isLoading = isApproveLoading,
onConfirm = onApproveClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16),
)
} else {
PrimaryButtonIconEnd(
text = stringResourceSafe(id = CommonUiR.string.common_approve),
iconResId = walletInteractionIcon,
showProgress = isApproveLoading,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16),
onClick = onApproveClick,
enabled = isApproveEnabled,
)
}
SpacerH12()
@ -309,6 +322,7 @@ private fun GiveApprovalContentPreview(
walletInteractionIcon = params.walletInteractionIcon,
isApproveEnabled = params.isApproveEnabled,
isApproveLoading = params.isApproveLoading,
isHoldToConfirm = false,
onApproveClick = {},
onCancelClick = {},
onOpenLearnMoreAboutApproveClick = {},

View file

@ -23,6 +23,7 @@ dependencies {
implementation(projects.core.analytics.models)
implementation(projects.core.configToggles)
implementation(projects.core.navigation)
implementation(projects.core.utils)
/** Domain */
implementation(projects.domain.wallets)
@ -45,7 +46,6 @@ dependencies {
}
/** Other */
implementation(deps.timber)
/** DI */
implementation(deps.hilt.android)

View file

@ -22,13 +22,13 @@ import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.biometry.impl.ui.state.AskBiometryUM
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@ -88,7 +88,7 @@ internal class AskBiometryModel @Inject constructor(
* because it will be automatically saved on UserWalletsListManager switch
*/
val selectedUserWallet = getSelectedWalletUseCase.sync().getOrNull() ?: run {
Timber.e("Unable to save user wallet")
TangemLogger.e("Unable to save user wallet")
uiMessageSender.send(
SnackbarMessage(stringReference("No selected user wallet")),
)

View file

@ -68,7 +68,6 @@ dependencies {
implementation(deps.arrow.core)
implementation(deps.kotlin.immutable.collections)
implementation(deps.kotlin.serialization)
implementation(deps.timber)
implementation(deps.firebase.crashlytics)
/** DI */

View file

@ -66,7 +66,6 @@ dependencies {
implementation(deps.arrow.core)
implementation(deps.kotlin.immutable.collections)
implementation(deps.kotlin.serialization)
implementation(deps.timber)
implementation(deps.firebase.crashlytics)
/** DI */

View file

@ -18,7 +18,7 @@ import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.dialog.Dialogs
import com.tangem.core.ui.message.dialog.Dialogs.hotWalletCreationNotSupportedDialog
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
import com.tangem.domain.card.ScanCardProcessor
@ -40,7 +40,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
import javax.inject.Inject
private const val HIDE_PROGRESS_DELAY = 400L
@ -211,7 +211,7 @@ internal class CreateWalletStartModel @Inject constructor(
val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build()
if (userWallet == null) {
Timber.e("User wallet not created")
TangemLogger.e("User wallet not created")
setLoading(false)
return
}
@ -221,7 +221,7 @@ internal class CreateWalletStartModel @Inject constructor(
delay(HIDE_PROGRESS_DELAY)
setLoading(false)
when (error) {
is SaveWalletError.DataError -> Timber.e(error.toString(), "Unable to save user wallet")
is SaveWalletError.DataError -> TangemLogger.e("Unable to save user wallet: $error")
is SaveWalletError.WalletAlreadySaved -> {
userWalletsListRepository.unlock(
userWalletId = userWallet.walletId,
@ -246,17 +246,12 @@ internal class CreateWalletStartModel @Inject constructor(
private fun handleScanError(error: TangemError) {
when (error) {
is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable()
is TangemSdkError -> Timber.e(error, "Scan error occurred")
else -> Timber.e(error, "Error happened")
is TangemSdkError -> TangemLogger.e("Scan error occurred", error)
else -> TangemLogger.e("Error happened", error)
}
}
private fun handleNfcFeatureUnavailable() {
uiMessageSender.send(
message = DialogMessage(
message = resourceReference(R.string.nfc_error_unavailable),
title = resourceReference(id = R.string.common_error),
),
)
uiMessageSender.send(Dialogs.nfcFeatureUnavailable())
}
}

View file

@ -28,6 +28,7 @@ dependencies {
implementation(projects.core.configToggles)
implementation(projects.core.navigation)
implementation(projects.core.analytics)
implementation(projects.core.utils)
implementation(projects.core.analytics.models)
implementation(projects.common.routing)
implementation(projects.common.ui)
@ -78,7 +79,6 @@ dependencies {
/* Other */
implementation(deps.kotlin.immutable.collections)
implementation(deps.reKotlin)
implementation(deps.timber)
implementation(deps.arrow.core)
implementation(deps.arrow.fx)
}

View file

@ -2,6 +2,7 @@ package com.tangem.features.details.model
import android.content.res.Resources
import arrow.core.getOrElse
import com.tangem.utils.logging.TangemLogger
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
@ -45,7 +46,6 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import timber.log.Timber
import java.util.Locale
import javax.inject.Inject
@ -84,8 +84,8 @@ internal class DetailsModel @Inject constructor(
val isWalletConnectAvailable = runBlocking {
// danger region, this works immediately, but will be refactored later with WC
checkIsWalletConnectAvailableUseCase(params.userWalletId).getOrElse {
Timber.w("Unable to check WalletConnect availability: $it")
checkIsWalletConnectAvailableUseCase(params.userWalletId).getOrElse { throwable ->
TangemLogger.w("Unable to check WalletConnect availability: $throwable")
false
}

View file

@ -29,7 +29,7 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
import javax.inject.Inject
@Suppress("LongParameterList")
@ -115,7 +115,7 @@ internal class UserWalletListModel @Inject constructor(
unlockWalletUseCase(userWalletId)
.onRight { router.push(AppRoute.WalletSettings(userWalletId)) }
.onLeft { error ->
Timber.e("Failed to unlock wallet $userWalletId: $error")
TangemLogger.e("Failed to unlock wallet $userWalletId: $error")
error.handle(
onUserCancelled = {},
isFromUnlockAll = false,
@ -142,7 +142,7 @@ internal class UserWalletListModel @Inject constructor(
applyUserWalletListSortingUseCase(userWalletIds).onRight {
analyticsEventHandler.send(WalletSettingsAnalyticEvents.WalletsReorder())
}.onLeft { error ->
Timber.e("Failed to apply wallet list sorting: $error")
TangemLogger.e("Failed to apply wallet list sorting: $error")
}
}
}

View file

@ -45,7 +45,6 @@ dependencies {
/** Other dependencies */
implementation(deps.arrow.core)
implementation(deps.timber)
/** DI */
implementation(deps.hilt.android)

View file

@ -5,7 +5,7 @@ import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebView
import com.google.accompanist.web.AccompanistWebViewClient
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
/**
* [AccompanistWebViewClient] for Disclaimer [WebView]
@ -24,7 +24,7 @@ internal class DisclaimerWebViewClient(
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
Timber.d("onPageStarted: $url")
TangemLogger.d("onPageStarted: $url")
if (url != null) {
loadedUrls[url] = false
}
@ -33,7 +33,7 @@ internal class DisclaimerWebViewClient(
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
Timber.d("onPageFinished: $url")
TangemLogger.d("onPageFinished: $url")
if (url != null && loadedUrls.containsKey(url)) {
loadedUrls[url] = true
onLoadingFinished(false)
@ -45,7 +45,7 @@ internal class DisclaimerWebViewClient(
val url = request?.url?.toString()
Timber.d("onReceivedError: $url")
TangemLogger.d("onReceivedError: $url")
if (url != null && loadedUrls.containsKey(url)) {
loadedUrls[url] = true

View file

@ -10,6 +10,7 @@ import kotlinx.serialization.Serializable
interface AddToPortfolioManager {
// todo swap make updatable
val token: TokenMarketParams
val analyticsParams: AnalyticsParams?
val portfolioFetcher: PortfolioFetcher

View file

@ -57,6 +57,7 @@ dependencies {
implementation(projects.domain.yieldSupply.models)
implementation(projects.domain.yieldSupply)
implementation(projects.domain.earn)
implementation(projects.domain.search)
/* Compose */
implementation(deps.compose.coil)
@ -75,7 +76,6 @@ dependencies {
/* Other */
implementation(deps.kotlin.immutable.collections)
implementation(deps.timber)
implementation(deps.decompose.ext.compose)
/* Core */

View file

@ -22,7 +22,6 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.news.model.NewsListConfig
import com.tangem.features.feed.components.earn.DefaultEarnComponent
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent
import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent
@ -87,14 +86,6 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
innerRouter.push(
route = FeedEntryChildFactory.Child.TokenList(
params = DefaultMarketsTokenListComponent.Params(
onBackClicked = { onChildBack() },
onTokenClick = { token, currency ->
onMarketItemClick(
token = token,
appCurrency = currency,
source = AnalyticsParam.ScreensSources.Market.value,
)
},
preselectedSortType = sortBy ?: SortByTypeUM.Rating,
shouldAlwaysShowSearchBar = sortBy == null,
),
@ -134,11 +125,11 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
}
override fun onOpenEarnPage() {
innerRouter.push(
FeedEntryChildFactory.Child.Earn(
params = DefaultEarnComponent.Params(onBackClick = { onChildBack() }),
),
)
innerRouter.push(FeedEntryChildFactory.Child.Earn)
}
override fun openSearch() {
innerRouter.push(FeedEntryChildFactory.Child.Search)
}
}
@ -244,14 +235,6 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
)
FeedEntryRoute.MarketTokenList -> FeedEntryChildFactory.Child.TokenList(
DefaultMarketsTokenListComponent.Params(
onBackClicked = { router.pop() },
onTokenClick = { token, currency ->
clickIntents.onMarketItemClick(
token = token,
appCurrency = currency,
source = AnalyticsParam.ScreensSources.Market.value,
)
},
preselectedSortType = SortByTypeUM.Rating,
shouldAlwaysShowSearchBar = false,
),

View file

@ -7,15 +7,18 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.navigation.Route
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.features.feed.components.earn.DefaultEarnComponent
import com.tangem.features.promobanners.api.NewPromoBannersFeatureToggles
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
import com.tangem.features.feed.components.feed.DefaultFeedComponent
import com.tangem.features.feed.components.feed.DefaultFeedComponent.FeedParams
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent
import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent
import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent
import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent
import com.tangem.features.feed.components.news.list.DefaultNewsListComponent
import com.tangem.features.feed.components.news.list.DefaultNewsListComponent.Params
import com.tangem.features.feed.components.search.DefaultSearchComponent
import com.tangem.features.promobanners.api.NewPromoBannersFeatureToggles
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
import kotlinx.serialization.Serializable
import javax.inject.Inject
@ -53,9 +56,14 @@ internal class FeedEntryChildFactory @Inject constructor(
@Serializable
@Immutable
data class Earn(val params: DefaultEarnComponent.Params) : Child
data object Earn : Child
@Serializable
@Immutable
data object Search : Child
}
@Suppress("LongMethod")
fun createChild(
child: Child,
appComponentContext: AppComponentContext,
@ -75,6 +83,17 @@ internal class FeedEntryChildFactory @Inject constructor(
DefaultMarketsTokenListComponent(
appComponentContext = appComponentContext,
params = child.params,
clickIntents = DefaultMarketsTokenListComponent.ClickIntents(
onBackClicked = onBackClicked,
onSearchClicked = feedEntryClickIntents::openSearch,
onTokenClick = { token, currency ->
feedEntryClickIntents.onMarketItemClick(
token = token,
appCurrency = currency,
source = AnalyticsParam.ScreensSources.Market.value,
)
},
),
)
}
is Child.NewsDetails -> {
@ -86,7 +105,7 @@ internal class FeedEntryChildFactory @Inject constructor(
Child.NewsList -> {
DefaultNewsListComponent(
appComponentContext = appComponentContext,
params = DefaultNewsListComponent.Params(
params = Params(
onArticleClicked = { currentArticle, prefetchedArticles, paginationConfig ->
feedEntryClickIntents.onArticleClick(
articleId = currentArticle,
@ -102,7 +121,7 @@ internal class FeedEntryChildFactory @Inject constructor(
Child.Feed -> {
DefaultFeedComponent(
appComponentContext = appComponentContext,
params = DefaultFeedComponent.FeedParams(feedClickIntents = feedEntryClickIntents),
params = FeedParams(feedClickIntents = feedEntryClickIntents),
addToPortfolioComponentFactory = addToPortfolioPreselectedDataComponent,
promoBannersBlockComponentFactory = promoBannersBlockComponentFactory,
newPromoBannersFeatureToggles = newPromoBannersFeatureToggles,
@ -111,10 +130,19 @@ internal class FeedEntryChildFactory @Inject constructor(
is Child.Earn -> {
DefaultEarnComponent(
appComponentContext = appComponentContext,
params = child.params,
params = DefaultEarnComponent.Params(
onBackClick = onBackClicked,
onSearchClicked = feedEntryClickIntents::openSearch,
),
addToPortfolioComponentFactory = addToPortfolioPreselectedDataComponent,
)
}
Child.Search -> DefaultSearchComponent(
appComponentContext = appComponentContext,
params = DefaultSearchComponent.Params(
onBackClick = onBackClicked,
),
)
}
}
}

View file

@ -1,9 +1,17 @@
package com.tangem.features.feed.components.earn
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.subscribeAsState
@ -17,13 +25,16 @@ import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.feed.components.feed.FeedBottomSheetRoute
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent
import com.tangem.features.feed.model.earn.EarnModel
import com.tangem.features.feed.ui.components.FeedSearchBar
import com.tangem.features.feed.ui.earn.EarnContent
import kotlinx.serialization.Serializable
internal class DefaultEarnComponent(
appComponentContext: AppComponentContext,
@ -44,15 +55,41 @@ internal class DefaultEarnComponent(
override fun Title(bottomSheetState: State<BottomSheetState>) {
val background = LocalMainBottomSheetColor.current.value
val state by earnModel.state.collectAsStateWithLifecycle()
TangemTopAppBar(
containerColor = background,
title = stringResourceSafe(R.string.earn_title),
startButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_back_24,
onClicked = state.onBackClick,
isEnabled = bottomSheetState.value == BottomSheetState.EXPANDED,
),
)
if (LocalRedesignEnabled.current) {
FeedSearchBar(
isSearchBarClickable = bottomSheetState.value == BottomSheetState.EXPANDED,
feedListSearchBar = state.feedListSearchBar,
modifier = Modifier.drawBehind { drawRect(background) },
startContent = {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28),
contentDescription = null,
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier
.size(TangemTheme.dimens2.x11)
.background(
color = TangemTheme.colors2.button.backgroundSecondary,
shape = CircleShape,
)
.clickableSingle(
onClick = state.onBackClick,
enabled = bottomSheetState.value == BottomSheetState.EXPANDED,
)
.padding(TangemTheme.dimens2.x2),
)
},
)
} else {
TangemTopAppBar(
containerColor = background,
title = stringResourceSafe(R.string.earn_title),
startButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_back_24,
onClicked = state.onBackClick,
isEnabled = bottomSheetState.value == BottomSheetState.EXPANDED,
),
)
}
}
@Composable
@ -91,8 +128,8 @@ internal class DefaultEarnComponent(
)
}
@Serializable
data class Params(
val onBackClick: () -> Unit,
val onSearchClicked: () -> Unit,
)
}

View file

@ -1,10 +1,17 @@
package com.tangem.features.feed.components.market.details
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
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.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network
@ -12,9 +19,15 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds.topbar.TangemTopBarType
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.currency.CryptoCurrency
@ -86,15 +99,57 @@ internal class DefaultMarketsTokenDetailsComponent(
override fun Title(bottomSheetState: State<BottomSheetState>) {
val state by model.state.collectAsStateWithLifecycle()
val background = LocalMainBottomSheetColor.current.value
MarketsTokenDetailsTopBar(
onBackClick = { params.onBackClicked() },
isBackButtonEnabled = bottomSheetState.value == BottomSheetState.EXPANDED,
shouldShowPriceSubtitle = state.shouldShowPriceSubtitle,
tokenName = state.tokenName,
tokenPrice = state.priceText,
backgroundColor = background,
onShareClick = state.onShareClick,
)
if (LocalRedesignEnabled.current) {
TangemTopBar(
startContent = {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28),
contentDescription = null,
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier
.size(TangemTheme.dimens2.x11)
.background(
color = TangemTheme.colors2.button.backgroundSecondary,
shape = CircleShape,
)
.clickableSingle(
onClick = { params.onBackClicked() },
enabled = bottomSheetState.value == BottomSheetState.EXPANDED,
)
.padding(TangemTheme.dimens2.x2),
)
},
endContent = {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_share_new_24),
contentDescription = null,
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier
.size(TangemTheme.dimens2.x11)
.background(
color = TangemTheme.colors2.button.backgroundSecondary,
shape = CircleShape,
)
.clickableSingle(
onClick = state.onShareClick,
enabled = bottomSheetState.value == BottomSheetState.EXPANDED,
)
.padding(TangemTheme.dimens2.x2_5),
)
},
type = TangemTopBarType.BottomSheet,
)
} else {
MarketsTokenDetailsTopBar(
onBackClick = { params.onBackClicked() },
isBackButtonEnabled = bottomSheetState.value == BottomSheetState.EXPANDED,
shouldShowPriceSubtitle = state.shouldShowPriceSubtitle,
tokenName = state.tokenName,
tokenPrice = state.priceText,
backgroundColor = background,
onShareClick = state.onShareClick,
)
}
}
@Composable

View file

@ -8,6 +8,7 @@ import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.filterCryptoPortfolio
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
@ -28,7 +29,7 @@ internal class AvailableToAddDataConverter @Inject constructor(
availableNetworks: Set<TokenMarketInfo.Network>,
marketParams: TokenMarketParams,
): AvailableToAddData {
suspend fun AccountStatus.getAvailableToAddAccount(wallet: UserWallet): AvailableToAddAccount? {
suspend fun AccountStatus.CryptoPortfolio.getAvailableToAddAccount(wallet: UserWallet): AvailableToAddAccount? {
val currencies = availableNetworks
.mapNotNull { network ->
createCryptoCurrency(
@ -64,7 +65,7 @@ internal class AvailableToAddDataConverter @Inject constructor(
val (_, balance) = entry
val wallet = balance.userWallet
val filteredNetworks = wallet.filteredAvailableNetworks(availableNetworks)
val accounts = balance.accountsBalance.accountStatuses
val accounts = balance.accountsBalance.accountStatuses.filterCryptoPortfolio()
val availableToAddAccounts: Map<AccountId, AvailableToAddAccount> = accounts
.mapNotNull { accountStatus ->
val availableToAddAccount = accountStatus.getAvailableToAddAccount(wallet) ?: return@mapNotNull null
@ -103,17 +104,13 @@ internal class AvailableToAddDataConverter @Inject constructor(
userWallet: UserWallet,
network: TokenMarketInfo.Network,
marketParams: TokenMarketParams,
account: Account,
account: Account.CryptoPortfolio,
): CryptoCurrency? {
val derivationIndex = when (account) {
is Account.CryptoPortfolio -> account.derivationIndex
is Account.Payment -> TODO("[REDACTED_JIRA]")
}
return getTokenMarketCryptoCurrency(
userWalletId = userWallet.walletId,
tokenMarketParams = marketParams,
network = network,
accountIndex = derivationIndex,
accountIndex = account.derivationIndex,
)
}
}

View file

@ -34,7 +34,7 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
import javax.inject.Inject
private const val TOKEN_ACTIONS_DELAY = 500L
@ -205,7 +205,7 @@ internal class AddToPortfolioModel @Inject constructor(
finishFlow()
}
.catch { throwable ->
Timber.e(throwable)
TangemLogger.e("Error", throwable)
params.callback.onDismiss()
}
.launchIn(modelScope)

View file

@ -29,7 +29,7 @@ import com.tangem.features.feed.model.earn.analytics.EarnAnalyticsEvent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
import java.math.BigDecimal
import javax.inject.Inject
@ -134,7 +134,7 @@ internal class AddToPortfolioPreselectedDataModel @Inject constructor(
finishSuccessFlow(addedToken.currency, selectedPortfolioValue.userWallet.walletId)
}
.catch { throwable ->
Timber.e(throwable)
TangemLogger.e("Error", throwable)
params.callback.onDismiss()
}
.launchIn(modelScope)

View file

@ -11,7 +11,7 @@ import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase
import com.tangem.domain.managetokens.model.CurrencyUnsupportedState
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.feed.impl.R
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
import javax.inject.Inject
class CheckCurrencyUnsupportedDelegate @Inject constructor(
@ -29,14 +29,14 @@ class CheckCurrencyUnsupportedDelegate @Inject constructor(
networkId = rawNetworkId,
isMainNetwork = isMainNetwork,
).getOrElse { throwable ->
Timber.e(
throwable,
TangemLogger.e(
"""
Failed to check currency unsupported state
|- User wallet ID: $userWalletId
|- Network ID: $rawNetworkId
|- Is main network: $isMainNetwork
""".trimIndent(),
throwable,
)
val message = SnackbarMessage(

View file

@ -18,6 +18,7 @@ import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.filterCryptoPortfolio
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
@ -192,15 +193,12 @@ internal class MarketsPortfolioDelegate @AssistedInject constructor(
}.distinctUntilChanged()
private fun AccountStatusList.filterByRawID(): List<AccountWithAdded> {
fun AccountStatus.filterByRawID(): List<CryptoCurrencyStatus> = when (this) {
is AccountStatus.CryptoPortfolio -> this.tokenList.flattenCurrencies()
.filter { status ->
val currencyId = status.currency.id.rawCurrencyId ?: return@filter false
getTokenIdIfL2Network(currencyId.value) == currencyRawId.value
}
is AccountStatus.Payment -> TODO("[REDACTED_JIRA]")
}
return accountStatuses.map { accountStatus ->
fun AccountStatus.CryptoPortfolio.filterByRawID(): List<CryptoCurrencyStatus> = tokenList.flattenCurrencies()
.filter { status ->
val currencyId = status.currency.id.rawCurrencyId ?: return@filter false
getTokenIdIfL2Network(currencyId.value) == currencyRawId.value
}
return accountStatuses.filterCryptoPortfolio().map { accountStatus ->
AccountWithAdded(
accountStatus = accountStatus,
addedCurrency = accountStatus.filterByRawID(),

View file

@ -1,20 +1,34 @@
package com.tangem.features.feed.components.market.list
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
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.draw.drawBehind
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.features.feed.model.market.list.MarketsListModel
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
import com.tangem.features.feed.ui.components.FeedSearchBar
import com.tangem.features.feed.ui.market.list.MarketsList
import com.tangem.features.feed.ui.market.list.TopBarWithSearch
import kotlinx.serialization.Serializable
@ -22,20 +36,54 @@ import kotlinx.serialization.Serializable
internal class DefaultMarketsTokenListComponent(
appComponentContext: AppComponentContext,
private val params: Params,
private val clickIntents: ClickIntents,
) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext {
private val model: MarketsListModel = getOrCreateModel<MarketsListModel, Params>(params = params)
private val model: MarketsListModel = getOrCreateModel<MarketsListModel, ModelParams>(
params = ModelParams(
params = params,
clickIntents = clickIntents,
),
)
@Composable
override fun Title(bottomSheetState: State<BottomSheetState>) {
val state by model.state.collectAsStateWithLifecycle()
val bsState by bottomSheetState
TopBarWithSearch(
onBackClick = params.onBackClicked,
onSearchClick = state.onSearchClicked,
marketsSearchBar = state.marketsSearchBar,
bottomSheetState = bsState,
)
val background = LocalMainBottomSheetColor.current.value
if (LocalRedesignEnabled.current) {
FeedSearchBar(
isSearchBarClickable = bottomSheetState.value == BottomSheetState.EXPANDED,
feedListSearchBar = state.feedListSearchBar,
modifier = Modifier.drawBehind { drawRect(background) },
startContent = {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28),
contentDescription = null,
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier
.size(TangemTheme.dimens2.x11)
.background(
color = TangemTheme.colors2.button.backgroundSecondary,
shape = CircleShape,
)
.clickableSingle(
onClick = clickIntents.onBackClicked,
enabled = bottomSheetState.value == BottomSheetState.EXPANDED,
)
.padding(TangemTheme.dimens2.x2),
)
},
)
} else {
TopBarWithSearch(
onBackClick = clickIntents.onBackClicked,
onSearchClick = state.onSearchClicked,
marketsSearchBar = state.marketsSearchBar,
bottomSheetState = bsState,
)
}
}
@Composable
@ -62,9 +110,18 @@ internal class DefaultMarketsTokenListComponent(
@Serializable
data class Params(
val onBackClicked: () -> Unit,
val onTokenClick: ((TokenMarketParams, AppCurrency) -> Unit),
val preselectedSortType: SortByTypeUM,
val shouldAlwaysShowSearchBar: Boolean,
)
data class ClickIntents(
val onBackClicked: () -> Unit,
val onSearchClicked: () -> Unit,
val onTokenClick: ((TokenMarketParams, AppCurrency) -> Unit),
)
data class ModelParams(
val params: Params,
val clickIntents: ClickIntents,
)
}

View file

@ -22,7 +22,6 @@ import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds.topbar.TangemTopBarType
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
@ -47,7 +46,6 @@ internal class DefaultNewsDetailsComponent(
val state by newsDetailsModel.state.collectAsStateWithLifecycle()
if (LocalRedesignEnabled.current) {
TangemTopBar(
title = resourceReference(R.string.common_news),
type = TangemTopBarType.BottomSheet,
startContent = {
Icon(

View file

@ -0,0 +1,73 @@
package com.tangem.features.feed.components.search
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.ds.field.search.TangemFieldShape
import com.tangem.core.ui.ds.field.search.TangemSearchField
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds.topbar.TangemTopBarType
import com.tangem.features.feed.model.search.SearchModel
import com.tangem.features.feed.ui.search.SearchContent
import com.tangem.features.feed.ui.search.state.SearchCallbacks
internal class DefaultSearchComponent(
appComponentContext: AppComponentContext,
private val params: Params,
) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext {
private val model = getOrCreateModel<SearchModel, Params>(params = params)
@Composable
override fun Title(bottomSheetState: State<BottomSheetState>) {
val state by model.state.collectAsStateWithLifecycle()
val focusRequester = remember { FocusRequester() }
LaunchedEffect(bottomSheetState.value) {
if (bottomSheetState.value == BottomSheetState.EXPANDED) {
focusRequester.requestFocus()
}
}
TangemTopBar(
type = TangemTopBarType.BottomSheet,
reserveSlotSpace = false,
content = {
TangemSearchField(
state = state.searchBar,
shape = TangemFieldShape.Circle,
focusRequester = focusRequester,
modifier = Modifier.weight(1f),
enabled = bottomSheetState.value == BottomSheetState.EXPANDED,
)
},
)
}
@Composable
override fun Content(bottomSheetState: State<BottomSheetState>, modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
val searchCallbacks = remember {
SearchCallbacks(
onLoadMore = model::loadMore,
onClearHintsClick = model::clearSearchHistory,
onTextHintClick = model::onTextHintClick,
onResultMarketTokenClick = model::onResultMarketTokenClick,
)
}
SearchContent(
modifier = modifier,
content = state.content,
searchCallbacks = searchCallbacks,
)
}
data class Params(
val onBackClick: () -> Unit,
)
}

View file

@ -16,7 +16,7 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
internal class DefaultMarketsTokenDetailDeepLinkHandler @AssistedInject constructor(
@Assisted private val scope: CoroutineScope,
@ -44,7 +44,7 @@ internal class DefaultMarketsTokenDetailDeepLinkHandler @AssistedInject construc
tokenId = rawTokenId,
tokenSymbol = "", // used for analytics
).getOrElse {
Timber.e("Failed to get market token info")
TangemLogger.e("Failed to get market token info")
return@launch
}

View file

@ -9,7 +9,7 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
internal class DefaultNewsDetailsDeepLinkHandler @AssistedInject constructor(
@Assisted private val scope: CoroutineScope,
@ -25,7 +25,7 @@ internal class DefaultNewsDetailsDeepLinkHandler @AssistedInject constructor(
scope.launch {
val articleId = extractArticleIdFromUri(deeplinkUri)
if (articleId == null) {
Timber.e(
TangemLogger.e(
"""
Failed to extract article ID from deep link
|- Received URI: $deeplinkUri

View file

@ -11,6 +11,7 @@ import com.tangem.features.feed.model.market.details.MarketsTokenDetailsModel
import com.tangem.features.feed.model.market.list.MarketsListModel
import com.tangem.features.feed.model.news.details.NewsDetailsModel
import com.tangem.features.feed.model.news.list.NewsListModel
import com.tangem.features.feed.model.search.SearchModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@ -65,4 +66,9 @@ internal interface ModelModule {
@IntoMap
@ClassKey(EarnTypeFilterModel::class)
fun provideEarnTypeFilterModel(model: EarnTypeFilterModel): Model
@Binds
@IntoMap
@ClassKey(SearchModel::class)
fun provideSearchModel(model: SearchModel): Model
}

View file

@ -325,6 +325,7 @@ internal class EarnModel @Inject constructor(
onNetworkFilterClick = ::onNetworkFilterClick,
onTypeFilterClick = ::onTypeFilterClick,
onScroll = ::onMostlyUsedScrolled,
onSearchBarClicked = params.onSearchClicked,
),
)
}

View file

@ -1,8 +1,10 @@
package com.tangem.features.feed.model.earn.state
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.feed.model.earn.state.transformers.EarnUMTransformer
import com.tangem.features.feed.ui.earn.state.*
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -34,6 +36,10 @@ internal class EarnStateController @Inject constructor() {
onNetworkFilterClick = {},
onTypeFilterClick = {},
onSliderScroll = {},
feedListSearchBar = FeedListSearchBar(
placeholderText = TextReference.EMPTY,
onBarClick = {},
),
)
}
}

View file

@ -1,12 +1,16 @@
package com.tangem.features.feed.model.earn.state.transformers
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.ui.earn.state.EarnUM
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
internal class UpdateEarnUMInitialStateTransformer(
private val onBackClick: () -> Unit,
private val onNetworkFilterClick: () -> Unit,
private val onTypeFilterClick: () -> Unit,
private val onScroll: () -> Unit,
private val onSearchBarClicked: () -> Unit,
) : EarnUMTransformer {
override fun transform(prevState: EarnUM): EarnUM {
@ -15,6 +19,10 @@ internal class UpdateEarnUMInitialStateTransformer(
onNetworkFilterClick = onNetworkFilterClick,
onTypeFilterClick = onTypeFilterClick,
onSliderScroll = onScroll,
feedListSearchBar = FeedListSearchBar(
onBarClick = onSearchBarClicked,
placeholderText = resourceReference(id = R.string.markets_search_title_placeholder),
),
)
}
}

View file

@ -13,6 +13,7 @@ import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.DesignFeatureToggles
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
@ -66,6 +67,7 @@ internal class FeedComponentModel @Inject constructor(
private val fetchTopEarnTokensUseCase: FetchTopEarnTokensUseCase,
private val getTopEarnTokensUseCase: GetTopEarnTokensUseCase,
private val appRouter: AppRouter,
private val designFeatureToggles: DesignFeatureToggles,
getTopFiveMarketTokenUseCase: GetTopFiveMarketTokenUseCase,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
paramsContainer: ParamsContainer,
@ -232,10 +234,20 @@ internal class FeedComponentModel @Inject constructor(
return FeedListUM(
currentDate = getCurrentDate(),
feedListSearchBar = FeedListSearchBar(
placeholderText = resourceReference(R.string.markets_search_header_title),
placeholderText = resourceReference(
id = if (designFeatureToggles.isRedesignEnabled) {
R.string.markets_search_title_placeholder
} else {
R.string.markets_search_header_title
},
),
onBarClick = {
analyticsEventHandler.send(FeedAnalyticsEvent.TokenSearchedClicked())
params.feedClickIntents.onMarketOpenClick(null)
if (designFeatureToggles.isRedesignEnabled) {
params.feedClickIntents.openSearch()
} else {
params.feedClickIntents.onMarketOpenClick(null)
}
},
),
feedListCallbacks = FeedListCallbacks(

View file

@ -29,4 +29,6 @@ internal interface FeedModelClickIntents {
fun onOpenAllNews()
fun onOpenEarnPage()
fun openSearch()
}

View file

@ -188,6 +188,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
marketChartLook.copy(
type = percentChangeType.toChartType(),
isMinMaxLook = designFeatureToggles.isRedesignEnabled,
xAxisFormatter = MarketsDateTimeFormatters.getChartXFormatterByInterval(PriceChangeInterval.H24),
yAxisFormatter = { value ->
value.format {

View file

@ -1,6 +1,7 @@
package com.tangem.features.feed.model.market.details.converter
import androidx.compose.runtime.Stable
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
@ -37,7 +38,7 @@ internal class MetricsConverter(
metrics = persistentListOf(
InfoPointUM(
title = resourceReference(R.string.markets_token_details_market_capitalization),
value = marketCap.formatAmount(),
value = marketCap.formatAmount() ?: StringsSigns.DASH_SIGN,
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
@ -65,7 +66,7 @@ internal class MetricsConverter(
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_trading_volume),
value = volume24h.formatAmount(),
value = volume24h.formatAmount() ?: StringsSigns.DASH_SIGN,
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
@ -79,7 +80,7 @@ internal class MetricsConverter(
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_fully_diluted_valuation),
value = fullyDilutedValuation.formatAmount(),
value = fullyDilutedValuation.formatAmount() ?: StringsSigns.DASH_SIGN,
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
@ -95,7 +96,7 @@ internal class MetricsConverter(
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_circulating_supply),
value = circulatingSupply.formatAmount(crypto = true),
value = circulatingSupply.formatAmount(crypto = true) ?: StringsSigns.DASH_SIGN,
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
@ -109,7 +110,7 @@ internal class MetricsConverter(
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_max_supply),
value = maxSupply.formatMaxSupply(),
value = maxSupply.formatMaxSupply() ?: StringsSigns.DASH_SIGN,
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
@ -135,7 +136,7 @@ internal class MetricsConverter(
val liquidity = getLiquidity(value.volume24h, value.marketCap)
persistentListOf(
InfoPointUMV2.MarketCap(
capitalizationValue = stringReference(marketCap.formatAmount()),
capitalizationValue = marketCap.formatAmount()?.let(::stringReference),
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
@ -150,7 +151,7 @@ internal class MetricsConverter(
},
),
InfoPointUMV2.TradingVolume(
tradingValue = stringReference(volume24h.formatAmount()),
tradingValue = volume24h.formatAmount()?.let(::stringReference),
liquidity = liquidity,
trendingVolumeLiquidityType = getTrendingVolumeLiquidityType(liquidity),
onInfoClick = {
@ -165,7 +166,9 @@ internal class MetricsConverter(
},
),
InfoPointUMV2.MarketPosition(
position = marketRating?.toString() ?: StringsSigns.DASH_SIGN,
position = marketRating?.let {
stringReference(it.toString())
},
rangeValue = getMarketRatingRangeValue(marketRating),
marketRatingType = getMarketRatingType(marketRating),
onInfoClick = {
@ -176,12 +179,15 @@ internal class MetricsConverter(
),
)
},
marketRatingChange24H = getMarketRatingChange(marketRatingChange24h),
),
InfoPointUMV2.FullyDilutedValuation(
value = resourceReference(
R.string.markets_token_details_valuation_value_in_total,
wrappedList(fullyDilutedValuation.formatAmount()),
),
value = fullyDilutedValuation?.formatAmount()?.let { formatted ->
resourceReference(
id = R.string.markets_token_details_valuation_value_in_total,
formatArgs = wrappedList(formatted),
)
},
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
@ -196,20 +202,29 @@ internal class MetricsConverter(
},
),
InfoPointUMV2.CirculatingSupply(
currentValue = stringReference(circulatingSupply.formatAmount(crypto = true)),
maxValue = maxSupply?.let { supply ->
if (supply > BigDecimal.ZERO) {
stringReference(supply.formatMaxSupply())
} else {
null
currentValue = circulatingSupply?.formatAmount(true)?.let(::stringReference),
maxValue = when (maxSupply) {
null -> null
BigDecimal.ZERO -> {
resourceReference(
R.string
.markets_token_details_metrics_no_limited,
)
}
else -> maxSupply.formatAmount(true)?.let(::stringReference)
},
fillValue = getCirculatingSupplyFillValue(circulatingSupply, maxSupply),
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
title = resourceReference(R.string.markets_token_details_max_supply_full),
body = resourceReference(R.string.markets_token_details_total_supply_description),
title = resourceReference(
R.string.markets_token_details_max_supply_and_circulation_full,
),
body = combinedReference(
resourceReference(R.string.markets_token_details_circulating_supply_description),
stringReference("\n\n"),
resourceReference(R.string.markets_token_details_total_supply_description),
),
),
)
},
@ -234,7 +249,7 @@ internal class MetricsConverter(
)
}
private fun BigDecimal?.formatMaxSupply(): String {
private fun BigDecimal?.formatMaxSupply(): String? {
when (this) {
null -> return StringsSigns.DASH_SIGN
BigDecimal.ZERO -> return StringsSigns.INFINITY_SIGN
@ -243,8 +258,8 @@ internal class MetricsConverter(
return this.formatAmount(crypto = true)
}
private fun BigDecimal?.formatAmount(crypto: Boolean = false): String {
if (this == null) return StringsSigns.DASH_SIGN
private fun BigDecimal?.formatAmount(crypto: Boolean = false): String? {
if (this == null) return null
return if (crypto) {
format {
@ -266,9 +281,8 @@ internal class MetricsConverter(
}
@Suppress("MagicNumber")
private fun getLiquidity(volume24h: BigDecimal?, marketCap: BigDecimal?): Float {
if (volume24h == null || marketCap == null || marketCap == BigDecimal.ZERO) return 0f
private fun getLiquidity(volume24h: BigDecimal?, marketCap: BigDecimal?): Float? {
if (volume24h == null || marketCap == null || marketCap == BigDecimal.ZERO) return null
val ratio = volume24h
.divide(marketCap, 6, RoundingMode.HALF_UP)
.toFloat()
@ -277,8 +291,9 @@ internal class MetricsConverter(
}
@Suppress("MagicNumber")
private fun getTrendingVolumeLiquidityType(liquidity: Float): TrendingVolumeLiquidityType {
private fun getTrendingVolumeLiquidityType(liquidity: Float?): TrendingVolumeLiquidityType {
return when {
liquidity == null -> TrendingVolumeLiquidityType.UNKNOWN
liquidity >= 0.5f -> TrendingVolumeLiquidityType.HIGH
liquidity in 0.2f..<0.5f -> TrendingVolumeLiquidityType.MEDIUM
else -> TrendingVolumeLiquidityType.LOW
@ -296,8 +311,8 @@ internal class MetricsConverter(
}
@Suppress("MagicNumber")
private fun getMarketRatingRangeValue(marketRating: Int?): Float {
if (marketRating == null) return 0f
private fun getMarketRatingRangeValue(marketRating: Int?): Float? {
if (marketRating == null) return null
return when {
marketRating <= 20 -> {
@ -352,4 +367,12 @@ internal class MetricsConverter(
return ratio.coerceIn(0f, 1f)
}
private fun getMarketRatingChange(change: Int?): MarketRatingChange24H {
return when {
change == null || change == 0 -> MarketRatingChange24H.NoChanges
change < 0 -> MarketRatingChange24H.Down(-change)
else -> MarketRatingChange24H.Up(change)
}
}
}

View file

@ -45,7 +45,7 @@ internal class MarketsListModel @Inject constructor(
private val updateQuotesJob = JobHolder()
private val params = paramsContainer.require<DefaultMarketsTokenListComponent.Params>()
private val modelParams = paramsContainer.require<DefaultMarketsTokenListComponent.ModelParams>()
private val currentAppCurrency = getSelectedAppCurrencyUseCase().map { maybeAppCurrency ->
maybeAppCurrency.getOrElse { AppCurrency.Default }
@ -65,10 +65,11 @@ internal class MarketsListModel @Inject constructor(
onRetryButtonClicked = { activeListManager.reload() },
onTokenClick = { onTokenUIClicked(it) },
onShowTokensUnder100kClicked = { analyticsEventHandler.send(MarketsListAnalyticsEvent.ShowTokens()) },
shouldAlwaysShowSearchBar = Provider { params.shouldAlwaysShowSearchBar },
preselectedSortType = Provider { params.preselectedSortType },
onBackClick = params.onBackClicked,
shouldAlwaysShowSearchBar = Provider { modelParams.params.shouldAlwaysShowSearchBar },
preselectedSortType = Provider { modelParams.params.preselectedSortType },
onBackClick = modelParams.clickIntents.onBackClicked,
analyticsEventHandler = analyticsEventHandler,
onSearchBarClick = modelParams.clickIntents.onSearchClicked,
)
}
@ -261,7 +262,7 @@ internal class MarketsListModel @Inject constructor(
private fun onTokenUIClicked(token: MarketsListItemUM) {
modelScope.launch {
activeListManager.getTokenById(token.id)?.let { found ->
params.onTokenClick(found.toSerializableParam(), currentAppCurrency.value)
modelParams.clickIntents.onTokenClick(found.toSerializableParam(), currentAppCurrency.value)
}
}
}

View file

@ -9,6 +9,7 @@ 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.models.currency.CryptoCurrency
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
import kotlinx.collections.immutable.ImmutableList
internal data class MarketsListUM(
@ -20,6 +21,8 @@ internal data class MarketsListUM(
val onIntervalClick: (TrendInterval) -> Unit,
val onSortByButtonClick: () -> Unit,
val onSearchClicked: () -> Unit,
val feedListSearchBar: FeedListSearchBar,
val sortByMenuUM: SortByMenuUM,
) {
val isInSearchMode
get() = marketsSearchBar.searchBarUM.query.isNotEmpty()

View file

@ -0,0 +1,6 @@
package com.tangem.features.feed.model.market.list.state
internal data class SortByMenuUM(
val selectedOption: SortByTypeUM,
val onOptionClicked: (SortByTypeUM) -> Unit,
)

View file

@ -75,6 +75,11 @@ internal class MarketsListBatchFlowManager(
private val resultBatches = MutableStateFlow(ResultBatches())
private val uiBatches = stateManager.state.map { it.uiBatches }
val rawItems: Flow<List<TokenMarket>>
get() = batchFlow.state
.map { state -> state.data.flatMap { batch -> batch.data } }
.distinctUntilChanged()
val uiItems: StateFlow<ImmutableList<MarketsListItemUM>>
get() = uiBatches
.map { batches ->

View file

@ -12,6 +12,7 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.model.feed.analytics.FeedAnalyticsEvent
import com.tangem.features.feed.model.market.list.state.*
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
import com.tangem.utils.Provider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@ -33,6 +34,7 @@ internal class MarketsListUMStateManager(
private val onShowTokensUnder100kClicked: () -> Unit,
private val onBackClick: () -> Unit,
private val analyticsEventHandler: AnalyticsEventHandler,
private val onSearchBarClick: () -> Unit,
) {
val state = MutableStateFlow(state())
@ -64,6 +66,9 @@ internal class MarketsListUMStateManager(
selectedOption = value,
),
),
sortByMenuUM = marketsListUM.sortByMenuUM.copy(
selectedOption = value,
),
list = if (marketsListUM.list is ListUM.Content && marketsListUM.selectedSortBy != value) {
marketsListUM.list.copy(
triggerScrollReset = triggeredEvent(Unit) { consumeTriggerResetScrollEvent() },
@ -232,16 +237,24 @@ internal class MarketsListUMStateManager(
onDismissRequest = { isSortByBottomSheetShown = false },
content = SortByBottomSheetContentUM(
selectedOption = preselectedSortType(),
onOptionClicked = ::onBottomSheetOptionClicked,
onOptionClicked = ::onBottomSheetOrMenuOptionClicked,
),
),
onSearchClicked = {
analyticsEventHandler.send(FeedAnalyticsEvent.TokenSearchedClicked())
changeSearchBarIsActive(true)
},
feedListSearchBar = FeedListSearchBar(
onBarClick = onSearchBarClick,
placeholderText = resourceReference(id = R.string.markets_search_title_placeholder),
),
sortByMenuUM = SortByMenuUM(
selectedOption = preselectedSortType(),
onOptionClicked = ::onBottomSheetOrMenuOptionClicked,
),
)
private fun onBottomSheetOptionClicked(sortByTypeUM: SortByTypeUM) {
private fun onBottomSheetOrMenuOptionClicked(sortByTypeUM: SortByTypeUM) {
state.update { marketsListUM ->
marketsListUM.copy(
selectedSortBy = sortByTypeUM,
@ -251,6 +264,7 @@ internal class MarketsListUMStateManager(
selectedOption = sortByTypeUM,
),
),
sortByMenuUM = marketsListUM.sortByMenuUM.copy(selectedOption = sortByTypeUM),
)
}
}

View file

@ -6,10 +6,10 @@ import com.tangem.domain.markets.TokenMarketUpdateRequest
import com.tangem.pagination.BatchAction
import com.tangem.pagination.BatchUpdateResult
import com.tangem.pagination.PaginationStatus
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
internal fun logStatus(tag: String, status: PaginationStatus<List<TokenMarket>>) {
Timber.tag(tag).d(
TangemLogger.withTag(tag).d(
"""
Status
$status
@ -19,19 +19,19 @@ internal fun logStatus(tag: String, status: PaginationStatus<List<TokenMarket>>)
internal fun logAction(tag: String, action: BatchAction<Int, TokenMarketListConfig, TokenMarketUpdateRequest>) {
when (action) {
is BatchAction.Reload -> Timber.tag(tag).d(
is BatchAction.Reload -> TangemLogger.withTag(tag).d(
"""
Reload = ${action.requestParams}
""".trimIndent(),
)
is BatchAction.UpdateBatches -> Timber.tag(tag).d(
is BatchAction.UpdateBatches -> TangemLogger.withTag(tag).d(
"""
To update:
keys: ${action.keys.toList()}
updateType: ${action.updateRequest.javaClass.simpleName}
""".trimIndent(),
)
else -> Timber.tag(tag).d(
else -> TangemLogger.withTag(tag).d(
"""
$action
""".trimIndent(),
@ -48,7 +48,7 @@ internal fun logUpdateResults(
is BatchUpdateResult.Error -> s.throwable.toString()
}
Timber.tag(tag).d(
TangemLogger.withTag(tag).d(
"""
updateResults
request: ${updateResult.first}

View file

@ -35,7 +35,7 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
import java.util.Locale
import javax.inject.Inject
@ -134,7 +134,7 @@ internal class NewsDetailsModel @Inject constructor(
modelScope.launch {
toggleArticleLikedUseCase
.toggleLiked(articleId)
.onLeft { Timber.e(it) }
.onLeft { TangemLogger.e("Error", it) }
analyticsEventHandler.send(NewsDetailsAnalyticsEvent.NewsLikeClicked(articleId))
}
}
@ -231,7 +231,7 @@ internal class NewsDetailsModel @Inject constructor(
}
// global request executing error
newsId < 0 -> {
Timber.e(error)
TangemLogger.e("Error", error)
}
else -> {
val (code, message) = when (error) {

View file

@ -0,0 +1,302 @@
package com.tangem.features.feed.model.search
import arrow.core.getOrElse
import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
import com.tangem.domain.models.account.AccountName
import com.tangem.domain.search.model.RecentSearchToken
import com.tangem.domain.search.usecase.ClearSearchHistoryUseCase
import com.tangem.domain.search.usecase.GetSearchResultsUseCase
import com.tangem.domain.search.usecase.SaveSearchQueryUseCase
import com.tangem.features.feed.components.search.DefaultSearchComponent
import com.tangem.features.feed.model.market.list.state.MarketsListUM
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
import com.tangem.features.feed.model.market.list.statemanager.MarketsListBatchFlowManager
import com.tangem.features.feed.model.search.state.SearchStateController
import com.tangem.features.feed.model.search.state.transformers.*
import com.tangem.features.feed.ui.search.state.*
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
private const val UPDATE_QUOTES_TIMER_MILLIS = 60000L
@Suppress("LongParameterList")
@ModelScoped
internal class SearchModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
paramsContainer: ParamsContainer,
getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getSearchResultsUseCase: GetSearchResultsUseCase,
private val saveSearchQueryUseCase: SaveSearchQueryUseCase,
private val clearSearchHistoryUseCase: ClearSearchHistoryUseCase,
private val stateController: SearchStateController,
) : Model() {
private val params = paramsContainer.require<DefaultSearchComponent.Params>()
private val updateQuotesJob = JobHolder()
private val searchResultsJob = JobHolder()
private var shouldShowAllTokensIncludingUnder100k = false
private val currentAppCurrency = getSelectedAppCurrencyUseCase().map { maybeAppCurrency ->
maybeAppCurrency.getOrElse { AppCurrency.Default }
}.stateIn(
scope = modelScope,
started = SharingStarted.Eagerly,
initialValue = AppCurrency.Default,
)
private val searchMarketsListManager by lazy {
MarketsListBatchFlowManager(
getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase,
batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search,
currentAppCurrency = Provider { currentAppCurrency.value },
currentTrendInterval = Provider { MarketsListUM.TrendInterval.H24 },
currentSortByType = Provider { SortByTypeUM.Rating },
currentSearchText = Provider { stateController.value.searchBar.query },
modelScope = modelScope,
dispatchers = dispatchers,
)
}
val state: StateFlow<SearchUM> get() = stateController.uiState
init {
initCallbacks()
subscribeToQueryChanges()
subscribeToMarketUiItems()
subscribeToQuotesPolling()
subscribeToAppCurrencyChanges()
loadHistory()
}
fun loadMore() {
val content = stateController.value.content
if (content is SearchContentUM.Results) {
val market = content.marketTokens
if (market is MarketSearchResultUM.Content) {
searchMarketsListManager.loadMore()
}
}
}
fun clearSearchHistory() {
modelScope.launch(dispatchers.default) {
clearSearchHistoryUseCase()
}
}
fun onTextHintClick(text: String) {
stateController.update(UpdateSearchBarQueryTransformer(text))
}
fun onResultMarketTokenClick() {
modelScope.launch(dispatchers.default) {
saveSearchQueryUseCase(stateController.value.searchBar.query)
}
}
private fun initCallbacks() {
stateController.update(object : SearchUMTransformer {
override fun transform(prevState: SearchUM): SearchUM {
return prevState.copy(
searchBar = prevState.searchBar.copy(
onQueryChange = ::onQueryChange,
onActiveChange = ::onActiveChange,
onClearClick = ::onClearClick,
),
)
}
})
}
private fun onQueryChange(query: String) {
stateController.update(UpdateSearchBarQueryTransformer(query))
}
private fun onActiveChange(isActive: Boolean) {
if (!isActive) params.onBackClick()
}
private fun onClearClick() {
stateController.update(UpdateSearchBarQueryTransformer(""))
}
private fun subscribeToQueryChanges() {
stateController.uiState
.map { it.searchBar.query.trim() }
.distinctUntilChanged()
.onEach { query ->
shouldShowAllTokensIncludingUnder100k = false
if (query.isEmpty()) {
searchMarketsListManager.clearStateAndStopAllActions()
updateQuotesJob.cancel()
loadHistory()
} else {
stateController.update(SetSearchResultsLoadingTransformer())
searchMarketsListManager.reload(searchText = query)
subscribeToSearchResults(query)
}
}
.launchIn(modelScope)
}
private fun subscribeToSearchResults(query: String) {
modelScope.launch {
getSearchResultsUseCase(
query = query,
marketTokens = searchMarketsListManager.rawItems,
).collectLatest { searchResult ->
val userAssets = searchResult.userAssets.map { entry ->
UserAssetItemUM(
id = "${entry.userWalletId.stringValue}_${entry.accountId.value}" +
"_${entry.currencyStatus.currency.id.value}",
tokenIconUrl = entry.currencyStatus.currency.iconUrl,
tokenName = entry.currencyStatus.currency.name,
tokenSymbol = entry.currencyStatus.currency.symbol,
accountName = entry.accountName.toDisplayString(),
onClick = {
// TODO in [REDACTED_TASK_KEY] while just a stub item. Will be handled in next task.
},
)
}.toImmutableList()
stateController.update(UpdateUserAssetsTransformer(userAssets))
}
}.saveIn(searchResultsJob)
}
private fun subscribeToQuotesPolling() {
searchMarketsListManager.onLastBatchLoadedSuccess.onEach { batchKey ->
searchMarketsListManager.loadCharts(setOf(batchKey), MarketsListUM.TrendInterval.H24)
modelScope.loadQuotesWithTimer(UPDATE_QUOTES_TIMER_MILLIS)
}.launchIn(modelScope)
}
private fun subscribeToAppCurrencyChanges() {
currentAppCurrency.drop(1).onEach {
val query = stateController.value.searchBar.query
if (query.isNotEmpty()) {
searchMarketsListManager.reload()
}
}.launchIn(modelScope)
}
private fun subscribeToMarketUiItems() {
combine(
flow = stateController.uiState.map { it.searchBar.query }.distinctUntilChanged(),
flow2 = searchMarketsListManager.uiItems,
flow3 = searchMarketsListManager.isSearchNotFoundState,
flow4 = searchMarketsListManager.isInInitialLoadingErrorState,
) { query, uiItems, isSearchNotFound, isInErrorState ->
if (query.isEmpty()) return@combine null
val marketResult = when {
isSearchNotFound -> MarketSearchResultUM.NotFound
isInErrorState -> MarketSearchResultUM.NotFound
uiItems.isEmpty() -> MarketSearchResultUM.Empty
else -> buildMarketContent(uiItems)
}
SearchMarketBatchUiSnapshot(
marketResult = marketResult,
isSearchNotFound = isSearchNotFound,
isInErrorState = isInErrorState,
)
}
.filterNotNull()
.onEach { snapshot ->
stateController.update(ApplySearchMarketBatchTransformer(snapshot))
}
.launchIn(modelScope)
}
private fun buildMarketContent(allItems: ImmutableList<MarketsListItemUM>): MarketSearchResultUM.Content {
if (shouldShowAllTokensIncludingUnder100k) {
return MarketSearchResultUM.Content(items = allItems)
}
val filtered = allItems.filter { !it.isUnder100kMarketCap }.toImmutableList()
val hasUnder100k = filtered.size != allItems.size
return if (hasUnder100k) {
MarketSearchResultUM.Content(
items = filtered,
shouldShowUnder100kNotification = true,
onShowUnder100kClick = {
shouldShowAllTokensIncludingUnder100k = true
stateController.update(
UpdateMarketItemsTransformer(
MarketSearchResultUM.Content(items = allItems),
),
)
},
)
} else {
MarketSearchResultUM.Content(items = allItems)
}
}
private fun loadHistory() {
modelScope.launch {
getSearchResultsUseCase(query = "").collectLatest { searchResult ->
val textHints = searchResult.textHints.map { hint ->
TextHintItemUM(text = hint.text)
}.toImmutableList()
val recentTokens = searchResult.recentTokens.map { token ->
token.toMarketsListItemUM()
}.toImmutableList()
stateController.update(UpdateHistoryTransformer(textHints, recentTokens))
}
}.saveIn(searchResultsJob)
}
private fun RecentSearchToken.toMarketsListItemUM(): MarketsListItemUM {
return MarketsListItemUM(
id = id,
name = name,
currencySymbol = symbol,
iconUrl = imageUrl,
ratingPosition = null,
marketCap = null,
price = MarketsListItemUM.Price(text = ""),
trendPercentText = "",
trendType = PriceChangeType.NEUTRAL,
chartData = null,
isUnder100kMarketCap = false,
stakingRate = null,
updateTimestamp = timestamp,
)
}
private fun AccountName.toDisplayString(): String {
return when (this) {
is AccountName.DefaultMain -> "Main" // TODO [REDACTED_TASK_KEY] localize
is AccountName.Custom -> value
}
}
private fun CoroutineScope.loadQuotesWithTimer(timeMillis: Long) {
launch {
while (true) {
delay(timeMillis)
searchMarketsListManager.updateQuotes()
}
}.saveIn(updateQuotesJob)
}
}

View file

@ -0,0 +1,43 @@
package com.tangem.features.feed.model.search.state
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.model.search.state.transformers.SearchUMTransformer
import com.tangem.features.feed.ui.search.state.SearchContentUM
import com.tangem.features.feed.ui.search.state.SearchUM
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import javax.inject.Inject
@ModelScoped
internal class SearchStateController @Inject constructor() {
private val mutableUiState: MutableStateFlow<SearchUM> = MutableStateFlow(value = getInitialState())
val uiState: StateFlow<SearchUM> = mutableUiState.asStateFlow()
val value: SearchUM
get() = uiState.value
fun update(transformer: SearchUMTransformer) {
mutableUiState.update(function = transformer::transform)
}
private fun getInitialState(): SearchUM {
return SearchUM(
searchBar = SearchBarUM(
placeholderText = resourceReference(id = R.string.markets_search_title_placeholder),
query = "",
onQueryChange = {},
isActive = false,
onActiveChange = {},
onClearClick = {},
),
content = SearchContentUM.InitialEmpty,
)
}
}

View file

@ -0,0 +1,32 @@
package com.tangem.features.feed.model.search.state.transformers
import com.tangem.features.feed.ui.search.state.MarketSearchResultUM
import com.tangem.features.feed.ui.search.state.SearchContentUM
import com.tangem.features.feed.ui.search.state.SearchUM
internal class ApplySearchMarketBatchTransformer(
private val snapshot: SearchMarketBatchUiSnapshot,
) : SearchUMTransformer {
override fun transform(prevState: SearchUM): SearchUM {
if (shouldIgnoreTransientMarketEmpty(prevState, snapshot)) return prevState
return UpdateMarketItemsTransformer(snapshot.marketResult).transform(prevState)
}
private fun shouldIgnoreTransientMarketEmpty(prevState: SearchUM, snapshot: SearchMarketBatchUiSnapshot): Boolean {
if (snapshot.marketResult !is MarketSearchResultUM.Empty) return false
if (snapshot.isSearchNotFound || snapshot.isInErrorState) return false
val prev = prevState.content as? SearchContentUM.Results ?: return false
return when (prev.marketTokens) {
is MarketSearchResultUM.Content, is MarketSearchResultUM.Loading -> true
else -> false
}
}
}
/** Inputs from the search market list flow before merging into [SearchUM]. */
internal data class SearchMarketBatchUiSnapshot(
val marketResult: MarketSearchResultUM,
val isSearchNotFound: Boolean,
val isInErrorState: Boolean,
)

View file

@ -0,0 +1,7 @@
package com.tangem.features.feed.model.search.state.transformers
import com.tangem.features.feed.ui.search.state.SearchUM
internal interface SearchUMTransformer {
fun transform(prevState: SearchUM): SearchUM
}

View file

@ -0,0 +1,35 @@
package com.tangem.features.feed.model.search.state.transformers
import com.tangem.features.feed.ui.search.state.MarketSearchResultUM
import com.tangem.features.feed.ui.search.state.SearchContentUM
import com.tangem.features.feed.ui.search.state.SearchUM
import kotlinx.collections.immutable.persistentListOf
internal class SetSearchResultsLoadingTransformer : SearchUMTransformer {
override fun transform(prevState: SearchUM): SearchUM {
val currentContent = prevState.content
val currentUserAssets = if (currentContent is SearchContentUM.Results) {
currentContent.userAssets
} else {
persistentListOf()
}
// Keep showing the previous market list while the new query loads (stale-while-revalidate).
// Replacing with Loading + empty batch flashes skeletons / empty section even when the API
// returns the same tokens for a refined query.
val nextMarketTokens = if (currentContent is SearchContentUM.Results) {
when (val market = currentContent.marketTokens) {
is MarketSearchResultUM.Content -> market
else -> MarketSearchResultUM.Loading
}
} else {
MarketSearchResultUM.Loading
}
return prevState.copy(
content = SearchContentUM.Results(
userAssets = currentUserAssets,
marketTokens = nextMarketTokens,
),
)
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.features.feed.model.search.state.transformers
import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.features.feed.ui.search.state.SearchContentUM
import com.tangem.features.feed.ui.search.state.SearchUM
import com.tangem.features.feed.ui.search.state.TextHintItemUM
import kotlinx.collections.immutable.ImmutableList
internal class UpdateHistoryTransformer(
private val textHints: ImmutableList<TextHintItemUM>,
private val recentTokens: ImmutableList<MarketsListItemUM>,
) : SearchUMTransformer {
override fun transform(prevState: SearchUM): SearchUM {
return prevState.copy(
content = SearchContentUM.History(
textHints = textHints,
recentTokens = recentTokens,
),
)
}
}

View file

@ -0,0 +1,27 @@
package com.tangem.features.feed.model.search.state.transformers
import com.tangem.features.feed.ui.search.state.MarketSearchResultUM
import com.tangem.features.feed.ui.search.state.SearchContentUM
import com.tangem.features.feed.ui.search.state.SearchUM
import kotlinx.collections.immutable.persistentListOf
internal class UpdateMarketItemsTransformer(
private val marketResult: MarketSearchResultUM,
) : SearchUMTransformer {
override fun transform(prevState: SearchUM): SearchUM {
val currentContent = prevState.content
val currentUserAssets = if (currentContent is SearchContentUM.Results) {
currentContent.userAssets
} else {
persistentListOf()
}
return prevState.copy(
content = SearchContentUM.Results(
userAssets = currentUserAssets,
marketTokens = marketResult,
),
)
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.features.feed.model.search.state.transformers
import com.tangem.features.feed.ui.search.state.SearchUM
internal class UpdateSearchBarQueryTransformer(
private val query: String,
) : SearchUMTransformer {
override fun transform(prevState: SearchUM): SearchUM {
return prevState.copy(
searchBar = prevState.searchBar.copy(query = query),
)
}
}

View file

@ -0,0 +1,27 @@
package com.tangem.features.feed.model.search.state.transformers
import com.tangem.features.feed.ui.search.state.MarketSearchResultUM
import com.tangem.features.feed.ui.search.state.SearchContentUM
import com.tangem.features.feed.ui.search.state.SearchUM
import com.tangem.features.feed.ui.search.state.UserAssetItemUM
import kotlinx.collections.immutable.ImmutableList
internal class UpdateUserAssetsTransformer(
private val userAssets: ImmutableList<UserAssetItemUM>,
) : SearchUMTransformer {
override fun transform(prevState: SearchUM): SearchUM {
val currentContent = prevState.content
val currentMarketTokens = if (currentContent is SearchContentUM.Results) {
currentContent.marketTokens
} else {
MarketSearchResultUM.Loading
}
return prevState.copy(
content = SearchContentUM.Results(
userAssets = userAssets,
marketTokens = currentMarketTokens,
),
)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.features.feed.ui
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.Scaffold
@ -11,16 +12,17 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import com.arkivanov.decompose.extensions.compose.stack.Children
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.router.stack.ChildStack
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.features.feed.components.FeedEntryChildFactory
import com.tangem.features.feed.ui.utils.contentFeedEntryStackAnimation
import com.tangem.features.feed.ui.utils.topBarFeedEntryStackAnimation
import com.tangem.features.feed.ui.utils.contentFeedEntryAnimatedContentTransitionSpec
import com.tangem.features.feed.ui.utils.topBarFeedEntryAnimatedContentTransitionSpec
@OptIn(ExperimentalDecomposeApi::class)
@Composable
internal fun EntryContent(
bottomSheetState: State<BottomSheetState>,
@ -30,15 +32,15 @@ internal fun EntryContent(
) {
val density = LocalDensity.current
val background = LocalMainBottomSheetColor.current.value
val animationContent = remember { contentFeedEntryStackAnimation() }
val animationAppBar = remember { topBarFeedEntryStackAnimation() }
val animationContent = remember(stackState) { contentFeedEntryAnimatedContentTransitionSpec(stackState) }
val animationAppBar = remember(stackState) { topBarFeedEntryAnimatedContentTransitionSpec(stackState) }
Surface(contentColor = background) {
Scaffold(
containerColor = background,
contentWindowInsets = WindowInsetsZero,
topBar = {
Children(
AnimatedContent(
modifier = Modifier
.then(
if (!isOpenedInBottomSheet) {
@ -54,18 +56,22 @@ internal fun EntryContent(
}
}
},
stack = stackState.value,
animation = animationAppBar,
) { child ->
child.instance.Title(bottomSheetState)
targetState = stackState.value.active,
transitionSpec = animationAppBar,
contentKey = { it.key },
label = "FeedEntryAppBar",
) { state ->
state.instance.Title(bottomSheetState)
}
},
content = { contentPadding ->
Children(
stack = stackState.value,
animation = animationContent,
) { child ->
child.instance.Content(
AnimatedContent(
targetState = stackState.value.active,
transitionSpec = animationContent,
contentKey = { it.key },
label = "FeedEntryContent",
) { state ->
state.instance.Content(
modifier = Modifier.padding(contentPadding),
bottomSheetState = bottomSheetState,
)

View file

@ -0,0 +1,136 @@
package com.tangem.features.feed.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds.topbar.TangemTopBarType
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
@Composable
internal fun FeedSearchBar(
isSearchBarClickable: Boolean,
feedListSearchBar: FeedListSearchBar,
modifier: Modifier = Modifier,
startContent: @Composable (() -> Unit)? = null,
endContent: @Composable (() -> Unit)? = null,
) {
if (LocalRedesignEnabled.current) {
FeedSearchBarV2(
isSearchBarClickable = isSearchBarClickable,
feedListSearchBar = feedListSearchBar,
modifier = modifier,
startContent = startContent,
endContent = endContent,
)
} else {
FeedSearchBarV1(
isSearchBarClickable = isSearchBarClickable,
feedListSearchBar = feedListSearchBar,
modifier = modifier,
)
}
}
@Composable
private fun FeedSearchBarV1(
isSearchBarClickable: Boolean,
feedListSearchBar: FeedListSearchBar,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.padding(horizontal = 16.dp)
.padding(bottom = 8.dp)
.fillMaxWidth()
.clip(RoundedCornerShape(36.dp))
.background(color = TangemTheme.colors.field.focused)
.conditional(condition = isSearchBarClickable) {
clickable(onClick = feedListSearchBar.onBarClick)
}
.padding(14.dp),
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size20),
imageVector = ImageVector.vectorResource(id = R.drawable.ic_search_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
SpacerW(14.dp)
Text(
text = feedListSearchBar.placeholderText.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
}
@Composable
private fun FeedSearchBarV2(
isSearchBarClickable: Boolean,
feedListSearchBar: FeedListSearchBar,
modifier: Modifier = Modifier,
startContent: @Composable (() -> Unit)? = null,
endContent: @Composable (() -> Unit)? = null,
) {
TangemTopBar(
modifier = modifier,
startContent = startContent,
endContent = endContent,
type = TangemTopBarType.BottomSheet,
reserveSlotSpace = false,
content = {
Row(
modifier = Modifier
.weight(1f)
.padding(
start = if (startContent != null) TangemTheme.dimens2.x3 else 0.dp,
end = if (endContent != null) TangemTheme.dimens2.x3 else 0.dp,
)
.clip(CircleShape)
.background(color = TangemTheme.colors2.button.backgroundSecondary)
.conditional(condition = isSearchBarClickable) {
clickable(onClick = feedListSearchBar.onBarClick)
}
.padding(TangemTheme.dimens2.x3),
horizontalArrangement = Arrangement.Center,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens2.x5),
imageVector = ImageVector.vectorResource(id = R.drawable.ic_search_default_24),
tint = TangemTheme.colors2.markers.iconGray,
contentDescription = null,
)
SpacerW(TangemTheme.dimens2.x1)
Text(
text = feedListSearchBar.placeholderText.resolveReference(),
style = TangemTheme.typography2.bodySemibold16,
color = TangemTheme.colors2.text.neutral.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
},
)
}

View file

@ -22,6 +22,7 @@ import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.list.InfiniteListHandler
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.extensions.conditionalCompose
import com.tangem.core.ui.extensions.stringReference
@ -29,6 +30,7 @@ import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.*
import com.tangem.features.feed.ui.earn.components.*
import com.tangem.features.feed.ui.earn.state.*
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
import kotlinx.collections.immutable.persistentListOf
private const val EARN_LOAD_MORE_BUFFER = 3
@ -655,6 +657,10 @@ private fun previewEarnUM(
onNetworkFilterClick = {},
onTypeFilterClick = {},
onSliderScroll = {},
feedListSearchBar = FeedListSearchBar(
placeholderText = TextReference.Str("Search tokens & news"),
onBarClick = {},
),
)
private const val PLACEHOLDER_ITEMS_COUNT = 8

View file

@ -1,6 +1,7 @@
package com.tangem.features.feed.ui.earn.state
import androidx.compose.runtime.Immutable
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
@Immutable
internal data class EarnUM(
@ -11,4 +12,5 @@ internal data class EarnUM(
val onNetworkFilterClick: () -> Unit,
val onTypeFilterClick: () -> Unit,
val onSliderScroll: () -> Unit,
val feedListSearchBar: FeedListSearchBar,
)

View file

@ -4,34 +4,22 @@ import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.BaseSearchBarTestTags.SEARCH_BAR
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
import com.tangem.features.feed.ui.components.FeedSearchBar
import com.tangem.features.feed.ui.feed.components.*
import com.tangem.features.feed.ui.feed.preview.FeedListPreviewDataProvider.createFeedPreviewState
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
@ -50,8 +38,6 @@ internal fun FeedListHeader(
feedListSearchBar = feedListSearchBar,
modifier = modifier
.drawBehind { drawRect(background) }
.padding(horizontal = 16.dp)
.padding(bottom = 8.dp)
.testTag(SEARCH_BAR),
)
}
@ -95,39 +81,6 @@ internal fun FeedList(
}
}
@Composable
private fun FeedSearchBar(
isSearchBarClickable: Boolean,
feedListSearchBar: FeedListSearchBar,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.fillMaxWidth()
.clip(RoundedCornerShape(36.dp))
.background(color = TangemTheme.colors.field.focused)
.conditional(condition = isSearchBarClickable) {
clickable(onClick = feedListSearchBar.onBarClick)
}
.padding(14.dp),
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size20),
imageVector = ImageVector.vectorResource(id = R.drawable.ic_search_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
SpacerW(14.dp)
Text(
text = feedListSearchBar.placeholderText.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
}
@Composable
private fun FeedListContent(
state: FeedListUM,

View file

@ -14,6 +14,7 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.components.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
@ -23,13 +24,16 @@ import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun Header(
internal fun ColumnScope.Header(
onSeeAllClick: () -> Unit,
isLoading: Boolean,
shouldShowSeeAll: Boolean,
title: @Composable () -> Unit,
) {
val isRedesignEnabled = LocalRedesignEnabled.current
if (isRedesignEnabled) {
SpacerH(20.dp)
}
AnimatedContent(isLoading) { animatedState ->
Row(
modifier = Modifier

View file

@ -9,10 +9,20 @@ import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun DateBlock(currentDate: String) {
if (LocalRedesignEnabled.current) {
DateBlockV2(currentDate)
} else {
DateBlockV1(currentDate)
}
}
@Composable
private fun DateBlockV1(currentDate: String) {
SpacerH(20.dp)
Text(
modifier = Modifier
@ -30,4 +40,26 @@ internal fun DateBlock(currentDate: String) {
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.tertiary,
)
}
@Composable
private fun DateBlockV2(currentDate: String) {
SpacerH(TangemTheme.dimens2.x1)
Text(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp),
text = stringResourceSafe(R.string.feed_market_and_news),
style = TangemTheme.typography2.headingRegular28,
color = TangemTheme.colors2.text.neutral.primary,
)
Text(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp),
text = currentDate,
style = TangemTheme.typography2.headingRegular28,
color = TangemTheme.colors2.text.neutral.tertiary,
)
SpacerH(TangemTheme.dimens2.x6)
}

View file

@ -71,7 +71,7 @@ internal fun MarketBlock(marketChart: MarketChartUM?, feedListCallbacks: FeedLis
isLoading = currentChart is MarketChartUM.Loading,
)
SpacerH(12.dp)
SpacerH(if (isRedesignEnabled) 20.dp else 12.dp)
Charts(
onItemClick = feedListCallbacks.onMarketItemClick,
@ -89,7 +89,7 @@ internal fun MarketBlock(marketChart: MarketChartUM?, feedListCallbacks: FeedLis
@Suppress("LongMethod")
@Composable
internal fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) {
internal fun ColumnScope.MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) {
val isRedesignEnabled = LocalRedesignEnabled.current
val onSeeAllClick by rememberUpdatedState {
feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType)

View file

@ -17,11 +17,10 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import com.tangem.features.feed.ui.feed.components.articles.ArticleCard
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerW
@ -31,6 +30,8 @@ import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.feed.ui.feed.components.articles.ArticleCard
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
import com.tangem.features.feed.ui.feed.state.*
internal const val FOURTH_ITEM_INDEX = 3
@ -87,8 +88,16 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM,
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = stringResourceSafe(R.string.common_news),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
style = if (isRedesignEnabled) {
TangemTheme.typography2.headingSemibold20
} else {
TangemTheme.typography.h3
},
color = if (isRedesignEnabled) {
TangemTheme.colors2.text.neutral.primary
} else {
TangemTheme.colors.text.primary1
},
)
SpacerW(4.dp)
@ -113,7 +122,11 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM,
append(stringResourceSafe(R.string.feed_tangem_ai))
}
},
style = TangemTheme.typography.subtitle1,
style = if (isRedesignEnabled) {
TangemTheme.typography2.bodyRegular16.copy(fontWeight = FontWeight.Medium)
} else {
TangemTheme.typography.subtitle1
},
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)

View file

@ -32,6 +32,8 @@ import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
import com.tangem.core.ui.components.currency.icon.CoinIcon
import com.tangem.core.ui.components.marketprice.PriceChangeInPercent
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.ds.tabs.TangemSegmentUM
import com.tangem.core.ui.ds.tabs.TangemSegmentedPicker
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
@ -49,6 +51,7 @@ import com.tangem.features.feed.ui.market.detailed.state.InfoBottomSheetContent
import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM
import com.tangem.features.feed.ui.market.detailed.state.SecurityScoreBottomSheetContent
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.distinctUntilChanged
import com.tangem.core.ui.R as CoreR
@ -245,6 +248,26 @@ private fun IntervalSelector(
onIntervalClick: (PriceChangeInterval) -> Unit,
modifier: Modifier = Modifier,
) {
if (LocalRedesignEnabled.current) {
val items = remember {
PriceChangeInterval.entries
.map { TangemSegmentUM(it.toString(), it.getText()) }.toImmutableList()
}
val selectedItem = remember(trendInterval) {
items.firstOrNull { it.id == trendInterval.toString() }
}
TangemSegmentedPicker(
items = items,
initialSelectedItem = selectedItem,
isFixed = true,
modifier = modifier,
onClick = { onIntervalClick(PriceChangeInterval.valueOf(it.id)) },
)
return
}
SegmentedButtons(
config = persistentListOf(
PriceChangeInterval.H24,
@ -296,7 +319,6 @@ private fun ShowPriceSubtitleEffect(lazyListState: LazyListState, onShouldShowPr
}
}
@Composable
fun PriceChangeInterval.getText(): TextReference {
return when (this) {
PriceChangeInterval.H24 -> resourceReference(R.string.markets_selector_interval_24h_title)

View file

@ -111,20 +111,23 @@ private fun InsightsBlockV1(state: InsightsUM, modifier: Modifier = Modifier) {
@Composable
private fun InsightsBlockV2(state: InsightsUM, modifier: Modifier = Modifier) {
val segmentItems = persistentListOf(
TangemSegmentUM(
id = PriceChangeInterval.H24.name,
title = resourceReference(R.string.markets_token_details_insight_day_timeline),
),
TangemSegmentUM(
id = PriceChangeInterval.WEEK.name,
title = resourceReference(R.string.markets_token_details_insight_week_timeline),
),
TangemSegmentUM(
id = PriceChangeInterval.MONTH.name,
title = resourceReference(R.string.markets_token_details_insight_month_timeline),
),
)
val segmentItems = remember {
persistentListOf(
TangemSegmentUM(
id = PriceChangeInterval.H24.name,
title = resourceReference(R.string.markets_token_details_insight_day_timeline),
),
TangemSegmentUM(
id = PriceChangeInterval.WEEK.name,
title = resourceReference(R.string.markets_token_details_insight_week_timeline),
),
TangemSegmentUM(
id = PriceChangeInterval.MONTH.name,
title = resourceReference(R.string.markets_token_details_insight_month_timeline),
),
)
}
var currentInterval by remember { mutableStateOf(segmentItems.first()) }
TokenMarketInformationBlock(
@ -143,7 +146,6 @@ private fun InsightsBlockV2(state: InsightsUM, modifier: Modifier = Modifier) {
TangemSegmentedPicker(
items = segmentItems,
initialSelectedItem = segmentItems.first(),
hasSeparator = true,
isFixed = false,
isAltSurface = true,
minSegmentWidth = 48.dp,

View file

@ -21,14 +21,12 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.components.progressbar.TangemLinearProgressIndicator
import com.tangem.core.ui.ds.progress.TangemLinearProgressIndicatorWithDot
import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId
import com.tangem.core.ui.extensions.resolveReference
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.extensions.*
import com.tangem.core.ui.res.LocalIsInDarkTheme
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
@ -36,6 +34,7 @@ import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.ui.components.MetricsCard
import com.tangem.features.feed.ui.market.detailed.state.InfoPointUMV2
import com.tangem.features.feed.ui.market.detailed.state.MarketRatingChange24H
import com.tangem.features.feed.ui.market.detailed.state.MarketRatingType
import com.tangem.features.feed.ui.market.detailed.state.TrendingVolumeLiquidityType
@ -45,15 +44,7 @@ internal fun MarketCapCard(item: InfoPointUMV2.MarketCap) {
modifier = Modifier
.heightIn(120.dp)
.fillMaxWidth(),
title = {
Text(
text = item.capitalizationValue.resolveReference(),
style = TangemTheme.typography2.headingSemibold20,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
title = { MetricValueText(value = item.capitalizationValue) },
content = {
InformationTextBlock(
text = resourceReference(R.string.markets_token_details_market_capitalization),
@ -69,25 +60,22 @@ internal fun TradingVolumeCard(item: InfoPointUMV2.TradingVolume) {
TrendingVolumeLiquidityType.HIGH -> TangemTheme.colors2.markers.backgroundSolidGreen
TrendingVolumeLiquidityType.MEDIUM -> TangemTheme.colors2.graphic.status.attention
TrendingVolumeLiquidityType.LOW -> TangemTheme.colors2.graphic.status.warning
TrendingVolumeLiquidityType.UNKNOWN -> TangemTheme.colors2.surface.level3
}
val valueColor = metricValueColor(hasData = item.tradingValue != null)
MetricsCard(
modifier = Modifier
.heightIn(120.dp)
.fillMaxWidth(),
title = {
Row {
Text(
text = item.tradingValue.resolveReference(),
style = TangemTheme.typography2.headingSemibold20,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
MetricValueText(item.tradingValue)
Text(
modifier = Modifier.padding(TangemTheme.dimens2.x1),
text = stringResourceSafe(R.string.markets_token_details_trading_interval),
style = TangemTheme.typography2.captionSemibold11,
color = TangemTheme.colors2.text.neutral.primary,
color = valueColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
@ -95,14 +83,17 @@ internal fun TradingVolumeCard(item: InfoPointUMV2.TradingVolume) {
},
content = {
Column(modifier = Modifier.fillMaxWidth()) {
TangemLinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.height(6.dp),
progress = { item.liquidity },
color = tradingColor,
backgroundColor = TangemTheme.colors2.graphic.neutral.primaryInvertedConstant.copy(alpha = .1f),
)
if (item.liquidity != null) {
TangemLinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.height(6.dp),
progress = { item.liquidity },
color = tradingColor,
backgroundColor = TangemTheme.colors2.graphic.neutral.primaryInvertedConstant
.copy(alpha = .1f),
)
}
SpacerH(12.dp)
InformationTextBlock(
text = resourceReference(R.string.markets_token_details_trading_volume),
@ -127,37 +118,25 @@ internal fun MarketPositionCard(item: InfoPointUMV2.MarketPosition) {
.fillMaxWidth(),
title = {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_big_laurel_left_20),
tint = ratingColor,
contentDescription = null,
)
Text(
textAlign = TextAlign.Center,
text = item.position,
color = ratingColor,
style = TangemTheme.typography2.headingSemibold20.copy(letterSpacing = 0.sp),
maxLines = 1,
)
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_big_laurel_right_20),
tint = ratingColor,
contentDescription = null,
)
MarketPositionValue(position = item.position, ratingColor = ratingColor)
if (item.position != null) {
SpacerW(6.dp)
RatingChangeIndicator(change = item.marketRatingChange24H)
}
}
},
content = {
Column(modifier = Modifier.fillMaxWidth()) {
TangemLinearProgressIndicatorWithDot(
modifier = Modifier
.fillMaxWidth()
.height(6.dp),
progress = { item.rangeValue },
dotColor = TangemTheme.colors2.fill.neutral.primaryInvertedConstant,
backgroundColor = TangemTheme.colors2.graphic.neutral.primaryInvertedConstant.copy(alpha = .1f),
)
if (item.rangeValue != null) {
TangemLinearProgressIndicatorWithDot(
modifier = Modifier
.fillMaxWidth()
.height(6.dp),
progress = { item.rangeValue },
dotColor = TangemTheme.colors2.fill.neutral.primaryInvertedConstant,
backgroundColor = TangemTheme.colors2.graphic.neutral.primaryInvertedConstant.copy(alpha = .1f),
)
}
SpacerH(12.dp)
InformationTextBlock(
text = resourceReference(R.string.markets_token_details_market_rating),
@ -177,15 +156,7 @@ internal fun FDVCard(item: InfoPointUMV2.FullyDilutedValuation) {
modifier = Modifier
.heightIn(120.dp)
.fillMaxWidth(),
title = {
Text(
text = item.value.resolveReference(),
style = TangemTheme.typography2.headingSemibold20,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
title = { MetricValueText(value = item.value) },
content = {
InformationTextBlock(
text = resourceReference(R.string.markets_token_details_fully_diluted_valuation),
@ -212,15 +183,11 @@ internal fun CirculatingSupplyCard(item: InfoPointUMV2.CirculatingSupply) {
overflow = TextOverflow.Ellipsis,
)
Text(
MetricValueText(
value = item.currentValue,
modifier = Modifier
.padding(top = 12.dp)
.layoutId(TangemRowLayoutId.START_BOTTOM),
text = item.currentValue.resolveReference(),
style = TangemTheme.typography2.headingSemibold20,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
@ -238,7 +205,7 @@ internal fun CirculatingSupplyCard(item: InfoPointUMV2.CirculatingSupply) {
.padding(top = 12.dp)
.layoutId(TangemRowLayoutId.END_BOTTOM),
text = item.maxValue.resolveReference(),
style = TangemTheme.typography2.headingSemibold20,
style = TangemTheme.typography2.headingSemibold22,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
@ -253,7 +220,8 @@ internal fun CirculatingSupplyCard(item: InfoPointUMV2.CirculatingSupply) {
.fillMaxWidth()
.height(6.dp),
color = TangemTheme.colors2.graphic.status.accent,
trackColor = TangemTheme.colors2.graphic.neutral.primaryInvertedConstant.copy(alpha = .1f),
trackColor = TangemTheme.colors2.graphic.neutral.primaryInvertedConstant
.copy(alpha = .1f),
progress = { item.fillValue },
strokeCap = StrokeCap.Round,
drawStopIndicator = {},
@ -265,6 +233,87 @@ internal fun CirculatingSupplyCard(item: InfoPointUMV2.CirculatingSupply) {
)
}
// region Private helpers
@Composable
private fun MetricValueText(value: TextReference?, modifier: Modifier = Modifier) {
Text(
modifier = modifier,
text = value?.resolveReference() ?: stringResourceSafe(R.string.token_market_metrics_no_data),
style = TangemTheme.typography2.headingSemibold22,
color = metricValueColor(hasData = value != null),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
@Composable
private fun metricValueColor(hasData: Boolean): Color {
return if (hasData) TangemTheme.colors2.text.neutral.primary else TangemTheme.colors2.text.neutral.tertiary
}
@Composable
private fun MarketPositionValue(position: TextReference?, ratingColor: Color) {
if (position != null) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_big_laurel_left_20),
tint = ratingColor,
contentDescription = null,
)
Text(
textAlign = TextAlign.Center,
text = position.resolveReference(),
color = ratingColor,
style = TangemTheme.typography2.headingSemibold22.copy(letterSpacing = 0.sp),
maxLines = 1,
)
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_big_laurel_right_20),
tint = ratingColor,
contentDescription = null,
)
} else {
MetricValueText(value = null)
}
}
@Composable
private fun RatingChangeIndicator(change: MarketRatingChange24H) {
when (change) {
is MarketRatingChange24H.Up -> RatingChangeContent(
iconRes = R.drawable.ic_arrow_up_8,
iconTint = TangemTheme.colors2.markers.iconGreen,
changeValue = change.changeValue.toString(),
textColor = TangemTheme.colors2.text.status.positive,
)
is MarketRatingChange24H.Down -> RatingChangeContent(
iconRes = R.drawable.ic_arrow_down_8,
iconTint = TangemTheme.colors2.markers.iconRed,
changeValue = change.changeValue.toString(),
textColor = TangemTheme.colors2.text.status.warning,
)
MarketRatingChange24H.NoChanges -> Unit
}
}
@Composable
private fun RatingChangeContent(iconRes: Int, iconTint: Color, changeValue: String, textColor: Color) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
modifier = Modifier.size(TangemTheme.dimens2.x3),
imageVector = ImageVector.vectorResource(id = iconRes),
tint = iconTint,
contentDescription = null,
)
SpacerW(2.dp)
Text(
text = changeValue,
style = TangemTheme.typography2.captionSemibold12,
color = textColor,
)
}
}
@Composable
private fun MarketRatingType.baseColor(): Color {
val isDarkTheme = LocalIsInDarkTheme.current
@ -295,6 +344,8 @@ private fun mapRatingToCardColor(marketRatingType: MarketRatingType): Color {
}
}
// endregion
private const val GOLD_PLACE_COLOR_NIGHT = 0xFFFBEE76
private const val GOLD_PLACE_COLOR_LIGHT = 0xFFD9B900
private const val SILVER_PLACE_COLOR_NIGHT = 0xFFAABEF7
@ -353,17 +404,19 @@ private fun MetricsCardsPreview() {
MarketPositionCard(
item = InfoPointUMV2.MarketPosition(
position = "1",
position = stringReference("1"),
rangeValue = 0.02f,
marketRatingType = MarketRatingType.GOLD,
onInfoClick = {},
marketRatingChange24H = MarketRatingChange24H.NoChanges,
),
)
MarketPositionCard(
item = InfoPointUMV2.MarketPosition(
position = "2",
position = stringReference("2"),
rangeValue = 0.05f,
marketRatingChange24H = MarketRatingChange24H.Up(1),
marketRatingType = MarketRatingType.SILVER,
onInfoClick = {},
),
@ -371,19 +424,21 @@ private fun MetricsCardsPreview() {
MarketPositionCard(
item = InfoPointUMV2.MarketPosition(
position = "3",
rangeValue = 0.08f,
marketRatingType = MarketRatingType.BRONZE,
position = null,
rangeValue = null,
marketRatingType = MarketRatingType.OTHER,
onInfoClick = {},
marketRatingChange24H = MarketRatingChange24H.NoChanges,
),
)
MarketPositionCard(
item = InfoPointUMV2.MarketPosition(
position = "42",
position = stringReference("42"),
rangeValue = 0.42f,
marketRatingType = MarketRatingType.OTHER,
onInfoClick = {},
marketRatingChange24H = MarketRatingChange24H.Down(15),
),
)

View file

@ -2,12 +2,12 @@ package com.tangem.features.feed.ui.market.detailed.state
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.charts.state.MarketChartDataProducer
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
import kotlinx.collections.immutable.ImmutableList
import java.math.BigDecimal

View file

@ -14,35 +14,36 @@ internal sealed interface InfoPointUMV2 {
@Immutable
data class MarketCap(
val capitalizationValue: TextReference,
val capitalizationValue: TextReference?,
val onInfoClick: () -> Unit,
) : InfoPointUMV2
@Immutable
data class TradingVolume(
val tradingValue: TextReference,
val liquidity: Float,
val tradingValue: TextReference?,
val liquidity: Float?,
val trendingVolumeLiquidityType: TrendingVolumeLiquidityType,
val onInfoClick: () -> Unit,
) : InfoPointUMV2
@Immutable
data class MarketPosition(
val position: String,
val rangeValue: Float,
val position: TextReference?,
val rangeValue: Float?,
val marketRatingChange24H: MarketRatingChange24H,
val marketRatingType: MarketRatingType,
val onInfoClick: () -> Unit,
) : InfoPointUMV2
@Immutable
data class FullyDilutedValuation(
val value: TextReference,
val value: TextReference?,
val onInfoClick: () -> Unit,
) : InfoPointUMV2
@Immutable
data class CirculatingSupply(
val currentValue: TextReference,
val currentValue: TextReference?,
val maxValue: TextReference?,
val fillValue: Float?,
val onInfoClick: () -> Unit,
@ -60,9 +61,18 @@ internal data class MetricsV2UM(
}
internal enum class TrendingVolumeLiquidityType {
HIGH, MEDIUM, LOW,
HIGH, MEDIUM, LOW, UNKNOWN
}
internal enum class MarketRatingType {
GOLD, SILVER, BRONZE, OTHER
}
internal sealed interface MarketRatingChange24H {
data class Up(val changeValue: Int) : MarketRatingChange24H
data class Down(val changeValue: Int) : MarketRatingChange24H
data object NoChanges : MarketRatingChange24H
}

View file

@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.focus.FocusRequester
@ -24,27 +23,27 @@ import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.components.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
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.components.haze.hazeSourceTangem
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.conditionalCompose
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.model.market.list.state.*
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
import com.tangem.features.feed.ui.market.list.components.MarketsListLazyColumn
import com.tangem.features.feed.ui.market.list.components.MarketsListSortByBottomSheet
import kotlinx.collections.immutable.persistentListOf
import com.tangem.features.feed.ui.market.list.components.Options
import dev.chrisbanes.haze.rememberHazeState
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
@ -123,7 +122,16 @@ internal fun MarketsList(state: MarketsListUM, modifier: Modifier = Modifier) {
@Suppress("LongMethod")
@Composable
private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modifier) {
val strokeColor = TangemTheme.colors.stroke.primary
val isRedesignEnabled = LocalRedesignEnabled.current
val hazeState = rememberHazeState()
val strokeColor = if (isRedesignEnabled) {
TangemTheme.colors2.border.neutral.primary
} else {
TangemTheme.colors.stroke.primary
}
val scrolledState = remember { mutableStateOf(false) }
Column(modifier.padding(horizontal = TangemTheme.dimens.size16)) {
@ -145,11 +153,19 @@ private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modif
Column {
AnimatedVisibility(!state.isInSearchMode && !state.marketsSearchBar.shouldAlwaysShowSearchBar) {
Options(
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
modifier = Modifier.padding(
bottom = if (isRedesignEnabled) {
TangemTheme.dimens2.x2
} else {
TangemTheme.dimens.spacing12
},
),
sortByTypeUM = state.selectedSortBy,
trendInterval = state.selectedInterval,
onIntervalClick = state.onIntervalClick,
onSortByClick = state.onSortByButtonClick,
sortMenuUM = state.sortByMenuUM,
hazeState = hazeState,
)
}
}
@ -172,65 +188,18 @@ private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modif
},
)
ItemsList(
modifier = Modifier.conditionalCompose(
condition = isRedesignEnabled,
modifier = {
hazeSourceTangem(zIndex = 0f, state = hazeState)
},
),
scrolledState = scrolledState,
isInSearchMode = state.isInSearchMode,
state = state.list,
)
}
@Composable
private fun Options(
sortByTypeUM: SortByTypeUM,
trendInterval: MarketsListUM.TrendInterval,
onSortByClick: () -> Unit,
onIntervalClick: (MarketsListUM.TrendInterval) -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.height(IntrinsicSize.Max)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
SecondarySmallButton(
config = SmallButtonConfig(
text = sortByTypeUM.text,
onClick = onSortByClick,
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24),
),
)
SegmentedButtons(
config = persistentListOf(
MarketsListUM.TrendInterval.H24,
MarketsListUM.TrendInterval.D7,
MarketsListUM.TrendInterval.M1,
),
color = TangemTheme.colors.button.secondary,
initialSelectedItem = trendInterval,
onClick = onIntervalClick,
modifier = Modifier
.width(160.dp)
.fillMaxHeight(),
) {
Box(
Modifier
.fillMaxSize()
.align(Alignment.Center)
.padding(
vertical = TangemTheme.dimens.spacing4,
),
) {
Text(
modifier = Modifier.align(Alignment.Center),
text = it.text.resolveReference(),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.primary1,
)
}
}
}
}
@Composable
private fun ItemsList(
scrolledState: MutableState<Boolean>,
@ -343,6 +312,14 @@ private fun Preview() {
content = SortByBottomSheetContentUM(selectedOption = SortByTypeUM.Rating) {},
),
onSearchClicked = {},
feedListSearchBar = FeedListSearchBar(
onBarClick = {},
placeholderText = resourceReference(id = R.string.markets_search_title_placeholder),
),
sortByMenuUM = SortByMenuUM(
selectedOption = SortByTypeUM.Rating,
onOptionClicked = {},
),
),
)
}

View file

@ -3,7 +3,7 @@ package com.tangem.features.feed.ui.market.list.components
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Text
import androidx.compose.runtime.*
@ -11,6 +11,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.markets.MarketsListItem
import com.tangem.common.ui.markets.MarketsListItemPlaceholder
import com.tangem.common.ui.markets.models.MarketsListItemUM.Companion.TOKEN_LAZY_LIST_ID_SEPARATOR
@ -19,9 +20,12 @@ import com.tangem.core.ui.components.UnableToLoadData
import com.tangem.core.ui.components.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.components.list.InfiniteListHandler
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.extensions.conditionalCompose
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.MarketsTestTags
import com.tangem.domain.models.currency.CryptoCurrency
@ -40,6 +44,7 @@ internal fun MarketsListLazyColumn(
lazyListState: LazyListState,
modifier: Modifier = Modifier,
) {
val isRedesignEnabled = LocalRedesignEnabled.current
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
val coroutineScope = rememberCoroutineScope()
@ -93,11 +98,22 @@ internal fun MarketsListLazyColumn(
}
}
is ListUM.Content -> {
items(
itemsIndexed(
items = state.items,
key = { it.getComposeKey() },
) { item ->
key = { _, item -> item.getComposeKey() },
) { index, item ->
MarketsListItem(
modifier = Modifier.conditionalCompose(
condition = isRedesignEnabled,
modifier = {
roundedShapeItemDecoration(
currentIndex = index,
lastIndex = state.items.lastIndex,
backgroundColor = TangemTheme.colors2.surface.level3,
radius = TangemTheme.dimens2.x5,
)
},
),
model = item,
onClick = { state.onItemClick(item) },
)
@ -144,8 +160,8 @@ private fun LoadingErrorItem(onTryAgain: () -> Unit, modifier: Modifier = Modifi
Box(
modifier
.padding(
horizontal = TangemTheme.dimens.spacing16,
vertical = TangemTheme.dimens.spacing12,
horizontal = 16.dp,
vertical = 12.dp,
)
.fillMaxWidth(),
contentAlignment = Alignment.Center,

View file

@ -0,0 +1,176 @@
package com.tangem.features.feed.ui.market.list.components
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
import com.tangem.core.ui.components.haze.hazeEffectTangem
import com.tangem.core.ui.ds.button.PrimaryInverseTangemButton
import com.tangem.core.ui.ds.button.TangemButtonShape
import com.tangem.core.ui.ds.button.TangemButtonSize
import com.tangem.core.ui.ds.tabs.TangemSegmentUM
import com.tangem.core.ui.ds.tabs.TangemSegmentedPicker
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.model.market.list.state.MarketsListUM
import com.tangem.features.feed.model.market.list.state.SortByMenuUM
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
import dev.chrisbanes.haze.HazeState
import kotlinx.collections.immutable.persistentListOf
import com.tangem.core.ui.ds.button.TangemButtonIconPosition as RedesignTangemButtonIconPosition
@Suppress("LongParameterList")
@Composable
internal fun Options(
sortMenuUM: SortByMenuUM,
sortByTypeUM: SortByTypeUM,
trendInterval: MarketsListUM.TrendInterval,
hazeState: HazeState,
onSortByClick: () -> Unit,
onIntervalClick: (MarketsListUM.TrendInterval) -> Unit,
modifier: Modifier = Modifier,
) {
if (LocalRedesignEnabled.current) {
OptionsV2(
sortMenuUM = sortMenuUM,
trendInterval = trendInterval,
onIntervalClick = onIntervalClick,
modifier = modifier,
hazeState = hazeState,
)
} else {
OptionsV1(
sortByTypeUM = sortByTypeUM,
trendInterval = trendInterval,
onSortByClick = onSortByClick,
onIntervalClick = onIntervalClick,
modifier = modifier,
)
}
}
@Composable
private fun OptionsV1(
sortByTypeUM: SortByTypeUM,
trendInterval: MarketsListUM.TrendInterval,
onSortByClick: () -> Unit,
onIntervalClick: (MarketsListUM.TrendInterval) -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.height(IntrinsicSize.Max)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
SecondarySmallButton(
config = SmallButtonConfig(
text = sortByTypeUM.text,
onClick = onSortByClick,
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24),
),
)
SegmentedButtons(
config = persistentListOf(
MarketsListUM.TrendInterval.H24,
MarketsListUM.TrendInterval.D7,
MarketsListUM.TrendInterval.M1,
),
color = TangemTheme.colors.button.secondary,
initialSelectedItem = trendInterval,
onClick = onIntervalClick,
modifier = Modifier
.width(160.dp)
.fillMaxHeight(),
) {
Box(
Modifier
.fillMaxSize()
.align(Alignment.Center)
.padding(
vertical = TangemTheme.dimens.spacing4,
),
) {
Text(
modifier = Modifier.align(Alignment.Center),
text = it.text.resolveReference(),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.primary1,
)
}
}
}
}
@Composable
private fun OptionsV2(
sortMenuUM: SortByMenuUM,
trendInterval: MarketsListUM.TrendInterval,
hazeState: HazeState,
onIntervalClick: (MarketsListUM.TrendInterval) -> Unit,
modifier: Modifier = Modifier,
) {
var isShowDropdownMenu by rememberSaveable { mutableStateOf(false) }
val segmentItems = remember {
persistentListOf(
TangemSegmentUM(
id = MarketsListUM.TrendInterval.H24.name,
title = MarketsListUM.TrendInterval.H24.text,
),
TangemSegmentUM(
id = MarketsListUM.TrendInterval.D7.name,
title = MarketsListUM.TrendInterval.D7.text,
),
TangemSegmentUM(
id = MarketsListUM.TrendInterval.M1.name,
title = MarketsListUM.TrendInterval.M1.text,
),
)
}
Row(
modifier = modifier
.height(IntrinsicSize.Max)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
PrimaryInverseTangemButton(
onClick = {
isShowDropdownMenu = true
},
iconPosition = RedesignTangemButtonIconPosition.End,
iconRes = R.drawable.ic_chewron_down_20,
text = sortMenuUM.selectedOption.text,
size = TangemButtonSize.X9,
shape = TangemButtonShape.Rounded,
)
TangemSegmentedPicker(
items = segmentItems,
initialSelectedItem = segmentItems.firstOrNull { it.id == trendInterval.name },
isFixed = false,
isAltSurface = true,
minSegmentWidth = 54.dp,
onClick = { segment -> onIntervalClick(MarketsListUM.TrendInterval.valueOf(segment.id)) },
)
}
SortByMenu(
sortMenuUM = sortMenuUM,
showDropdownMenu = isShowDropdownMenu,
onDropdownDismiss = { isShowDropdownMenu = false },
modifier = Modifier.hazeEffectTangem(hazeState) {
blurRadius = 10.dp
},
)
}

View file

@ -0,0 +1,36 @@
package com.tangem.features.feed.ui.market.list.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.ds.contextmenu.TangemContextMenu
import com.tangem.core.ui.ds.contextmenu.TangemContextMenuCheckboxItem
import com.tangem.features.feed.model.market.list.state.SortByMenuUM
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
@Composable
internal fun SortByMenu(
sortMenuUM: SortByMenuUM,
showDropdownMenu: Boolean,
onDropdownDismiss: () -> Unit,
modifier: Modifier = Modifier,
) {
TangemContextMenu(
expanded = showDropdownMenu,
onDismissRequest = onDropdownDismiss,
offset = DpOffset.Zero,
modifier = modifier,
) {
SortByTypeUM.entries.fastForEach { sortType ->
TangemContextMenuCheckboxItem(
title = sortType.text,
isChecked = sortMenuUM.selectedOption == sortType,
onClick = {
sortMenuUM.onOptionClicked(sortType)
onDropdownDismiss()
},
)
}
}
}

View file

@ -13,11 +13,9 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.UnableToLoadData
import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.components.pager.PagerIndicator
import com.tangem.core.ui.ds.TangemPagerIndicator
import com.tangem.core.ui.ds.TangemPagerIndicatorColors
import com.tangem.core.ui.extensions.conditionalCompose
import com.tangem.core.ui.res.*
import com.tangem.features.feed.ui.news.details.components.ArticleDetail
import com.tangem.features.feed.ui.news.details.components.NewsDetailsPlaceholder
@ -84,12 +82,6 @@ private fun Content(state: NewsDetailsUM, background: Color) {
Column(
modifier = Modifier
.fillMaxSize()
.conditionalCompose(
condition = isRedesignEnabled,
modifier = {
hazeSourceTangem(zIndex = 1f)
},
)
.background(background),
) {
Box(modifier = Modifier.fillMaxSize()) {

View file

@ -0,0 +1,348 @@
package com.tangem.features.feed.ui.search
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.markets.MarketsListItem
import com.tangem.common.ui.markets.MarketsListItemPlaceholder
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.components.list.InfiniteListHandler
import com.tangem.core.ui.ds.button.*
import com.tangem.core.ui.ds.image.TangemIcon
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.feed.ui.search.state.*
private const val PLACEHOLDER_COUNT = 10
private const val LOAD_MORE_THRESHOLD = 5
@Composable
internal fun SearchContent(content: SearchContentUM, searchCallbacks: SearchCallbacks, modifier: Modifier = Modifier) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
val lazyListState = rememberLazyListState()
LazyColumn(
state = lazyListState,
modifier = modifier.fillMaxSize(),
contentPadding = PaddingValues(
start = TangemTheme.dimens2.x4,
end = TangemTheme.dimens2.x4,
bottom = bottomBarHeight,
),
) {
when (content) {
is SearchContentUM.InitialEmpty -> Unit
is SearchContentUM.History -> searchHistoryItems(
history = content,
onClearAllClick = searchCallbacks.onClearHintsClick,
onHintClick = searchCallbacks.onTextHintClick,
)
is SearchContentUM.Results -> searchResultsItems(
results = content,
onResultMarketTokenClick = searchCallbacks.onResultMarketTokenClick,
)
}
}
if (content is SearchContentUM.Results) {
InfiniteListHandler(
listState = lazyListState,
buffer = LOAD_MORE_THRESHOLD,
triggerLoadMoreCheckOnItemsCountChange = true,
onLoadMore = remember(content.marketTokens) {
{
searchCallbacks.onLoadMore()
true
}
},
)
}
}
private fun LazyListScope.searchHistoryItems(
history: SearchContentUM.History,
onClearAllClick: (() -> Unit),
onHintClick: (String) -> Unit,
) {
if (!history.textHints.isEmpty() || !history.recentTokens.isEmpty()) {
item(key = "recents") {
SectionHeader(
title = stringResourceSafe(R.string.markets_search_hint_header),
onClearAllClick = onClearAllClick,
)
}
}
items(
items = history.textHints,
key = { "hint_${it.text}" },
) { hint ->
TextHintItem(hint = hint, onHintClick = { onHintClick(hint.text) })
HorizontalDivider(
modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x2),
color = TangemTheme.colors2.border.neutral.primary,
)
}
items(
items = history.recentTokens,
key = { "recent_${it.getComposeKey()}" },
) { token ->
MarketsListItem(
modifier = Modifier
.padding(bottom = TangemTheme.dimens2.x2)
.background(
color = TangemTheme.colors2.surface.level3,
shape = RoundedCornerShape(TangemTheme.dimens2.x5),
),
model = token,
onClick = {}, // TODO in [REDACTED_TASK_KEY]
)
}
}
private fun LazyListScope.searchResultsItems(results: SearchContentUM.Results, onResultMarketTokenClick: () -> Unit) {
if (results.userAssets.isNotEmpty()) {
item(key = "header_portfolio") {
SectionHeader(title = stringResourceSafe(R.string.markets_search_portfolio_header))
}
items(
items = results.userAssets,
key = { it.id },
) { asset ->
UserAssetItem(asset)
}
}
when (val market = results.marketTokens) {
is MarketSearchResultUM.Empty -> Unit
is MarketSearchResultUM.Content -> marketSearchResultItems(
market = market,
hasUserAssetsSection = results.userAssets.isNotEmpty(),
onResultMarketTokenClick = onResultMarketTokenClick,
)
is MarketSearchResultUM.Loading -> marketSearchResultLoadingItems(
hasUserAssetsSection = results.userAssets.isNotEmpty(),
)
is MarketSearchResultUM.NotFound -> marketSearchResultNotFoundItem()
}
}
private fun LazyListScope.marketSearchResultItems(
market: MarketSearchResultUM.Content,
hasUserAssetsSection: Boolean,
onResultMarketTokenClick: () -> Unit,
) {
if (hasUserAssetsSection) {
item(key = "spacer_between_sections") {
SpacerH(TangemTheme.dimens2.x9)
}
}
item(key = "header_market") {
SectionHeader(title = stringResourceSafe(R.string.markets_common_title))
}
items(
items = market.items,
key = { "market_${it.getComposeKey()}" },
) { token ->
MarketsListItem(
modifier = Modifier
.padding(bottom = TangemTheme.dimens2.x2)
.background(
color = TangemTheme.colors2.surface.level3,
shape = RoundedCornerShape(TangemTheme.dimens2.x5),
),
model = token,
onClick = onResultMarketTokenClick,
)
}
if (market.shouldShowUnder100kNotification) {
item(key = "show_tokens_under_100k") {
ShowTokensUnder100kItem(
onShowTokensClick = market.onShowUnder100kClick,
)
}
}
}
private fun LazyListScope.marketSearchResultLoadingItems(hasUserAssetsSection: Boolean) {
if (hasUserAssetsSection) {
item(key = "spacer_between_sections") {
SpacerH(TangemTheme.dimens2.x9)
}
}
item(key = "header_market") {
SectionHeader(title = stringResourceSafe(R.string.markets_common_title))
}
items(count = PLACEHOLDER_COUNT) {
MarketsListItemPlaceholder()
}
}
private fun LazyListScope.marketSearchResultNotFoundItem() {
item(key = "not_found") {
Box(
modifier = Modifier.fillParentMaxSize(),
contentAlignment = Alignment.Center,
) {
Text(
text = stringResourceSafe(R.string.common_no_results),
style = TangemTheme.typography2.bodyRegular14,
color = TangemTheme.colors2.text.neutral.tertiary,
)
}
}
}
@Composable
private fun TextHintItem(hint: TextHintItemUM, onHintClick: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onHintClick)
.padding(vertical = 22.dp, horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_search_default_24),
contentDescription = null,
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier.size(TangemTheme.dimens2.x5),
)
SpacerW(TangemTheme.dimens2.x1)
Text(
modifier = Modifier.weight(1f),
text = hint.text,
style = TangemTheme.typography2.bodySemibold16,
color = TangemTheme.colors2.text.neutral.primary,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
SpacerW(TangemTheme.dimens2.x2)
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_return_24),
contentDescription = null,
tint = TangemTheme.colors2.markers.iconGray,
modifier = Modifier.size(TangemTheme.dimens2.x6),
)
}
}
// TODO in [REDACTED_TASK_KEY] while just a stub item. Will be handled in next task.
@Composable
private fun UserAssetItem(asset: UserAssetItemUM) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = asset.onClick)
.padding(horizontal = 12.dp, vertical = 14.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
TangemIcon(
tangemIconUM = TangemIconUM.Url(asset.tokenIconUrl, fallbackRes = R.drawable.ic_custom_token_44),
modifier = Modifier
.size(40.dp)
.clip(CircleShape),
)
Column(modifier = Modifier.weight(1f)) {
Text(
text = asset.tokenName,
style = TangemTheme.typography2.bodySemibold16,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
)
Text(
text = "${asset.tokenSymbol} · ${asset.accountName}",
style = TangemTheme.typography2.captionRegular13,
color = TangemTheme.colors2.text.neutral.tertiary,
maxLines = 1,
)
}
}
}
@Composable
private fun ShowTokensUnder100kItem(onShowTokensClick: () -> Unit, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.padding(
top = TangemTheme.dimens2.x9,
bottom = TangemTheme.dimens2.x3,
start = TangemTheme.dimens2.x10,
end = TangemTheme.dimens2.x10,
)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResourceSafe(R.string.markets_search_see_tokens_under_100k),
style = TangemTheme.typography2.bodyRegular14,
color = TangemTheme.colors2.text.neutral.secondary,
)
TangemButton(
buttonUM = TangemButtonUM(
text = resourceReference(R.string.markets_search_show_tokens),
onClick = onShowTokensClick,
type = TangemButtonType.Secondary,
size = TangemButtonSize.X8,
shape = TangemButtonShape.Rounded,
),
)
}
}
@Composable
private fun SectionHeader(title: String, modifier: Modifier = Modifier, onClearAllClick: (() -> Unit)? = null) {
Row(
modifier = modifier
.padding(
vertical = TangemTheme.dimens2.x3,
horizontal = TangemTheme.dimens2.x2,
)
.padding(bottom = TangemTheme.dimens2.x3),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.weight(1f),
text = title,
style = TangemTheme.typography2.headingSemibold20,
color = TangemTheme.colors2.text.neutral.primary,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
if (onClearAllClick != null) {
Text(
modifier = Modifier.clickableSingle(onClick = onClearAllClick),
text = stringResourceSafe(R.string.markets_search_clear_all_hints),
style = TangemTheme.typography2.bodySemibold16,
color = TangemTheme.colors2.text.neutral.primary,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
}
}

View file

@ -0,0 +1,538 @@
package com.tangem.features.feed.ui.search.preview
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.charts.state.MarketChartRawData
import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.feed.ui.search.SearchContent
import com.tangem.features.feed.ui.search.state.*
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
/** Labeled UI state for [SearchContent] previews. */
internal data class SearchContentPreviewScenario(
val title: String,
val content: SearchContentUM,
)
/** Sample market rows, portfolio assets, and hints for [SearchContent] previews (callbacks are no-op). */
@Suppress("MagicNumber", "StringLiteralDuplication", "LargeClass")
internal object SearchContentPreviewFixtures {
private val chartYSample = persistentListOf(
0.4, 0.2, 0.45, 0.35, 0.5, 0.55, 0.48, 0.62, 0.58, 0.7, 0.65, 0.8,
)
val scenarioInitialEmpty = SearchContentPreviewScenario(
title = "InitialEmpty blank screen before input",
content = SearchContentUM.InitialEmpty,
)
val scenarioHistoryEmptyBoth = SearchContentPreviewScenario(
title = "History empty hints and recents (no Recents header)",
content = SearchContentUM.History(
textHints = persistentListOf(),
recentTokens = persistentListOf(),
),
)
val scenarioHistoryHintsOnly = SearchContentPreviewScenario(
title = "History text hints only",
content = SearchContentUM.History(
textHints = hintsSample(),
recentTokens = persistentListOf(),
),
)
val scenarioHistoryRecentsOnly = SearchContentPreviewScenario(
title = "History recent tokens only",
content = SearchContentUM.History(
textHints = persistentListOf(),
recentTokens = recentsSample(),
),
)
val scenarioHistoryFull = SearchContentPreviewScenario(
title = "History hints + recents + Clear all",
content = SearchContentUM.History(
textHints = hintsSample(),
recentTokens = recentsSample(),
),
)
val scenarioResultsMarketEmptyNoPortfolio = SearchContentPreviewScenario(
title = "Results Market.Empty, empty portfolio",
content = SearchContentUM.Results(
userAssets = persistentListOf(),
marketTokens = MarketSearchResultUM.Empty,
),
)
val scenarioResultsMarketEmptyWithPortfolio = SearchContentPreviewScenario(
title = "Results Market.Empty, with portfolio",
content = SearchContentUM.Results(
userAssets = portfolioTwo(),
marketTokens = MarketSearchResultUM.Empty,
),
)
val scenarioResultsLoadingNoPortfolio = SearchContentPreviewScenario(
title = "Results Market.Loading, no portfolio",
content = SearchContentUM.Results(
userAssets = persistentListOf(),
marketTokens = MarketSearchResultUM.Loading,
),
)
val scenarioResultsLoadingWithPortfolio = SearchContentPreviewScenario(
title = "Results Market.Loading + portfolio (spacer + Market header)",
content = SearchContentUM.Results(
userAssets = portfolioTwo(),
marketTokens = MarketSearchResultUM.Loading,
),
)
val scenarioResultsNotFoundNoPortfolio = SearchContentPreviewScenario(
title = "Results Market.NotFound, no portfolio",
content = SearchContentUM.Results(
userAssets = persistentListOf(),
marketTokens = MarketSearchResultUM.NotFound,
),
)
val scenarioResultsNotFoundWithPortfolio = SearchContentPreviewScenario(
title = "Results Market.NotFound + portfolio",
content = SearchContentUM.Results(
userAssets = portfolioTwo(),
marketTokens = MarketSearchResultUM.NotFound,
),
)
val scenarioResultsContentMarketOnly = SearchContentPreviewScenario(
title = "Results Market.Content, market only",
content = SearchContentUM.Results(
userAssets = persistentListOf(),
marketTokens = MarketSearchResultUM.Content(
items = marketListShort(),
shouldShowUnder100kNotification = false,
onShowUnder100kClick = {},
),
),
)
val scenarioResultsContentPortfolioAndMarket = SearchContentPreviewScenario(
title = "Results portfolio + market (no under 100k)",
content = SearchContentUM.Results(
userAssets = portfolioTwo(),
marketTokens = MarketSearchResultUM.Content(
items = marketListShort(),
shouldShowUnder100kNotification = false,
onShowUnder100kClick = {},
),
),
)
val scenarioResultsContentWithUnder100kBanner = SearchContentPreviewScenario(
title = "Results portfolio + market + under 100k banner",
content = SearchContentUM.Results(
userAssets = portfolioTwo(),
marketTokens = MarketSearchResultUM.Content(
items = marketListShort(),
shouldShowUnder100kNotification = true,
onShowUnder100kClick = {},
),
),
)
val scenarioResultsLongMarketScroll = SearchContentPreviewScenario(
title = "Results long market list (scroll)",
content = SearchContentUM.Results(
userAssets = portfolioTwo(),
marketTokens = MarketSearchResultUM.Content(
items = marketListLong(),
shouldShowUnder100kNotification = true,
onShowUnder100kClick = {},
),
),
)
private fun marketToken(
rawId: String,
name: String,
symbol: String,
trend: PriceChangeType = PriceChangeType.UP,
priceText: String = "$98,765.43",
trendText: String = "+2.34%",
marketCap: String? = "$1.2 T",
rating: String? = "1",
chart: MarketChartRawData? = MarketChartRawData(y = chartYSample),
staking: Boolean = false,
updateTimestamp: Long = rawId.hashCode().toLong(),
): MarketsListItemUM = MarketsListItemUM(
id = CryptoCurrency.RawID(rawId),
name = name,
currencySymbol = symbol,
iconUrl = null,
ratingPosition = rating,
marketCap = marketCap,
price = MarketsListItemUM.Price(text = priceText),
trendPercentText = trendText,
trendType = trend,
chartData = chart,
isUnder100kMarketCap = false,
stakingRate = if (staking) stringReference("APY 4.2%") else null,
updateTimestamp = updateTimestamp,
)
private fun userAsset(
id: String,
name: String,
symbol: String,
accountName: String,
iconUrl: String? = null,
): UserAssetItemUM = UserAssetItemUM(
id = id,
tokenIconUrl = iconUrl,
tokenName = name,
tokenSymbol = symbol,
accountName = accountName,
onClick = {},
)
private fun textHint(text: String): TextHintItemUM = TextHintItemUM(text = text)
private fun hintsSample(): ImmutableList<TextHintItemUM> = persistentListOf(
textHint("bitcoin"),
textHint("sol"),
textHint("very long search query example for ellipsis"),
)
private fun recentsSample(): ImmutableList<MarketsListItemUM> = persistentListOf(
marketToken(rawId = "btc_r", name = "Bitcoin", symbol = "BTC"),
marketToken(
rawId = "eth_r",
name = "Ethereum",
symbol = "ETH",
trend = PriceChangeType.DOWN,
trendText = "1.02%",
rating = "2",
),
marketToken(
rawId = "long_r",
name = "A Very Long Token Name That Should Ellipsize In The List",
symbol = "LONG",
trend = PriceChangeType.NEUTRAL,
trendText = "0.00%",
marketCap = "$999.123456789 B",
rating = "42",
),
)
private fun portfolioTwo(): ImmutableList<UserAssetItemUM> = persistentListOf(
userAsset(id = "p1", name = "Ethereum", symbol = "ETH", accountName = "Main wallet"),
userAsset(
id = "p2",
name = "Polygon",
symbol = "POL",
accountName = "Account with a long label for preview",
),
)
private fun marketListShort(): ImmutableList<MarketsListItemUM> = persistentListOf(
marketToken(rawId = "m1", name = "Bitcoin", symbol = "BTC", rating = "1"),
marketToken(
rawId = "m2",
name = "Ethereum",
symbol = "ETH",
trend = PriceChangeType.DOWN,
trendText = "0.55%",
rating = "2",
),
marketToken(
rawId = "m3",
name = "Solana",
symbol = "SOL",
trend = PriceChangeType.NEUTRAL,
trendText = "0.12%",
rating = "3",
),
)
@Suppress("LongMethod")
private fun marketListLong(): ImmutableList<MarketsListItemUM> = listOf(
marketToken(
rawId = "L1",
name = "Arbitrum",
symbol = "ARB",
trend = PriceChangeType.UP,
priceText = "$1.12",
trendText = "+8.1%",
marketCap = "$3.1 B",
rating = "10",
),
marketToken(
rawId = "L2",
name = "Optimism",
symbol = "OP",
trend = PriceChangeType.DOWN,
priceText = "$2.34",
trendText = "3.2%",
marketCap = "$2.8 B",
rating = "11",
),
marketToken(
rawId = "L3",
name = "Base",
symbol = "",
trend = PriceChangeType.NEUTRAL,
priceText = "$0.98",
trendText = "0.0%",
marketCap = "$1.9 B",
rating = "12",
chart = null,
),
marketToken(
rawId = "L4",
name = "Avalanche",
symbol = "AVAX",
trend = PriceChangeType.UP,
priceText = "$36.5",
trendText = "+4.4%",
marketCap = "$14 B",
rating = "13",
staking = true,
),
marketToken(
rawId = "L5",
name = "Polkadot",
symbol = "DOT",
trend = PriceChangeType.DOWN,
priceText = "$6.12",
trendText = "2.1%",
marketCap = "$8 B",
rating = "14",
),
marketToken(
rawId = "L6",
name = "Cosmos",
symbol = "ATOM",
trend = PriceChangeType.UP,
priceText = "$8.90",
trendText = "+1.1%",
marketCap = "$3.4 B",
rating = "15",
),
marketToken(
rawId = "L7",
name = "Near",
symbol = "NEAR",
trend = PriceChangeType.DOWN,
priceText = "$4.56",
trendText = "0.8%",
marketCap = "$4.5 B",
rating = "16",
),
marketToken(
rawId = "L8",
name = "Sui",
symbol = "SUI",
trend = PriceChangeType.UP,
priceText = "$2.10",
trendText = "+12.3%",
marketCap = "$2.2 B",
rating = "17",
),
).toImmutableList()
fun allScenarios(): List<SearchContentPreviewScenario> = listOf(
scenarioInitialEmpty,
scenarioHistoryEmptyBoth,
scenarioHistoryHintsOnly,
scenarioHistoryRecentsOnly,
scenarioHistoryFull,
scenarioResultsMarketEmptyNoPortfolio,
scenarioResultsMarketEmptyWithPortfolio,
scenarioResultsLoadingNoPortfolio,
scenarioResultsLoadingWithPortfolio,
scenarioResultsNotFoundNoPortfolio,
scenarioResultsNotFoundWithPortfolio,
scenarioResultsContentMarketOnly,
scenarioResultsContentPortfolioAndMarket,
scenarioResultsContentWithUnder100kBanner,
scenarioResultsLongMarketScroll,
)
}
private val SearchContentPreviewCallbacks = SearchCallbacks(
onLoadMore = {},
onClearHintsClick = {},
onTextHintClick = { _ -> },
onResultMarketTokenClick = {},
)
/** All [SearchContentPreviewScenario] values for the Preview Parameter dropdown in Android Studio. */
internal class SearchContentPreviewParameterProvider : PreviewParameterProvider<SearchContentPreviewScenario> {
override val values: Sequence<SearchContentPreviewScenario>
get() = SearchContentPreviewFixtures.allScenarios().asSequence()
}
@Composable
private fun SearchContentPreviewHost(
scenario: SearchContentPreviewScenario,
modifier: Modifier = Modifier,
previewHeightDp: Int = 720,
) {
Box(
modifier = modifier
.height(previewHeightDp.dp)
.fillMaxWidth()
.background(TangemTheme.colors.background.primary),
) {
SearchContent(
content = scenario.content,
searchCallbacks = SearchContentPreviewCallbacks,
modifier = Modifier.fillMaxSize(),
)
}
}
// region Named previews (quick access without cycling parameters)
@Composable
@Preview(name = "01 Initial empty", showBackground = true, widthDp = 360, heightDp = 720)
@Preview(
name = "01 Initial empty (night)",
showBackground = true,
widthDp = 360,
heightDp = 720,
uiMode = Configuration.UI_MODE_NIGHT_YES,
)
private fun SearchContentPreview_InitialEmpty() {
TangemThemePreviewRedesign {
SearchContentPreviewHost(scenario = SearchContentPreviewFixtures.scenarioInitialEmpty)
}
}
@Composable
@Preview(name = "02 History full", showBackground = true, widthDp = 360, heightDp = 720)
@Preview(
name = "02 History full (night)",
showBackground = true,
widthDp = 360,
heightDp = 720,
uiMode = Configuration.UI_MODE_NIGHT_YES,
)
private fun SearchContentPreview_HistoryFull() {
TangemThemePreviewRedesign {
SearchContentPreviewHost(scenario = SearchContentPreviewFixtures.scenarioHistoryFull)
}
}
@Composable
@Preview(name = "03 Results loading + portfolio", showBackground = true, widthDp = 360, heightDp = 720)
@Preview(
name = "03 Results loading + portfolio (night)",
showBackground = true,
widthDp = 360,
heightDp = 720,
uiMode = Configuration.UI_MODE_NIGHT_YES,
)
private fun SearchContentPreview_ResultsLoadingWithPortfolio() {
TangemThemePreviewRedesign {
SearchContentPreviewHost(scenario = SearchContentPreviewFixtures.scenarioResultsLoadingWithPortfolio)
}
}
@Composable
@Preview(name = "04 Results not found", showBackground = true, widthDp = 360, heightDp = 720)
@Preview(
name = "04 Results not found (night)",
showBackground = true,
widthDp = 360,
heightDp = 720,
uiMode = Configuration.UI_MODE_NIGHT_YES,
)
private fun SearchContentPreview_ResultsNotFound() {
TangemThemePreviewRedesign {
SearchContentPreviewHost(scenario = SearchContentPreviewFixtures.scenarioResultsNotFoundNoPortfolio)
}
}
@Composable
@Preview(name = "05 Results market + under 100k", showBackground = true, widthDp = 360, heightDp = 800)
@Preview(
name = "05 Results market + under 100k (night)",
showBackground = true,
widthDp = 360,
heightDp = 800,
uiMode = Configuration.UI_MODE_NIGHT_YES,
)
private fun SearchContentPreview_ResultsWithUnder100kBanner() {
TangemThemePreviewRedesign {
SearchContentPreviewHost(
scenario = SearchContentPreviewFixtures.scenarioResultsContentWithUnder100kBanner,
previewHeightDp = 800,
)
}
}
@Composable
@Preview(name = "06 Results long scroll", showBackground = true, widthDp = 360, heightDp = 640)
@Preview(
name = "06 Results long scroll (night)",
showBackground = true,
widthDp = 360,
heightDp = 640,
uiMode = Configuration.UI_MODE_NIGHT_YES,
)
private fun SearchContentPreview_ResultsLongList() {
TangemThemePreviewRedesign {
SearchContentPreviewHost(
scenario = SearchContentPreviewFixtures.scenarioResultsLongMarketScroll,
previewHeightDp = 640,
)
}
}
// endregion
/** All scenarios via Preview Parameter; scenario label is the [SearchContentPreviewScenario] title property. */
@Composable
@Preview(name = "All scenarios (parameter)", showBackground = true, widthDp = 360, heightDp = 720)
@Preview(
name = "All scenarios (parameter, night)",
showBackground = true,
widthDp = 360,
heightDp = 720,
uiMode = Configuration.UI_MODE_NIGHT_YES,
)
private fun SearchContentPreview_AllScenarios(
@PreviewParameter(SearchContentPreviewParameterProvider::class) scenario: SearchContentPreviewScenario,
) {
TangemThemePreviewRedesign {
SearchContentPreviewHost(
scenario = scenario,
previewHeightDp = when (scenario.title) {
SearchContentPreviewFixtures.scenarioResultsLongMarketScroll.title -> 640
SearchContentPreviewFixtures.scenarioResultsContentWithUnder100kBanner.title -> 800
else -> 720
},
)
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.features.feed.ui.search.state
internal data class SearchCallbacks(
val onLoadMore: () -> Unit,
val onClearHintsClick: () -> Unit,
val onTextHintClick: (hint: String) -> Unit,
val onResultMarketTokenClick: () -> Unit,
)

View file

@ -0,0 +1,52 @@
package com.tangem.features.feed.ui.search.state
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import kotlinx.collections.immutable.ImmutableList
data class SearchUM(
val searchBar: SearchBarUM,
val content: SearchContentUM,
)
@Immutable
sealed interface SearchContentUM {
data class History(
val textHints: ImmutableList<TextHintItemUM>,
val recentTokens: ImmutableList<MarketsListItemUM>,
) : SearchContentUM
data class Results(
val userAssets: ImmutableList<UserAssetItemUM>,
val marketTokens: MarketSearchResultUM,
) : SearchContentUM
data object InitialEmpty : SearchContentUM
}
@Immutable
sealed interface MarketSearchResultUM {
data class Content(
val items: ImmutableList<MarketsListItemUM>,
val shouldShowUnder100kNotification: Boolean = false,
val onShowUnder100kClick: () -> Unit = {},
) : MarketSearchResultUM
data object Loading : MarketSearchResultUM
data object NotFound : MarketSearchResultUM
data object Empty : MarketSearchResultUM
}
data class TextHintItemUM(val text: String)
data class UserAssetItemUM(
val id: String,
val tokenIconUrl: String?,
val tokenName: String,
val tokenSymbol: String,
val accountName: String,
val onClick: () -> Unit,
)

View file

@ -1,71 +1,70 @@
package com.tangem.features.feed.ui.utils
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.layout.layout
import androidx.compose.ui.unit.dp
import com.arkivanov.decompose.FaultyDecomposeApi
import com.arkivanov.decompose.extensions.compose.stack.animation.*
import androidx.compose.runtime.State
import com.arkivanov.decompose.Child
import com.arkivanov.decompose.router.stack.ChildStack
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.utils.toPx
import com.tangem.features.feed.components.FeedEntryChildFactory
private const val DELAY_FOR_TRANSITION = 400
private fun FeedEntryChildFactory.Child?.usesFadeStackTransition(): Boolean = when (this) {
is FeedEntryChildFactory.Child.Search -> true
is FeedEntryChildFactory.Child.TokenList -> params.shouldAlwaysShowSearchBar
else -> false
}
@OptIn(FaultyDecomposeApi::class)
internal fun topBarFeedEntryStackAnimation(): StackAnimation<
FeedEntryChildFactory.Child,
ComposableModularBottomSheetContentComponent,
> =
stackAnimation { to, from, _ ->
val isSearchToTokenList =
(to.configuration as? FeedEntryChildFactory.Child.TokenList)?.params?.shouldAlwaysShowSearchBar == true
val isFromSearchTokenList =
(from.configuration as? FeedEntryChildFactory.Child.TokenList)?.params?.shouldAlwaysShowSearchBar == true
if (isSearchToTokenList || isFromSearchTokenList) {
fade()
} else {
slide()
}
internal typealias FeedEntryActiveChild =
Child.Created<FeedEntryChildFactory.Child, ComposableModularBottomSheetContentComponent>
internal typealias FeedEntryChildStack =
ChildStack<FeedEntryChildFactory.Child, ComposableModularBottomSheetContentComponent>
internal fun topBarFeedEntryAnimatedContentTransitionSpec(
stackState: State<FeedEntryChildStack>,
): AnimatedContentTransitionScope<FeedEntryActiveChild>.() -> ContentTransform =
{ feedEntryAnimatedContentTransform(stackState.value) }
internal fun contentFeedEntryAnimatedContentTransitionSpec(
stackState: State<FeedEntryChildStack>,
): AnimatedContentTransitionScope<FeedEntryActiveChild>.() -> ContentTransform =
{ feedEntryAnimatedContentTransform(stackState.value) }
private fun AnimatedContentTransitionScope<FeedEntryActiveChild>.feedEntryAnimatedContentTransform(
stack: FeedEntryChildStack,
): ContentTransform {
val shouldUseFade = initialState.configuration.usesFadeStackTransition() ||
targetState.configuration.usesFadeStackTransition()
return if (shouldUseFade) {
fadeIn(animationSpec = tween(FEED_ENTRY_FADE_DURATION_MS)) togetherWith
fadeOut(animationSpec = tween(FEED_ENTRY_FADE_DURATION_MS))
} else {
feedEntrySlideTransform(stack)
}
}
@OptIn(FaultyDecomposeApi::class)
internal fun contentFeedEntryStackAnimation(): StackAnimation<
FeedEntryChildFactory.Child,
ComposableModularBottomSheetContentComponent,
> =
stackAnimation { to, from, _ ->
val isSearchToTokenList =
(to.configuration as? FeedEntryChildFactory.Child.TokenList)?.params?.shouldAlwaysShowSearchBar == true
val isFromSearchTokenList =
(from.configuration as? FeedEntryChildFactory.Child.TokenList)?.params?.shouldAlwaysShowSearchBar == true
if (isSearchToTokenList || isFromSearchTokenList) {
fadeAndSlideInt()
} else {
slide()
}
}
private fun fadeAndSlideInt(): StackAnimator =
stackAnimator(animationSpec = tween(DELAY_FOR_TRANSITION)) { factor, _, content ->
val alpha = 1f - kotlin.math.abs(factor)
val slidePx = 32.dp.toPx()
val yOffset = when {
factor > 0 -> (slidePx * factor).toInt()
factor < 0 -> (slidePx * factor * -1).toInt()
else -> 0
}
content(
Modifier
.alpha(alpha)
.offsetY(yOffset),
private fun AnimatedContentTransitionScope<FeedEntryActiveChild>.feedEntrySlideTransform(
stack: FeedEntryChildStack,
): ContentTransform {
val isPushing = stack.backStack.lastOrNull()?.configuration == initialState.configuration
return if (isPushing) {
slideInHorizontally(
animationSpec = tween(FEED_ENTRY_SLIDE_DURATION_MS),
initialOffsetX = { it },
) togetherWith slideOutHorizontally(
animationSpec = tween(FEED_ENTRY_SLIDE_DURATION_MS),
targetOffsetX = { -it },
)
} else {
slideInHorizontally(
animationSpec = tween(FEED_ENTRY_SLIDE_DURATION_MS),
initialOffsetX = { -it },
) togetherWith slideOutHorizontally(
animationSpec = tween(FEED_ENTRY_SLIDE_DURATION_MS),
targetOffsetX = { it },
)
}
}
private fun Modifier.offsetY(pixels: Int): Modifier = layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
placeable.placeRelative(x = 0, y = pixels)
}
}
private const val FEED_ENTRY_FADE_DURATION_MS = 300
private const val FEED_ENTRY_SLIDE_DURATION_MS = 300

View file

@ -22,7 +22,8 @@ dependencies {
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.core.navigation)
implementation(projects.core.utils)
/** Common */
implementation(projects.common.routing)
@ -63,7 +64,6 @@ dependencies {
/** Other libraries */
implementation(deps.kotlin.immutable.collections)
implementation(deps.timber)
/** DI */
implementation(deps.hilt.android)

View file

@ -17,9 +17,7 @@ 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.navigation.url.UrlOpener
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.dialog.Dialogs
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.analytics.IntroductionProcess
import com.tangem.domain.card.analytics.ParamCardCurrencyConverter
@ -48,7 +46,7 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
import java.util.Locale
import javax.inject.Inject
@ -196,7 +194,7 @@ internal class HomeModel @Inject constructor(
val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build()
if (userWallet == null) {
Timber.e("User wallet not created")
TangemLogger.e("User wallet not created")
setLoading(false)
return
}
@ -205,11 +203,11 @@ internal class HomeModel @Inject constructor(
userWallet = userWallet,
analyticsSource = AnalyticsParam.ScreensSources.Intro,
).fold(
ifLeft = {
ifLeft = { error ->
delay(HIDE_PROGRESS_DELAY)
setLoading(false)
when (it) {
is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet")
when (error) {
is SaveWalletError.DataError -> TangemLogger.e("Unable to save user wallet: $error")
is SaveWalletError.WalletAlreadySaved -> appRouter.replaceAll(AppRoute.Wallet)
}
},
@ -245,17 +243,12 @@ internal class HomeModel @Inject constructor(
private fun handleScanError(error: TangemError) {
when (error) {
is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable()
is TangemSdkError -> Timber.e(error, "Scan error occurred")
else -> Timber.e(error, "Error happened")
is TangemSdkError -> TangemLogger.e("Scan error occurred", error)
else -> TangemLogger.e("Error happened", error)
}
}
private fun handleNfcFeatureUnavailable() {
uiMessageSender.send(
message = DialogMessage(
message = resourceReference(R.string.nfc_error_unavailable),
title = resourceReference(id = R.string.common_error),
),
)
uiMessageSender.send(Dialogs.nfcFeatureUnavailable())
}
}

View file

@ -38,6 +38,7 @@ dependencies {
implementation(projects.domain.feedback)
implementation(projects.domain.feedback.models)
implementation(projects.domain.hotWallet)
implementation(projects.domain.tokensync)
/** Common */
implementation(projects.common.ui)
@ -72,7 +73,6 @@ dependencies {
implementation(deps.arrow.core)
implementation(deps.kotlin.immutable.collections)
implementation(deps.kotlin.serialization)
implementation(deps.timber)
implementation(deps.firebase.crashlytics)
/** DI */

View file

@ -11,10 +11,12 @@ import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.settings.CanUseBiometryUseCase
import com.tangem.domain.tokensync.usecase.StartTokenSyncUseCase
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Attempts
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.MAX_FAST_FORWARD_ATTEMPTS
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH
import com.tangem.features.hotwallet.accesscoderequest.entity.HotAccessCodeRequestUM
import com.tangem.features.hotwallet.impl.R
@ -26,16 +28,19 @@ import com.tangem.utils.coroutines.saveIn
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
import javax.inject.Inject
@ModelScoped
@Suppress("LongParameterList")
internal class HotAccessCodeRequestModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val hotAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository,
private val userWalletsListRepository: UserWalletsListRepository,
private val canUseBiometryUseCase: CanUseBiometryUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val startTokenSyncUseCase: StartTokenSyncUseCase,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
) : Model() {
private val result = MutableStateFlow<HotWalletPasswordRequester.Result?>(null)
@ -53,7 +58,7 @@ internal class HotAccessCodeRequestModel @Inject constructor(
suspend fun show(attemptRequest: HotWalletPasswordRequester.AttemptRequest) {
if (userWalletExists(attemptRequest.hotWalletId).not()) {
Timber.e("User wallet with id ${attemptRequest.hotWalletId} does not exist")
TangemLogger.e("User wallet with id ${attemptRequest.hotWalletId} does not exist")
result.value = HotWalletPasswordRequester.Result.Dismiss
return
}
@ -214,6 +219,11 @@ internal class HotAccessCodeRequestModel @Inject constructor(
val currentRequest = currentRequest.value ?: return
val userWallet = userWalletsListRepository.userWalletsSync()
.firstOrNull { it is UserWallet.Hot && it.hotWalletId == currentRequest.hotWalletId } ?: return
if (hotWalletFeatureToggles.isTokenSyncEnabled) {
startTokenSyncUseCase.cancel(userWallet.walletId)
}
userWalletsListRepository.delete(listOf(userWallet.walletId))
dismiss()
}

View file

@ -1,11 +1,7 @@
package com.tangem.features.hotwallet.accesscoderequest.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
@ -26,9 +22,7 @@ import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.fields.PinTextColor
import com.tangem.core.ui.components.fields.PinTextField
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.extensions.*
import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.res.LocalHapticManager
import com.tangem.core.ui.res.TangemTheme
@ -42,12 +36,11 @@ internal fun HotAccessCodeRequestFullScreenContent(state: HotAccessCodeRequestUM
val userInteractionTracker = LocalUserInteractionTracker.current
Box(
modifier = Modifier
modifier = modifier
.fillMaxSize()
.trackUserInteraction(userInteractionTracker),
) {
AnimatedVisibility(
modifier = modifier,
visible = state.isShown,
enter = fadeIn(),
exit = fadeOut(),

View file

@ -1,5 +1,6 @@
package com.tangem.features.hotwallet.addexistingwallet.im.port.model
import com.tangem.utils.logging.TangemLogger
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent
@ -16,8 +17,10 @@ import com.tangem.core.ui.message.bottomSheetMessage
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
import com.tangem.domain.common.wallets.error.SaveWalletError
import com.tangem.domain.tokensync.usecase.StartTokenSyncUseCase
import com.tangem.domain.wallets.builder.HotUserWalletBuilder
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.hotwallet.MnemonicRepository
import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent
import com.tangem.features.hotwallet.addexistingwallet.im.port.entity.AddExistingWalletImportUM
@ -28,7 +31,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@ -40,6 +42,8 @@ internal class AddExistingWalletImportModel @Inject constructor(
private val tangemHotSdk: TangemHotSdk,
private val hotUserWalletBuilderFactory: HotUserWalletBuilder.Factory,
private val saveUserWalletUseCase: SaveWalletUseCase,
private val startTokenSyncUseCase: StartTokenSyncUseCase,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
private val analyticsEventHandler: AnalyticsEventHandler,
private val appsFlyerStore: AppsFlyerStore,
@ -96,10 +100,10 @@ internal class AddExistingWalletImportModel @Inject constructor(
val hotUserWalletBuilder = hotUserWalletBuilderFactory.create(hotWalletId)
val userWallet = hotUserWalletBuilder.build()
saveUserWalletUseCase.invoke(userWallet.copy(backedUp = true))
.onLeft {
.onLeft { error ->
setImportProgress(false)
when (it) {
is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet")
when (error) {
is SaveWalletError.DataError -> TangemLogger.e("Unable to save user wallet: $error")
is SaveWalletError.WalletAlreadySaved -> {
uiMessageSender.send(
SnackbarMessage(resourceReference(R.string.hw_import_seed_phrase_already_imported)),
@ -109,6 +113,11 @@ internal class AddExistingWalletImportModel @Inject constructor(
}
.onRight {
setImportProgress(false)
if (hotWalletFeatureToggles.isTokenSyncEnabled) {
startTokenSyncUseCase(userWallet.walletId)
}
analyticsEventHandler.send(
event = OnboardingAnalyticsEvent.Onboarding.Finished(
source = AnalyticsParam.ScreensSources.ImportWallet.value,
@ -129,8 +138,8 @@ internal class AddExistingWalletImportModel @Inject constructor(
)
params.callbacks.onWalletImported(userWallet.walletId)
}
}.onFailure {
Timber.e(it)
}.onFailure { throwable ->
TangemLogger.e("Error", throwable)
setImportProgress(false)
}
}

View file

@ -12,9 +12,9 @@ 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.navigation.url.UrlOpener
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.dialog.Dialogs
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.analytics.IntroductionProcess
import com.tangem.domain.card.repository.CardSdkConfigRepository
@ -34,7 +34,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
import javax.inject.Inject
private const val HIDE_PROGRESS_DELAY = 400L
@ -129,7 +129,7 @@ internal class CreateHardwareWalletModel @Inject constructor(
val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build()
if (userWallet == null) {
Timber.e("User wallet not created")
TangemLogger.e("User wallet not created")
setLoading(false)
return
}
@ -142,7 +142,7 @@ internal class CreateHardwareWalletModel @Inject constructor(
delay(HIDE_PROGRESS_DELAY)
setLoading(false)
when (saveWalletError) {
is SaveWalletError.DataError -> Timber.e(saveWalletError.toString(), "Unable to save user wallet")
is SaveWalletError.DataError -> TangemLogger.e("Unable to save user wallet: $saveWalletError")
is SaveWalletError.WalletAlreadySaved -> handleAlreadySavedCard(
saveWalletError.messageId,
walletId = userWallet.walletId,
@ -164,18 +164,13 @@ internal class CreateHardwareWalletModel @Inject constructor(
private fun handleScanError(error: TangemError) {
when (error) {
is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable()
is TangemSdkError -> Timber.e(error, "Scan error occurred")
else -> Timber.e(error, "Error happened")
is TangemSdkError -> TangemLogger.e("Scan error occurred", error)
else -> TangemLogger.e("Error happened", error)
}
}
private fun handleNfcFeatureUnavailable() {
uiMessageSender.send(
message = DialogMessage(
message = resourceReference(R.string.nfc_error_unavailable),
title = resourceReference(id = R.string.common_error),
),
)
uiMessageSender.send(Dialogs.nfcFeatureUnavailable())
}
private suspend fun handleAlreadySavedCard(messageId: Int, walletId: UserWalletId, scanResponse: ScanResponse) {

View file

@ -28,7 +28,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
import javax.inject.Inject
@Suppress("LongParameterList")
@ -112,7 +112,7 @@ internal class CreateMobileWalletModel @Inject constructor(
router.replaceAll(AppRoute.Wallet)
}.onFailure { throwable ->
Timber.e(throwable)
TangemLogger.e("Error", throwable)
uiState.update { it.copy(createButtonLoading = false) }
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.hotwallet.forgetwallet
import arrow.core.getOrElse
import com.tangem.utils.logging.TangemLogger
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
@ -11,8 +12,10 @@ import com.tangem.core.ui.extensions.resourceReference
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.tokensync.usecase.StartTokenSyncUseCase
import com.tangem.domain.wallets.usecase.DeleteWalletUseCase
import com.tangem.features.hotwallet.ForgetWalletComponent
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.hotwallet.forgetwallet.entity.ForgetWalletUM
import com.tangem.features.hotwallet.impl.R
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -20,9 +23,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped
internal class ForgetWalletModel @Inject constructor(
paramsContainer: ParamsContainer,
@ -30,6 +33,8 @@ internal class ForgetWalletModel @Inject constructor(
private val router: Router,
private val deleteWalletUseCase: DeleteWalletUseCase,
private val uiMessageSender: UiMessageSender,
private val startTokenSyncUseCase: StartTokenSyncUseCase,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
) : Model() {
private val params = paramsContainer.require<ForgetWalletComponent.Params>()
@ -79,9 +84,13 @@ internal class ForgetWalletModel @Inject constructor(
private fun forgetWallet() {
modelScope.launch {
if (hotWalletFeatureToggles.isTokenSyncEnabled) {
startTokenSyncUseCase.cancel(params.userWalletId)
}
val hasUserWallets = deleteWalletUseCase(params.userWalletId)
.getOrElse {
Timber.e("Unable to delete wallet: $it")
.getOrElse { error ->
TangemLogger.e("Unable to delete wallet: $error")
uiMessageSender.send(
message = SnackbarMessage(resourceReference(R.string.common_unknown_error)),

View file

@ -3,6 +3,7 @@ package com.tangem.features.hotwallet.manualbackup.check.model
import androidx.compose.runtime.Stable
import androidx.compose.ui.text.input.TextFieldValue
import arrow.core.getOrElse
import com.tangem.utils.logging.TangemLogger
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -20,15 +21,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import kotlin.Boolean
import kotlin.Int
import kotlin.String
import kotlin.Suppress
import kotlin.collections.List
import kotlin.collections.all
import kotlin.collections.map
@Stable
@ModelScoped
@ -63,7 +56,7 @@ internal class ManualBackupCheckModel @Inject constructor(
}
}
}.onFailure {
Timber.e(it)
TangemLogger.e("Error", it)
}
}
}
@ -114,8 +107,8 @@ internal class ManualBackupCheckModel @Inject constructor(
uiState.update {
it.copy(completeButtonProgress = false)
}
}.onFailure {
Timber.e(it)
}.onFailure { throwable ->
TangemLogger.e("Error", throwable)
uiState.update {
it.copy(completeButtonProgress = false)

View file

@ -19,7 +19,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
import javax.inject.Inject
@Stable
@ -58,7 +58,7 @@ internal class ManualBackupPhraseModel @Inject constructor(
}
}
}.onFailure {
Timber.e(it)
TangemLogger.e("Error", it)
}
}
}

View file

@ -19,7 +19,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
import javax.inject.Inject
@Stable
@ -53,7 +53,7 @@ internal class ViewPhraseModel @Inject constructor(
modelScope.launch {
val words = exportSeedPhraseUseCase.invoke(userWallet.hotWalletId)
.getOrElse { error -> Timber.e(error); throw error }
.getOrElse { error -> TangemLogger.e("Error", error); throw error }
.mnemonic.mnemonicComponents
uiState.update {

View file

@ -21,7 +21,7 @@ import com.tangem.features.hotwallet.walletbackup.entity.WalletBackupUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
@ -71,7 +71,7 @@ internal class WalletBackupModel @Inject constructor(
.onEach { either ->
either.fold(
ifLeft = {
Timber.e("Error on getting user wallet: $it")
TangemLogger.e("Error on getting user wallet: $it")
},
ifRight = { userWallet ->
if (!isScreenOpenedEventSent.get() && userWallet is UserWallet.Hot) {
@ -128,13 +128,13 @@ internal class WalletBackupModel @Inject constructor(
getUserWalletUseCase.invoke(params.userWalletId)
.fold(
ifLeft = {
Timber.e("Error on getting user wallet: $it")
TangemLogger.e("Error on getting user wallet: $it")
},
ifRight = { userWallet ->
when (userWallet) {
is UserWallet.Cold -> {
val userWalletId = userWallet.walletId
Timber.e("Unexpected cold wallet when request seed phrase: $userWalletId")
TangemLogger.e("Unexpected cold wallet when request seed phrase: $userWalletId")
}
is UserWallet.Hot -> showSeedPhrase(userWallet)
}
@ -155,7 +155,7 @@ internal class WalletBackupModel @Inject constructor(
unlockHotWalletContextualUseCase.invoke(hotWallet.hotWalletId)
.fold(
ifLeft = {
Timber.e("Error while export seed phrase: $it")
TangemLogger.e("Error while export seed phrase: $it")
},
ifRight = { seedPhrasePrivateInfo ->
router.push(AppRoute.ViewPhrase(params.userWalletId))

View file

@ -35,7 +35,7 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
import javax.inject.Inject
@Suppress("LongParameterList")
@ -147,7 +147,7 @@ internal class WalletHardwareBackupModel @Inject constructor(
-> modelScope.launch {
unlockHotWalletContextualUseCase.invoke(hotWalletId)
.onLeft {
Timber.e(it, "Unable to unlock wallet with id ${params.userWalletId}")
TangemLogger.e("Unable to unlock wallet with id ${params.userWalletId}", it)
}
.onRight {
router.push(AppRoute.UpgradeWallet(userWalletId = params.userWalletId))

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