Updated on 2026-08-14
This commit is contained in:
commit
af09ce9ca4
877 changed files with 16091 additions and 6613 deletions
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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() },
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ interface GiveApprovalComponent : ComposableBottomSheetComponent {
|
|||
val amount: String,
|
||||
val spenderAddress: String,
|
||||
val subtitle: TextReference,
|
||||
val isHoldToConfirm: Boolean = false,
|
||||
val callback: Callback,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -11,4 +11,5 @@ internal data class GiveApprovalUM(
|
|||
@DrawableRes val walletInteractionIcon: Int?,
|
||||
val isApproveButtonEnabled: Boolean,
|
||||
val isApproveLoading: Boolean,
|
||||
val isHoldToConfirm: Boolean = false,
|
||||
)
|
||||
|
|
@ -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 = {},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ dependencies {
|
|||
|
||||
/** Other dependencies */
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.timber)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import kotlinx.serialization.Serializable
|
|||
|
||||
interface AddToPortfolioManager {
|
||||
|
||||
// todo swap make updatable
|
||||
val token: TokenMarketParams
|
||||
val analyticsParams: AnalyticsParams?
|
||||
val portfolioFetcher: PortfolioFetcher
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -325,6 +325,7 @@ internal class EarnModel @Inject constructor(
|
|||
onNetworkFilterClick = ::onNetworkFilterClick,
|
||||
onTypeFilterClick = ::onTypeFilterClick,
|
||||
onScroll = ::onMostlyUsedScrolled,
|
||||
onSearchBarClicked = params.onSearchClicked,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -29,4 +29,6 @@ internal interface FeedModelClickIntents {
|
|||
fun onOpenAllNews()
|
||||
|
||||
fun onOpenEarnPage()
|
||||
|
||||
fun openSearch()
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.features.feed.model.market.list.state
|
||||
|
||||
internal data class SortByMenuUM(
|
||||
val selectedOption: SortByTypeUM,
|
||||
val onOptionClicked: (SortByTypeUM) -> Unit,
|
||||
)
|
||||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue