Updated on 2026-08-14
This commit is contained in:
commit
304bf0df87
928 changed files with 23076 additions and 29831 deletions
|
|
@ -15,7 +15,7 @@ interface AccountCreateEditComponent : ComposableContentComponent {
|
|||
) : Params
|
||||
|
||||
data class Edit(
|
||||
val account: Account,
|
||||
val account: Account.CryptoPortfolio,
|
||||
) : Params
|
||||
}
|
||||
}
|
||||
|
|
@ -7,5 +7,5 @@ import com.tangem.domain.models.account.Account
|
|||
interface AccountDetailsComponent : ComposableContentComponent {
|
||||
interface Factory : ComponentFactory<Params, AccountDetailsComponent>
|
||||
|
||||
data class Params(val account: Account)
|
||||
data class Params(val account: Account.CryptoPortfolio)
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.account.createedit
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.ui.account.AccountNameUM
|
||||
import com.tangem.common.ui.account.CryptoPortfolioIconConverter
|
||||
import com.tangem.common.ui.account.toDomain
|
||||
|
|
@ -25,7 +26,6 @@ import com.tangem.domain.account.usecase.GetUnoccupiedAccountIndexUseCase
|
|||
import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.account.derivationIndex
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.account.AccountCreateEditComponent
|
||||
import com.tangem.features.account.analytics.AccountSettingsAnalyticEvents
|
||||
|
|
@ -73,7 +73,7 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
when (params) {
|
||||
is AccountCreateEditComponent.Params.Create -> updateDerivationInfo(userWalletId = params.userWalletId)
|
||||
is AccountCreateEditComponent.Params.Edit -> {
|
||||
val derivationIndex = params.account.derivationIndex?.value
|
||||
val derivationIndex = params.account.derivationIndex.value
|
||||
val event = AccountSettingsAnalyticEvents.AccountEditScreenOpened(derivationIndex)
|
||||
analyticsEventHandler.send(event)
|
||||
}
|
||||
|
|
@ -135,10 +135,14 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
|
||||
result
|
||||
.onLeft { error -> handleAddAccountError(error, derivationIndex.value) }
|
||||
.onRight {
|
||||
.onRight { account ->
|
||||
analyticsEventHandler.send(WalletSettingsAccountAnalyticEvents.AccountCreated())
|
||||
showMessage(R.string.account_create_success_message)
|
||||
router.pop()
|
||||
val route = AppRoute.ManageTokens(
|
||||
source = AppRoute.ManageTokens.Source.ACCOUNT,
|
||||
accountId = account.accountId,
|
||||
)
|
||||
router.replaceCurrent(route)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -170,7 +174,7 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
val icon = CryptoPortfolioIconConverter.convertBack(state.account.portfolioIcon)
|
||||
val isNewName = name != params.account.accountName
|
||||
val isNewIcon = icon != params.account.portfolioIcon
|
||||
val derivationIndex = params.account.derivationIndex?.value
|
||||
val derivationIndex = params.account.derivationIndex.value
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonSave(name, icon, derivationIndex))
|
||||
|
||||
uiState.value = uiState.value.toggleProgress(showProgress = true)
|
||||
|
|
@ -182,7 +186,7 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
uiState.value = uiState.value.toggleProgress(showProgress = false)
|
||||
|
||||
result
|
||||
.onLeft { error -> handleEditAccountError(error, params.account.derivationIndex?.value) }
|
||||
.onLeft { error -> handleEditAccountError(error, params.account.derivationIndex.value) }
|
||||
.onRight {
|
||||
showMessage(R.string.account_edit_success_message)
|
||||
router.pop()
|
||||
|
|
|
|||
|
|
@ -14,12 +14,9 @@ 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.ToastMessage
|
||||
import com.tangem.domain.account.producer.SingleAccountProducer
|
||||
import com.tangem.domain.account.status.usecase.ArchiveCryptoPortfolioUseCase
|
||||
import com.tangem.domain.account.supplier.SingleAccountSupplier
|
||||
import com.tangem.domain.models.PortfolioId
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.derivationIndex
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.account.AccountDetailsComponent
|
||||
|
|
@ -54,29 +51,29 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
|
||||
init {
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.AccountSettingsScreenOpened())
|
||||
singleAccountSupplier(SingleAccountProducer.Params(accountId))
|
||||
singleAccountSupplier.filterCryptoPortfolioAccount(accountId)
|
||||
.onEach { account -> uiState.update { buildUI(account) } }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun onEditAccountClick(account: Account) {
|
||||
private fun onEditAccountClick(account: Account.CryptoPortfolio) {
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonEdit())
|
||||
router.push(AppRoute.EditAccount(account))
|
||||
}
|
||||
|
||||
private fun onManageTokensClick(account: Account) {
|
||||
private fun onManageTokensClick(account: Account.CryptoPortfolio) {
|
||||
val route = AppRoute.ManageTokens(
|
||||
source = AppRoute.ManageTokens.Source.ACCOUNT,
|
||||
portfolioId = PortfolioId(account.accountId),
|
||||
accountId = account.accountId,
|
||||
)
|
||||
analyticsEventHandler.send(
|
||||
AccountSettingsAnalyticEvents.ButtonManageTokens(account.derivationIndex?.value),
|
||||
AccountSettingsAnalyticEvents.ButtonManageTokens(account.derivationIndex.value),
|
||||
)
|
||||
router.push(route)
|
||||
}
|
||||
|
||||
private fun onArchiveAccountClick() {
|
||||
val accountDerivation = params.account.derivationIndex?.value
|
||||
val accountDerivation = params.account.derivationIndex.value
|
||||
val event = AccountSettingsAnalyticEvents.ButtonArchiveAccount(accountDerivation)
|
||||
analyticsEventHandler.send(event)
|
||||
confirmArchiveDialog()
|
||||
|
|
@ -86,7 +83,7 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
val secondAction = EventMessageAction(
|
||||
title = resourceReference(R.string.common_cancel),
|
||||
onClick = {
|
||||
val accountDerivation = params.account.derivationIndex?.value
|
||||
val accountDerivation = params.account.derivationIndex.value
|
||||
val event = AccountSettingsAnalyticEvents.ButtonCancelAccountArchivation(accountDerivation)
|
||||
analyticsEventHandler.send(event)
|
||||
},
|
||||
|
|
@ -107,7 +104,7 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun archiveCryptoPortfolio() = modelScope.launch {
|
||||
val accountDerivation = params.account.derivationIndex?.value
|
||||
val accountDerivation = params.account.derivationIndex.value
|
||||
val event = AccountSettingsAnalyticEvents.ButtonArchiveAccountConfirmation(accountDerivation)
|
||||
analyticsEventHandler.send(event)
|
||||
uiState.update { it.toggleProgress(true) }
|
||||
|
|
@ -128,7 +125,7 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
val event = AccountSettingsAnalyticEvents.AccountError(
|
||||
source = AccountSettingsAnalyticEvents.Source.ARCHIVE,
|
||||
error = error.tag,
|
||||
accountDerivation = params.account.derivationIndex?.value,
|
||||
accountDerivation = params.account.derivationIndex.value,
|
||||
)
|
||||
analyticsEventHandler.send(event)
|
||||
val titleRes: Int
|
||||
|
|
@ -155,16 +152,13 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
messageSender.send(dialogMessage)
|
||||
}
|
||||
|
||||
private fun buildUI(account: Account): AccountDetailsUM {
|
||||
val archiveMode = when (account) {
|
||||
is Account.CryptoPortfolio -> when (account.isMainAccount) {
|
||||
true -> ArchiveMode.None
|
||||
false -> ArchiveMode.Available(
|
||||
onArchiveAccountClick = ::onArchiveAccountClick,
|
||||
isLoading = false,
|
||||
)
|
||||
}
|
||||
is Account.Payment -> TODO("[REDACTED_JIRA]")
|
||||
private fun buildUI(account: Account.CryptoPortfolio): AccountDetailsUM {
|
||||
val archiveMode = when (account.isMainAccount) {
|
||||
true -> ArchiveMode.None
|
||||
false -> ArchiveMode.Available(
|
||||
onArchiveAccountClick = ::onArchiveAccountClick,
|
||||
isLoading = false,
|
||||
)
|
||||
}
|
||||
val isMultiCurrency = getUserWalletUseCase(account.accountId.userWalletId).getOrNull()
|
||||
?.isMultiCurrency == true
|
||||
|
|
|
|||
28
features/approval/api/build.gradle.kts
Normal file
28
features/approval/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.approval.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Core */
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.ui)
|
||||
|
||||
/** Other */
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.runtime)
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.features.approval.api
|
||||
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
interface GiveApprovalComponent : ComposableBottomSheetComponent {
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val amount: String,
|
||||
val spenderAddress: String,
|
||||
val subtitle: TextReference,
|
||||
val callback: Callback,
|
||||
)
|
||||
|
||||
interface Callback {
|
||||
fun onApproveClick()
|
||||
fun onApproveDone()
|
||||
fun onApproveFailed()
|
||||
fun onCancelClick()
|
||||
}
|
||||
|
||||
interface Factory {
|
||||
fun create(context: AppComponentContext, params: Params): GiveApprovalComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.features.approval.api
|
||||
|
||||
interface GiveApprovalFeatureToggles {
|
||||
|
||||
val isGaslessApprovalEnabled: Boolean
|
||||
}
|
||||
60
features/approval/impl/build.gradle.kts
Normal file
60
features/approval/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.approval.impl"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/** Feature */
|
||||
implementation(projects.features.approval.api)
|
||||
implementation(projects.features.sendV2.api)
|
||||
|
||||
/** Core */
|
||||
implementation(projects.core.configToggles)
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.analytics.models)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.ui)
|
||||
|
||||
/** SDK */
|
||||
implementation(tangemDeps.blockchain) {
|
||||
exclude(module = "joda-time")
|
||||
}
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.transaction.models)
|
||||
implementation(projects.domain.transaction)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.foundation)
|
||||
implementation(deps.compose.runtime)
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.ui)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
|
||||
/** Other */
|
||||
implementation(deps.decompose)
|
||||
implementation(deps.decompose.ext.compose)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.arrow.core)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
package com.tangem.features.approval.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.approval.api.GiveApprovalComponent
|
||||
import com.tangem.features.approval.impl.model.GiveApprovalModel
|
||||
import com.tangem.features.approval.impl.ui.GiveApprovalContent
|
||||
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.api.params.FeeSelectorParams
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import com.tangem.common.ui.R as CommonUiR
|
||||
|
||||
internal class DefaultGiveApprovalComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: GiveApprovalComponent.Params,
|
||||
feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory,
|
||||
) : GiveApprovalComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: GiveApprovalModel = getOrCreateModel(params = params)
|
||||
|
||||
private val feeSelectorBlockComponent = feeSelectorBlockComponentFactory.create(
|
||||
context = child("giveApprovalFeeSelector"),
|
||||
params = FeeSelectorParams.FeeSelectorBlockParams(
|
||||
state = FeeSelectorUM.Loading,
|
||||
onLoadFee = { model.loadFee() },
|
||||
onLoadFeeExtended = { selectedFeeToken -> model.loadFeeExtended(selectedFeeToken) },
|
||||
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
|
||||
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
|
||||
feeStateConfiguration = FeeSelectorParams.FeeStateConfiguration.None,
|
||||
feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen,
|
||||
analyticsCategoryName = CommonSendAnalyticEvents.APPROVE_CATEGORY,
|
||||
analyticsSendSource = CommonSendAnalyticEvents.CommonSendSource.Approve,
|
||||
userWalletId = params.userWalletId,
|
||||
),
|
||||
onResult = model::onFeeResult,
|
||||
)
|
||||
|
||||
private val currency: String = params.cryptoCurrencyStatus.currency.symbol
|
||||
|
||||
override fun dismiss() {
|
||||
params.callback.onCancelClick()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun BottomSheet() {
|
||||
val uiState by model.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
val config = remember {
|
||||
TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = ::dismiss,
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
|
||||
config = config,
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
titleText = resourceReference(CommonUiR.string.give_permission_title),
|
||||
titleAction = TopAppBarButtonUM.Icon(
|
||||
iconRes = CommonUiR.drawable.ic_information_24,
|
||||
onClicked = model::showPermissionInfoDialog,
|
||||
),
|
||||
) {
|
||||
GiveApprovalContent(
|
||||
currency = currency,
|
||||
subtitle = params.subtitle,
|
||||
approveType = uiState.approveType,
|
||||
approveItems = uiState.approveItems,
|
||||
onChangeApproveType = model::onChangeApproveType,
|
||||
walletInteractionIcon = uiState.walletInteractionIcon,
|
||||
isApproveEnabled = uiState.isApproveButtonEnabled,
|
||||
isApproveLoading = uiState.isApproveLoading,
|
||||
onApproveClick = model::onApproveClick,
|
||||
onCancelClick = model::onCancelClick,
|
||||
onOpenLearnMoreAboutApproveClick = model::onOpenLearnMoreAboutApproveClick,
|
||||
feeSelectorBlockComponent = feeSelectorBlockComponent,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : GiveApprovalComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: GiveApprovalComponent.Params,
|
||||
): DefaultGiveApprovalComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.features.approval.impl
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.features.approval.api.GiveApprovalFeatureToggles
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultGiveApprovalFeatureToggles @Inject constructor(
|
||||
private val featureToggles: FeatureTogglesManager,
|
||||
) : GiveApprovalFeatureToggles {
|
||||
|
||||
// Remove GiveTxPermissionBottomSheet and all dependencies with this toggle
|
||||
override val isGaslessApprovalEnabled: Boolean
|
||||
get() = featureToggles.isFeatureEnabled("GASLESS_APPROVAL_ENABLED")
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.features.approval.impl.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.approval.api.GiveApprovalComponent
|
||||
import com.tangem.features.approval.api.GiveApprovalFeatureToggles
|
||||
import com.tangem.features.approval.impl.DefaultGiveApprovalComponent
|
||||
import com.tangem.features.approval.impl.DefaultGiveApprovalFeatureToggles
|
||||
import com.tangem.features.approval.impl.model.GiveApprovalModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
import javax.inject.Singleton
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
internal interface GiveApprovalFeatureModule {
|
||||
|
||||
@Singleton
|
||||
@Binds
|
||||
fun bindGiveApprovalFeatureToggle(toggles: DefaultGiveApprovalFeatureToggles): GiveApprovalFeatureToggles
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindComponentFactory(factory: DefaultGiveApprovalComponent.Factory): GiveApprovalComponent.Factory
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface GiveApprovalModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(GiveApprovalModel::class)
|
||||
fun bindModel(model: GiveApprovalModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
package com.tangem.features.approval.impl.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.left
|
||||
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.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.transaction.models.TransactionFeeExtended
|
||||
import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
||||
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.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 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
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
internal class GiveApprovalModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
paramsContainer: ParamsContainer,
|
||||
private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase,
|
||||
private val sendTransactionUseCase: SendTransactionUseCase,
|
||||
private val getFeeUseCase: GetFeeUseCase,
|
||||
private val getFeeForGaslessUseCase: GetFeeForGaslessUseCase,
|
||||
private val getFeeForTokenUseCase: GetFeeForTokenUseCase,
|
||||
private val createAndSendGaslessTransactionUseCase: CreateAndSendGaslessTransactionUseCase,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model(), FeeSelectorModelCallback {
|
||||
|
||||
private val params: GiveApprovalComponent.Params = paramsContainer.require()
|
||||
|
||||
private val userWallet by lazy {
|
||||
requireNotNull(
|
||||
getUserWalletUseCase(params.userWalletId).getOrNull(),
|
||||
) { "No wallet found for id: $params.userWalletId" }
|
||||
}
|
||||
|
||||
val uiState: StateFlow<GiveApprovalUM>
|
||||
field = MutableStateFlow(
|
||||
GiveApprovalUM(
|
||||
approveType = ApproveType.LIMITED,
|
||||
walletInteractionIcon = walletInterationIcon(userWallet),
|
||||
isApproveButtonEnabled = false,
|
||||
isApproveLoading = false,
|
||||
),
|
||||
)
|
||||
|
||||
private var feeSelectorUM: FeeSelectorUM = FeeSelectorUM.Loading
|
||||
|
||||
override fun onFeeResult(feeSelectorUM: FeeSelectorUM) {
|
||||
this.feeSelectorUM = feeSelectorUM
|
||||
uiState.update { it.copy(isApproveButtonEnabled = feeSelectorUM.isPrimaryButtonEnabled) }
|
||||
}
|
||||
|
||||
fun onApproveClick() {
|
||||
params.callback.onApproveClick()
|
||||
uiState.update { it.copy(isApproveLoading = true) }
|
||||
modelScope.launch(dispatchers.main) {
|
||||
val isSuccess = sendApprovalTransaction()
|
||||
uiState.update { it.copy(isApproveLoading = false) }
|
||||
if (isSuccess) {
|
||||
params.callback.onApproveDone()
|
||||
} else {
|
||||
params.callback.onApproveFailed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onCancelClick() {
|
||||
params.callback.onCancelClick()
|
||||
}
|
||||
|
||||
fun onChangeApproveType(approveType: ApproveType) {
|
||||
uiState.update { it.copy(approveType = approveType) }
|
||||
}
|
||||
|
||||
fun onOpenLearnMoreAboutApproveClick() {
|
||||
urlOpener.openUrl(RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP)
|
||||
}
|
||||
|
||||
fun showPermissionInfoDialog() {
|
||||
uiMessageSender.send(
|
||||
DialogMessage(
|
||||
message = resourceReference(com.tangem.common.ui.R.string.give_permission_staking_footer),
|
||||
title = resourceReference(com.tangem.common.ui.R.string.common_approve),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun prepareApprovalTransaction(): Either<Throwable, TransactionData> {
|
||||
val cryptoCurrencyStatus = params.cryptoCurrencyStatus
|
||||
val tokenCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token
|
||||
?: return Either.Left(IllegalStateException("Currency is not a token"))
|
||||
|
||||
return createApprovalTransactionUseCase(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
userWalletId = params.userWalletId,
|
||||
amount = getApprovalAmount(),
|
||||
contractAddress = tokenCurrency.contractAddress,
|
||||
spenderAddress = params.spenderAddress,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun loadFee(): Either<GetFeeError, TransactionFee> {
|
||||
val approvalTransaction = prepareApprovalTransaction()
|
||||
.getOrElse { return GetFeeError.DataError(it).left() }
|
||||
|
||||
return getFeeUseCase(
|
||||
transactionData = approvalTransaction,
|
||||
userWallet = userWallet,
|
||||
network = params.cryptoCurrencyStatus.currency.network,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun loadFeeExtended(maybeToken: CryptoCurrencyStatus?): Either<GetFeeError, TransactionFeeExtended> {
|
||||
val approvalTransaction = prepareApprovalTransaction()
|
||||
.getOrElse { return GetFeeError.DataError(it).left() }
|
||||
|
||||
return if (maybeToken == null) {
|
||||
getFeeForGaslessUseCase(
|
||||
transactionData = approvalTransaction,
|
||||
userWallet = userWallet,
|
||||
network = params.cryptoCurrencyStatus.currency.network,
|
||||
)
|
||||
} else {
|
||||
getFeeForTokenUseCase(
|
||||
transactionData = approvalTransaction,
|
||||
userWallet = userWallet,
|
||||
token = maybeToken.currency,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun sendApprovalTransaction(): Boolean {
|
||||
val cryptoCurrencyStatus = params.cryptoCurrencyStatus
|
||||
val tokenCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token ?: return false
|
||||
|
||||
val feeContent = feeSelectorUM as? FeeSelectorUM.Content ?: return false
|
||||
val selectedFee = feeContent.selectedFeeItem.fee
|
||||
val feeExtended = feeContent.feeExtraInfo.transactionFeeExtended
|
||||
|
||||
val isFeeInTokenCurrency = feeExtended?.transactionFee?.normal is Fee.Ethereum.TokenCurrency
|
||||
|
||||
val transactionData = createApprovalTransactionUseCase(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
userWalletId = params.userWalletId,
|
||||
amount = getApprovalAmount(),
|
||||
fee = selectedFee,
|
||||
contractAddress = tokenCurrency.contractAddress,
|
||||
spenderAddress = params.spenderAddress,
|
||||
).getOrElse { error ->
|
||||
Timber.e(error, "Failed to create approval transaction")
|
||||
return false
|
||||
}
|
||||
|
||||
return if (isFeeInTokenCurrency) {
|
||||
createAndSendGaslessTransactionUseCase(
|
||||
userWallet = userWallet,
|
||||
transactionData = transactionData,
|
||||
fee = feeExtended,
|
||||
)
|
||||
} else {
|
||||
sendTransactionUseCase(
|
||||
txData = transactionData,
|
||||
userWallet = userWallet,
|
||||
network = tokenCurrency.network,
|
||||
)
|
||||
}.fold(
|
||||
ifLeft = { error ->
|
||||
Timber.e("Failed to send approval transaction: $error")
|
||||
false
|
||||
},
|
||||
ifRight = {
|
||||
sendApproveSuccessAnalytics(feeContent)
|
||||
true
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendApproveSuccessAnalytics(feeContent: FeeSelectorUM.Content) {
|
||||
val currency = params.cryptoCurrencyStatus.currency
|
||||
val feeToken = feeContent.feeExtraInfo.feeCryptoCurrencyStatus.currency.symbol
|
||||
val permissionType = when (uiState.value.approveType) {
|
||||
ApproveType.LIMITED -> "Current transaction"
|
||||
ApproveType.UNLIMITED -> "Unlimited"
|
||||
}
|
||||
val event = AnalyticsParam.TxSentFrom.Approve(
|
||||
blockchain = currency.network.name,
|
||||
token = currency.symbol,
|
||||
feeType = feeContent.toAnalyticType(),
|
||||
feeToken = feeToken,
|
||||
permissionType = permissionType,
|
||||
)
|
||||
analyticsEventHandler.send(
|
||||
Basic.TransactionSent(
|
||||
sentFrom = event,
|
||||
memoType = Basic.TransactionSent.MemoType.Null,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getApprovalAmount(): BigDecimal? {
|
||||
return if (uiState.value.approveType == ApproveType.LIMITED) {
|
||||
params.amount.toBigDecimalOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.features.approval.impl.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
internal data class GiveApprovalUM(
|
||||
val approveType: ApproveType,
|
||||
val approveItems: ImmutableList<ApproveType> = ApproveType.entries.toImmutableList(),
|
||||
@DrawableRes val walletInteractionIcon: Int?,
|
||||
val isApproveButtonEnabled: Boolean,
|
||||
val isApproveLoading: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,353 @@
|
|||
package com.tangem.features.approval.impl.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.withLink
|
||||
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.DpOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.containers.FooterContainer
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import com.tangem.common.ui.R as CommonUiR
|
||||
|
||||
@Composable
|
||||
@Suppress("LongParameterList")
|
||||
internal fun GiveApprovalContent(
|
||||
currency: String,
|
||||
subtitle: TextReference,
|
||||
approveType: ApproveType,
|
||||
approveItems: ImmutableList<ApproveType>,
|
||||
onChangeApproveType: (ApproveType) -> Unit,
|
||||
walletInteractionIcon: Int?,
|
||||
isApproveEnabled: Boolean,
|
||||
isApproveLoading: Boolean,
|
||||
onApproveClick: () -> Unit,
|
||||
onCancelClick: () -> Unit,
|
||||
onOpenLearnMoreAboutApproveClick: () -> Unit,
|
||||
feeSelectorBlockComponent: FeeSelectorBlockComponent,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(color = TangemTheme.colors.background.secondary)
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = subtitle.resolveReference(),
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
style = TangemTheme.typography.body2,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing24),
|
||||
)
|
||||
|
||||
SpacerH16()
|
||||
|
||||
ApprovalInfo(
|
||||
currency = currency,
|
||||
approveType = approveType,
|
||||
approveItems = approveItems,
|
||||
onChangeApproveType = onChangeApproveType,
|
||||
onOpenLearnMoreAboutApproveClick = onOpenLearnMoreAboutApproveClick,
|
||||
feeSelectorBlockComponent = feeSelectorBlockComponent,
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
SpacerH12()
|
||||
|
||||
SecondaryButton(
|
||||
text = stringResourceSafe(id = CommonUiR.string.common_cancel),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
onClick = onCancelClick,
|
||||
)
|
||||
|
||||
SpacerH16()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
private fun ApprovalInfo(
|
||||
currency: String,
|
||||
approveType: ApproveType,
|
||||
approveItems: ImmutableList<ApproveType>,
|
||||
onChangeApproveType: (ApproveType) -> Unit,
|
||||
onOpenLearnMoreAboutApproveClick: () -> Unit,
|
||||
feeSelectorBlockComponent: FeeSelectorBlockComponent,
|
||||
) {
|
||||
FooterContainer(
|
||||
footer = annotatedReference {
|
||||
append(stringResourceSafe(CommonUiR.string.swap_approve_description))
|
||||
append(" ")
|
||||
withLink(
|
||||
link = LinkAnnotation.Clickable(
|
||||
tag = "APPROVE_TAG",
|
||||
linkInteractionListener = { onOpenLearnMoreAboutApproveClick() },
|
||||
),
|
||||
block = {
|
||||
appendColored(
|
||||
text = stringResourceSafe(CommonUiR.string.common_learn_more),
|
||||
color = TangemTheme.colors.text.accent,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
AmountItem(
|
||||
currency = currency,
|
||||
approveType = approveType,
|
||||
onChangeApproveType = onChangeApproveType,
|
||||
approveItems = approveItems,
|
||||
)
|
||||
}
|
||||
SpacerH16()
|
||||
FooterContainer(
|
||||
footer = resourceReference(CommonUiR.string.give_permission_policy_type_footer),
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
feeSelectorBlockComponent.Content(
|
||||
modifier = Modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AmountItem(
|
||||
currency: String,
|
||||
approveType: ApproveType,
|
||||
approveItems: ImmutableList<ApproveType>,
|
||||
onChangeApproveType: (ApproveType) -> Unit,
|
||||
) {
|
||||
var isExpandSelector by remember { mutableStateOf(false) }
|
||||
var amountSize by remember { mutableStateOf(IntSize.Zero) }
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(),
|
||||
onClick = { isExpandSelector = true },
|
||||
),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.onSizeChanged { amountSize = it }
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing16,
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(id = CommonUiR.string.give_permission_rows_amount, currency),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
maxLines = 1,
|
||||
)
|
||||
SpacerWMax()
|
||||
Text(
|
||||
text = approveType.text.resolveReference(),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body1,
|
||||
maxLines = 1,
|
||||
)
|
||||
Icon(
|
||||
painter = rememberVectorPainter(ImageVector.vectorResource(id = CommonUiR.drawable.ic_chevron_24)),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
modifier = Modifier.padding(start = TangemTheme.dimens.spacing2),
|
||||
)
|
||||
}
|
||||
DropdownSelector(
|
||||
isExpanded = isExpandSelector,
|
||||
onDismiss = { isExpandSelector = false },
|
||||
onItemClick = { type ->
|
||||
isExpandSelector = false
|
||||
onChangeApproveType(type)
|
||||
},
|
||||
items = approveItems,
|
||||
selectedType = approveType,
|
||||
amountSize = amountSize,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
private fun DropdownSelector(
|
||||
isExpanded: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onItemClick: (ApproveType) -> Unit,
|
||||
items: ImmutableList<ApproveType>,
|
||||
selectedType: ApproveType,
|
||||
amountSize: IntSize,
|
||||
) {
|
||||
var dropDownWidth by remember { mutableStateOf(IntSize.Zero) }
|
||||
val offsetY = amountSize.height.times(-1)
|
||||
val offsetX = amountSize.width - dropDownWidth.width
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = MaterialTheme.colorScheme.copy(surface = TangemTheme.colors.background.action),
|
||||
shapes = MaterialTheme.shapes.copy(extraSmall = RoundedCornerShape(TangemTheme.dimens.radius16)),
|
||||
) {
|
||||
DropdownMenu(
|
||||
expanded = isExpanded,
|
||||
onDismissRequest = onDismiss,
|
||||
properties = PopupProperties(clippingEnabled = false),
|
||||
offset = with(LocalDensity.current) {
|
||||
DpOffset(x = offsetX.toDp(), y = offsetY.toDp())
|
||||
},
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.onSizeChanged { dropDownWidth = it },
|
||||
) {
|
||||
items.forEach { item ->
|
||||
val color = if (item == selectedType) TangemTheme.colors.icon.accent else Color.Transparent
|
||||
|
||||
DropdownMenuItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = {
|
||||
Row {
|
||||
Text(
|
||||
text = when (item) {
|
||||
ApproveType.LIMITED -> stringResourceSafe(
|
||||
id = CommonUiR.string.give_permission_current_transaction,
|
||||
)
|
||||
ApproveType.UNLIMITED -> stringResourceSafe(
|
||||
id = CommonUiR.string.give_permission_unlimited,
|
||||
)
|
||||
},
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.body1,
|
||||
maxLines = 1,
|
||||
)
|
||||
SpacerWMax()
|
||||
Icon(
|
||||
painter = rememberVectorPainter(
|
||||
image = ImageVector.vectorResource(id = CommonUiR.drawable.ic_check_24),
|
||||
),
|
||||
tint = color,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(start = TangemTheme.dimens.size20),
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
onItemClick.invoke(item)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun GiveApprovalContentPreview(
|
||||
@PreviewParameter(GiveApprovalContentPreviewProvider::class) params: GiveApprovalPreviewParams,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
GiveApprovalContent(
|
||||
currency = params.currency,
|
||||
subtitle = params.subtitle,
|
||||
approveType = params.approveType,
|
||||
approveItems = params.approveItems,
|
||||
onChangeApproveType = {},
|
||||
walletInteractionIcon = params.walletInteractionIcon,
|
||||
isApproveEnabled = params.isApproveEnabled,
|
||||
isApproveLoading = params.isApproveLoading,
|
||||
onApproveClick = {},
|
||||
onCancelClick = {},
|
||||
onOpenLearnMoreAboutApproveClick = {},
|
||||
feeSelectorBlockComponent = PreviewFeeSelectorBlockComponent(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class GiveApprovalPreviewParams(
|
||||
val currency: String,
|
||||
val subtitle: TextReference,
|
||||
val approveType: ApproveType,
|
||||
val approveItems: ImmutableList<ApproveType>,
|
||||
val walletInteractionIcon: Int?,
|
||||
val isApproveEnabled: Boolean,
|
||||
val isApproveLoading: Boolean,
|
||||
)
|
||||
|
||||
private class GiveApprovalContentPreviewProvider : PreviewParameterProvider<GiveApprovalPreviewParams> {
|
||||
override val values: Sequence<GiveApprovalPreviewParams>
|
||||
get() = sequenceOf(
|
||||
GiveApprovalPreviewParams(
|
||||
currency = "USDT",
|
||||
subtitle = stringReference("Allow this app to access your USDT"),
|
||||
approveType = ApproveType.LIMITED,
|
||||
approveItems = persistentListOf(ApproveType.LIMITED, ApproveType.UNLIMITED),
|
||||
walletInteractionIcon = CommonUiR.drawable.ic_tangem_24,
|
||||
isApproveEnabled = true,
|
||||
isApproveLoading = false,
|
||||
),
|
||||
GiveApprovalPreviewParams(
|
||||
currency = "USDC",
|
||||
subtitle = stringReference("Allow this app to access your USDC"),
|
||||
approveType = ApproveType.UNLIMITED,
|
||||
approveItems = persistentListOf(ApproveType.LIMITED, ApproveType.UNLIMITED),
|
||||
walletInteractionIcon = CommonUiR.drawable.ic_tangem_24,
|
||||
isApproveEnabled = false,
|
||||
isApproveLoading = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.features.approval.impl.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
|
||||
internal class PreviewFeeSelectorBlockComponent : FeeSelectorBlockComponent {
|
||||
override fun updateState(feeSelectorUM: FeeSelectorUM) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.features.feed.entry.featuretoggle
|
||||
|
||||
interface FeedFeatureToggle {
|
||||
val isFeedEnabled: Boolean
|
||||
val isEarnBlockEnabled: Boolean
|
||||
}
|
||||
|
|
@ -42,6 +42,7 @@ dependencies {
|
|||
implementation(projects.domain.feedback.models)
|
||||
implementation(projects.domain.manageTokens)
|
||||
implementation(projects.domain.markets)
|
||||
implementation(projects.domain.offramp)
|
||||
implementation(projects.domain.onramp.models)
|
||||
implementation(projects.domain.staking.models)
|
||||
implementation(projects.domain.tokens)
|
||||
|
|
@ -56,12 +57,6 @@ dependencies {
|
|||
implementation(projects.domain.yieldSupply)
|
||||
implementation(projects.domain.earn)
|
||||
|
||||
// FIXME [REDACTED_TASK_KEY]
|
||||
// Remove the "Buy" and "Sell" actions from the redux middleware.
|
||||
// Instead, create some kind of interface for such cases.
|
||||
/* Redux -_- */
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(deps.reKotlin)
|
||||
|
||||
/* Compose */
|
||||
implementation(deps.compose.coil)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import com.tangem.core.analytics.models.AnalyticsParam
|
|||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.features.feed.components.earn.DefaultEarnComponent
|
||||
import com.tangem.features.feed.components.feed.DefaultFeedComponent
|
||||
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
|
||||
|
|
@ -20,7 +19,6 @@ import javax.inject.Inject
|
|||
|
||||
internal class FeedEntryChildFactory @Inject constructor(
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
private val portfolioComponentFactory: MarketsPortfolioComponent.Factory,
|
||||
private val addToPortfolioPreselectedDataComponent: AddToPortfolioPreselectedDataComponent.Factory,
|
||||
) {
|
||||
|
|
@ -66,7 +64,6 @@ internal class FeedEntryChildFactory @Inject constructor(
|
|||
appComponentContext = appComponentContext,
|
||||
params = child.params,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
accountsFeatureToggles = accountsFeatureToggles,
|
||||
portfolioComponentFactory = portfolioComponentFactory,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ 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.res.LocalMainBottomSheetColor
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
|
|
@ -33,7 +32,6 @@ internal class DefaultMarketsTokenDetailsComponent(
|
|||
appComponentContext: AppComponentContext,
|
||||
val params: Params,
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
portfolioComponentFactory: MarketsPortfolioComponent.Factory,
|
||||
) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
|
|
@ -117,7 +115,6 @@ internal class DefaultMarketsTokenDetailsComponent(
|
|||
modifier = modifier,
|
||||
backgroundColor = LocalMainBottomSheetColor.current.value,
|
||||
state = state,
|
||||
isAccountEnabled = accountsFeatureToggles.isFeatureEnabled,
|
||||
portfolioBlock = portfolioComponent?.let { component ->
|
||||
{ blockModifier ->
|
||||
component.Content(blockModifier)
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@ internal class DefaultMarketsPortfolioComponent @AssistedInject constructor(
|
|||
bottomSheet.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
@Suppress("UnsafeCallOnNullableType")
|
||||
private fun bottomSheetChild(
|
||||
config: MarketsPortfolioRoute,
|
||||
componentContext: ComponentContext,
|
||||
|
|
@ -66,7 +65,7 @@ internal class DefaultMarketsPortfolioComponent @AssistedInject constructor(
|
|||
MarketsPortfolioRoute.AddToPortfolio -> addToPortfolioComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = AddToPortfolioComponent.Params(
|
||||
addToPortfolioManager = model.newAddToPortfolioManager!!,
|
||||
addToPortfolioManager = model.addToPortfolioManager,
|
||||
callback = model.addToPortfolioCallback,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,135 +0,0 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.loader
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.core.lce.Lce
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tokens.GetAllWalletsCryptoCurrencyStatusesUseCase
|
||||
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
|
||||
import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase
|
||||
import com.tangem.domain.tokens.error.TokenListError
|
||||
import com.tangem.domain.yield.supply.models.YieldSupplyAvailability
|
||||
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetAvailabilityUseCase
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.*
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Loader of portfolio data
|
||||
*
|
||||
* @property getAllWalletsCryptoCurrencyStatusesUseCase use case for getting all wallets crypto currency statuses
|
||||
* @property getSelectedAppCurrencyUseCase use case for getting selected app currency
|
||||
* @property getBalanceHidingSettingsUseCase use case for getting balance hiding settings
|
||||
* @property getWalletTotalBalanceUseCase use case for getting wallet total balance
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class PortfolioDataLoader @Inject constructor(
|
||||
private val getAllWalletsCryptoCurrencyStatusesUseCase: GetAllWalletsCryptoCurrencyStatusesUseCase,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase,
|
||||
private val yieldSupplyGetAvailabilityUseCase: YieldSupplyGetAvailabilityUseCase,
|
||||
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase,
|
||||
) {
|
||||
|
||||
/** Load data by [currencyRawId] */
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun load(currencyRawId: CryptoCurrency.RawID): Flow<PortfolioData> {
|
||||
return combine(
|
||||
flow = getAllWalletsCryptoCurrenciesData(currencyRawId = currencyRawId),
|
||||
flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(),
|
||||
flow3 = getBalanceHidingSettingsUseCase.isBalanceHidden(),
|
||||
) { walletsWithCurrencies, appCurrency, isBalanceHidden ->
|
||||
PortfolioData(
|
||||
walletsWithCurrencies = walletsWithCurrencies,
|
||||
appCurrency = appCurrency,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
walletsWithBalance = emptyMap(),
|
||||
)
|
||||
}
|
||||
// setup balances for wallets from walletsWithCurrencyStatuses
|
||||
.flatMapLatest { portfolioData ->
|
||||
getWalletsWithTotalBalanceFlow(
|
||||
ids = portfolioData.walletsWithCurrencies.keys.map(UserWallet::walletId),
|
||||
)
|
||||
.map { portfolioData.copy(walletsWithBalance = it) }
|
||||
.onEmpty { emit(portfolioData) }
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun getAllWalletsCryptoCurrenciesData(
|
||||
currencyRawId: CryptoCurrency.RawID,
|
||||
): Flow<Map<UserWallet, List<PortfolioData.CryptoCurrencyData>>> {
|
||||
return getAllWalletsCryptoCurrencyStatusesUseCase(currencyRawId)
|
||||
.distinctUntilChanged()
|
||||
.map { walletsWithMaybeStatuses ->
|
||||
walletsWithMaybeStatuses.mapValues { entry ->
|
||||
entry.value.mapNotNull { it.getOrNull() }
|
||||
}
|
||||
}
|
||||
.flatMapLatest { walletsWithStatuses ->
|
||||
val actionsFlows = walletsWithStatuses.flatMap { (wallet, statuses) ->
|
||||
statuses.map { status ->
|
||||
val yieldSupplyAvailability = yieldSupplyGetAvailabilityUseCase(status.currency).getOrElse {
|
||||
YieldSupplyAvailability.Unavailable
|
||||
}
|
||||
getCryptoCurrencyActionsUseCase(wallet, status, yieldSupplyAvailability)
|
||||
.map { tokenActionsState ->
|
||||
PortfolioData.CryptoCurrencyData(
|
||||
userWallet = wallet,
|
||||
status = status,
|
||||
actions = tokenActionsState.states,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
combine(actionsFlows) { actions ->
|
||||
walletsWithStatuses.mapValues { entry ->
|
||||
entry.value.mapNotNull { status ->
|
||||
actions.firstOrNull {
|
||||
it.userWallet == entry.key && it.status == status
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onEmpty {
|
||||
emit(
|
||||
walletsWithStatuses.mapValues { (wallet, statuses) ->
|
||||
statuses.map { status ->
|
||||
PortfolioData.CryptoCurrencyData(
|
||||
userWallet = wallet,
|
||||
status = status,
|
||||
actions = emptyList(),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}.onEmpty {
|
||||
emit(emptyMap())
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
private fun getWalletsWithTotalBalanceFlow(
|
||||
ids: List<UserWalletId>,
|
||||
): Flow<Map<UserWalletId, Lce<TokenListError, TotalFiatBalance>>> {
|
||||
return combine(
|
||||
flows = ids
|
||||
.map { userWalletId ->
|
||||
getWalletTotalBalanceUseCase(userWalletId)
|
||||
.map { userWalletId to it }
|
||||
.distinctUntilChanged()
|
||||
},
|
||||
transform = { it.toMap() },
|
||||
)
|
||||
.distinctUntilChanged()
|
||||
.onEmpty { ids.associateWith { Lce.Loading<TotalFiatBalance>(partialContent = null) } }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,161 +0,0 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.model
|
||||
|
||||
import com.tangem.common.ui.userwallet.converter.UserWalletItemUMConverter
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.AddToPortfolioBSContentUM
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.WalletSelectorBSContentUM
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* Factory to create AddToPortfolio bottom sheet content [TangemBottomSheetConfig]
|
||||
*
|
||||
* @property token token params
|
||||
* @property onAddToPortfolioVisibilityChange callback is invoked when add to portfolio visibility is changed
|
||||
* @property onWalletSelectorVisibilityChange callback is invoked when wallet selector visibility is changed
|
||||
* @property onNetworkSwitchClick callback is invoked when network switch is clicked
|
||||
* @property onAnotherWalletSelect callback is invoked when wallet is selected
|
||||
* @property onContinueClick callback is invoked when continue button is clicked
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
internal class AddToPortfolioBSContentUMFactory(
|
||||
private val addToPortfolioManager: AddToPortfolioManager,
|
||||
private val token: TokenMarketParams,
|
||||
private val onAddToPortfolioVisibilityChange: (Boolean) -> Unit,
|
||||
private val onWalletSelectorVisibilityChange: (Boolean) -> Unit,
|
||||
private val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit,
|
||||
private val onAnotherWalletSelect: (UserWalletId) -> Unit,
|
||||
private val onContinueClick: (selectedWalletId: UserWalletId, addedNetworks: Set<TokenMarketInfo.Network>) -> Unit,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Create [TangemBottomSheetConfig]
|
||||
*
|
||||
|
||||
* @param portfolioData portfolio data
|
||||
* @param portfolioUIData portfolio bottom sheet visibility model
|
||||
* @param selectedWallet selected wallet
|
||||
* @param alreadyAddedNetworks already added networks
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
fun create(
|
||||
currentState: TangemBottomSheetConfig?,
|
||||
portfolioData: PortfolioData,
|
||||
portfolioUIData: PortfolioUIData,
|
||||
selectedWallet: UserWallet?,
|
||||
alreadyAddedNetworks: Set<String>?,
|
||||
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
|
||||
): TangemBottomSheetConfig {
|
||||
return (currentState ?: TangemBottomSheetConfig.Empty).copy(
|
||||
isShown = portfolioUIData.portfolioBSVisibilityModel.isAddToPortfolioBSVisible,
|
||||
onDismissRequest = { onAddToPortfolioVisibilityChange(false) },
|
||||
content = if (selectedWallet != null && alreadyAddedNetworks != null) {
|
||||
AddToPortfolioBSContentUM(
|
||||
selectedWallet = selectedWallet.toSelectedUserWalletItemUM(
|
||||
portfolioData = portfolioData,
|
||||
balance = portfolioData.walletsWithBalance[selectedWallet.walletId]?.getOrNull(),
|
||||
artwork = artworks[selectedWallet.walletId],
|
||||
),
|
||||
selectNetworkUM = SelectNetworkUMConverter(
|
||||
networksWithToggle = addToPortfolioManager.associateWithToggle(
|
||||
userWalletId = selectedWallet.walletId,
|
||||
alreadyAddedNetworkIds = alreadyAddedNetworks,
|
||||
addToPortfolioData = portfolioUIData.addToPortfolioData,
|
||||
),
|
||||
alreadyAddedNetworks = alreadyAddedNetworks,
|
||||
onNetworkSwitchClick = onNetworkSwitchClick,
|
||||
).convert(value = token),
|
||||
isScanCardNotificationVisible = portfolioUIData.isNeededColdWalletInteraction,
|
||||
isContinueButtonEnabled = portfolioUIData.addToPortfolioData.isUserAddedNetworks(
|
||||
userWalletId = selectedWallet.walletId,
|
||||
),
|
||||
onContinueButtonClick = {
|
||||
onContinueClick(
|
||||
selectedWallet.walletId,
|
||||
portfolioUIData.addToPortfolioData.getAddedNetworks(
|
||||
userWalletId = selectedWallet.walletId,
|
||||
alreadyAddedNetworkIds = alreadyAddedNetworks,
|
||||
),
|
||||
)
|
||||
},
|
||||
walletSelectorConfig = createWalletSelectorBSConfig(
|
||||
isShow = portfolioUIData.portfolioBSVisibilityModel.isWalletSelectorBSVisible,
|
||||
portfolioData = portfolioData,
|
||||
selectedWalletId = selectedWallet.walletId,
|
||||
artworks = artworks,
|
||||
),
|
||||
isWalletBlockVisible = portfolioData.walletsWithCurrencies
|
||||
.filterKeys(UserWallet::isMultiCurrency).size > 1,
|
||||
)
|
||||
} else {
|
||||
TangemBottomSheetConfigContent.Empty
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun UserWallet.toSelectedUserWalletItemUM(
|
||||
artwork: UserWalletItemUM.ImageState? = null,
|
||||
portfolioData: PortfolioData,
|
||||
balance: TotalFiatBalance?,
|
||||
): UserWalletItemUM {
|
||||
return UserWalletItemUMConverter(
|
||||
onClick = { onWalletSelectorVisibilityChange(true) },
|
||||
endIcon = UserWalletItemUM.EndIcon.Arrow,
|
||||
balance = balance,
|
||||
artwork = artwork,
|
||||
appCurrency = portfolioData.appCurrency,
|
||||
isBalanceHidden = portfolioData.isBalanceHidden,
|
||||
).convert(value = this)
|
||||
}
|
||||
|
||||
private fun createWalletSelectorBSConfig(
|
||||
isShow: Boolean,
|
||||
portfolioData: PortfolioData,
|
||||
selectedWalletId: UserWalletId,
|
||||
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
|
||||
): TangemBottomSheetConfig {
|
||||
return TangemBottomSheetConfig(
|
||||
isShown = isShow,
|
||||
onDismissRequest = { onWalletSelectorVisibilityChange(false) },
|
||||
content = WalletSelectorBSContentUM(
|
||||
userWallets = portfolioData.walletsWithCurrencies
|
||||
.filterKeys(UserWallet::isMultiCurrency)
|
||||
.map { it.key }
|
||||
.map { userWallet ->
|
||||
val balance = portfolioData.walletsWithBalance[userWallet.walletId]
|
||||
|
||||
UserWalletItemUMConverter(
|
||||
onClick = { id ->
|
||||
if (id != selectedWalletId) {
|
||||
onAnotherWalletSelect(id)
|
||||
onWalletSelectorVisibilityChange(false)
|
||||
}
|
||||
},
|
||||
appCurrency = portfolioData.appCurrency,
|
||||
balance = balance?.getOrNull(),
|
||||
isBalanceHidden = portfolioData.isBalanceHidden,
|
||||
endIcon = if (userWallet.walletId == selectedWalletId) {
|
||||
UserWalletItemUM.EndIcon.Checkmark
|
||||
} else {
|
||||
UserWalletItemUM.EndIcon.None
|
||||
},
|
||||
artwork = artworks[userWallet.walletId],
|
||||
).convert(userWallet)
|
||||
}
|
||||
.toImmutableList(),
|
||||
onBack = { onWalletSelectorVisibilityChange(false) },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,193 +0,0 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.model
|
||||
|
||||
import com.tangem.domain.markets.FilterAvailableNetworksForWalletUseCase
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.update
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.firstOrNull
|
||||
import kotlin.collections.orEmpty
|
||||
import kotlin.collections.set
|
||||
|
||||
internal typealias WalletsWithNetworks = Map<UserWalletId, Set<TokenMarketInfo.Network>>
|
||||
|
||||
/**
|
||||
* Manager for tracking changing networks in AddToPortfolio
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class AddToPortfolioManager @Inject constructor(
|
||||
private val filterAvailableNetworksForWalletUseCase: FilterAvailableNetworksForWalletUseCase,
|
||||
) {
|
||||
|
||||
val availableNetworks = MutableStateFlow<Set<TokenMarketInfo.Network>?>(value = null)
|
||||
private val addedNetworks = MutableStateFlow<WalletsWithNetworks>(value = emptyMap())
|
||||
private val removedNetworks = MutableStateFlow<WalletsWithNetworks>(value = emptyMap())
|
||||
|
||||
/** Get [AddToPortfolioData] as flow */
|
||||
fun getAddToPortfolioData(): Flow<AddToPortfolioData> {
|
||||
return combine(
|
||||
flow = availableNetworks,
|
||||
flow2 = addedNetworks,
|
||||
flow3 = removedNetworks,
|
||||
transform = ::AddToPortfolioData,
|
||||
)
|
||||
}
|
||||
|
||||
/** Set available networks [networks] */
|
||||
fun setAvailableNetworks(networks: List<TokenMarketInfo.Network>) {
|
||||
availableNetworks.value = networks.toSet()
|
||||
}
|
||||
|
||||
/** Add network [networkId] to [userWalletId] */
|
||||
fun addNetwork(userWalletId: UserWalletId, networkId: String) {
|
||||
addedNetworks.add(userWalletId, networkId)
|
||||
|
||||
removedNetworks.cancelPrevChangeIfExist(userWalletId = userWalletId, networkId = networkId)
|
||||
}
|
||||
|
||||
/** Remove network [networkId] from [userWalletId] */
|
||||
fun removeNetwork(userWalletId: UserWalletId, networkId: String) {
|
||||
removedNetworks.add(userWalletId, networkId)
|
||||
|
||||
addedNetworks.cancelPrevChangeIfExist(
|
||||
userWalletId = userWalletId,
|
||||
networkId = networkId,
|
||||
)
|
||||
}
|
||||
|
||||
/** Remove all networks by [userWalletId] */
|
||||
fun removeAllChanges(userWalletId: UserWalletId) {
|
||||
addedNetworks.update {
|
||||
it.toMutableMap().apply { remove(userWalletId) }
|
||||
}
|
||||
|
||||
removedNetworks.update {
|
||||
it.toMutableMap().apply { remove(userWalletId) }
|
||||
}
|
||||
}
|
||||
|
||||
fun associateWithToggle(
|
||||
userWalletId: UserWalletId,
|
||||
alreadyAddedNetworkIds: Set<String>,
|
||||
addToPortfolioData: AddToPortfolioData,
|
||||
): Map<TokenMarketInfo.Network, Boolean> {
|
||||
val filteredNetworks = filterAvailableNetworksForWalletUseCase(
|
||||
userWalletId = userWalletId,
|
||||
networks = addToPortfolioData.availableNetworks.orEmpty(),
|
||||
)
|
||||
// Use user choice or check already added networks
|
||||
return filteredNetworks.associateWith { availableNetwork ->
|
||||
val isAddedByUser = addToPortfolioData.addedNetworks[userWalletId]?.contains(availableNetwork)
|
||||
|
||||
if (isAddedByUser == true) return@associateWith true
|
||||
|
||||
val isRemovedByUser = addToPortfolioData.removedNetworks[userWalletId]?.contains(availableNetwork)
|
||||
|
||||
if (isRemovedByUser == true) return@associateWith false
|
||||
|
||||
val isAddedBefore = alreadyAddedNetworkIds.any { it == availableNetwork.networkId }
|
||||
|
||||
isAddedBefore
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableStateFlow<WalletsWithNetworks>.cancelPrevChangeIfExist(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: String,
|
||||
) {
|
||||
if (value[userWalletId].orEmpty().any { it.networkId == networkId }) remove(userWalletId, networkId)
|
||||
}
|
||||
|
||||
private fun MutableStateFlow<WalletsWithNetworks>.add(userWalletId: UserWalletId, networkId: String) {
|
||||
change(userWalletId = userWalletId, networkId = networkId, isAddAction = true)
|
||||
}
|
||||
|
||||
private fun MutableStateFlow<WalletsWithNetworks>.remove(userWalletId: UserWalletId, networkId: String) {
|
||||
change(userWalletId = userWalletId, networkId = networkId, isAddAction = false)
|
||||
}
|
||||
|
||||
private fun MutableStateFlow<WalletsWithNetworks>.change(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: String,
|
||||
isAddAction: Boolean,
|
||||
) {
|
||||
val network = availableNetworks.value.orEmpty().firstOrNull { it.networkId == networkId }
|
||||
|
||||
if (network == null) {
|
||||
Timber.d(
|
||||
"Network [$networkId] doesn't contain in available networks [%s]",
|
||||
availableNetworks.value?.joinToString { it.networkId },
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
update { walletsWithNetworks ->
|
||||
walletsWithNetworks.toMutableMap().apply {
|
||||
this[userWalletId] = if (isAddAction) {
|
||||
this[userWalletId].orEmpty() + network
|
||||
} else {
|
||||
this[userWalletId].orEmpty() - network
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add to portfolio data
|
||||
*
|
||||
* @property availableNetworks available networks that user can add to portfolio
|
||||
* @property addedNetworks networks that user toggled on, but it might have already been added to the wallet
|
||||
* @property removedNetworks networks that user toggled off, but it might haven't been added to the wallet
|
||||
*
|
||||
* Example for [addedNetworks] and [removedNetworks]. This lists will include new networks when user just
|
||||
* toggle it. But when we will save user changes, we will check what tokens have already been added or
|
||||
* haven't been added to the wallet. See [getAddedNetworks] and [getRemovedNetworks]
|
||||
*/
|
||||
data class AddToPortfolioData(
|
||||
val availableNetworks: Set<TokenMarketInfo.Network>?,
|
||||
val addedNetworks: WalletsWithNetworks,
|
||||
val removedNetworks: WalletsWithNetworks,
|
||||
) {
|
||||
|
||||
fun isUserAddedNetworks(userWalletId: UserWalletId): Boolean {
|
||||
return addedNetworks[userWalletId].orEmpty().isNotEmpty()
|
||||
}
|
||||
|
||||
fun isUserChangedNetworks(userWalletId: UserWalletId): Boolean {
|
||||
return addedNetworks[userWalletId].orEmpty().isNotEmpty() ||
|
||||
removedNetworks[userWalletId].orEmpty().isNotEmpty()
|
||||
}
|
||||
|
||||
/** Get new networks that user [userWalletId] added using [alreadyAddedNetworkIds] */
|
||||
fun getAddedNetworks(
|
||||
userWalletId: UserWalletId,
|
||||
alreadyAddedNetworkIds: Set<String>,
|
||||
): Set<TokenMarketInfo.Network> {
|
||||
val addedNetworksByUser = addedNetworks[userWalletId].orEmpty()
|
||||
|
||||
return addedNetworksByUser.map { it.networkId }
|
||||
.minus(alreadyAddedNetworkIds)
|
||||
.mapNotNull { networkId -> addedNetworksByUser.firstOrNull { it.networkId == networkId } }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
/** Get networks that user [userWalletId] removed using [alreadyAddedNetworkIds] */
|
||||
fun getRemovedNetworks(
|
||||
userWalletId: UserWalletId,
|
||||
alreadyAddedNetworkIds: Set<String>,
|
||||
): Set<TokenMarketInfo.Network> {
|
||||
val removedNetworksByUser = removedNetworks[userWalletId].orEmpty()
|
||||
|
||||
return alreadyAddedNetworkIds
|
||||
.minus(removedNetworksByUser.map { it.networkId }.toSet())
|
||||
.mapNotNull { networkId -> removedNetworksByUser.firstOrNull { it.networkId == networkId } }
|
||||
.toSet()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network
|
|||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
import com.tangem.common.ui.account.CryptoPortfolioIconConverter
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.account.models.AccountStatusList
|
||||
|
|
@ -45,7 +44,7 @@ import kotlinx.coroutines.flow.*
|
|||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Suppress("LongParameterList")
|
||||
internal class NewMarketsPortfolioDelegate @AssistedInject constructor(
|
||||
internal class MarketsPortfolioDelegate @AssistedInject constructor(
|
||||
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
private val allAccountSupplier: MultiAccountStatusListSupplier,
|
||||
|
|
@ -106,10 +105,7 @@ internal class NewMarketsPortfolioDelegate @AssistedInject constructor(
|
|||
private fun addFirstTokenFlow(): Flow<MyPortfolioUM> = buttonState.map { state ->
|
||||
when (state) {
|
||||
MyPortfolioUM.Tokens.AddButtonState.Loading -> MyPortfolioUM.Loading
|
||||
MyPortfolioUM.Tokens.AddButtonState.Available -> MyPortfolioUM.AddFirstToken(
|
||||
onAddClick = onAddClick,
|
||||
addToPortfolioBSConfig = TangemBottomSheetConfig.Empty,
|
||||
)
|
||||
MyPortfolioUM.Tokens.AddButtonState.Available -> MyPortfolioUM.AddFirstToken(onAddClick = onAddClick)
|
||||
MyPortfolioUM.Tokens.AddButtonState.Unavailable -> MyPortfolioUM.Unavailable
|
||||
}
|
||||
}
|
||||
|
|
@ -314,7 +310,7 @@ internal class NewMarketsPortfolioDelegate @AssistedInject constructor(
|
|||
tokenActionsHandler: TokenActionsHandler,
|
||||
buttonState: Flow<MyPortfolioUM.Tokens.AddButtonState>,
|
||||
onAddClick: () -> Unit,
|
||||
): NewMarketsPortfolioDelegate
|
||||
): MarketsPortfolioDelegate
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -5,91 +5,56 @@ import arrow.core.getOrElse
|
|||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase
|
||||
import com.tangem.domain.managetokens.model.CurrencyUnsupportedState
|
||||
import com.tangem.domain.markets.SaveMarketTokensUseCase
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.transaction.usecase.ReceiveAddressesFactory
|
||||
import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager
|
||||
import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.analytics.PortfolioAnalyticsEvent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioDataLoader
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.MyPortfolioUM
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.wallet.utils.UserWalletImageFetcher
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.operations.attestation.ArtworkSize
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager as NewAddToPortfolioManager
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
@Suppress("LongParameterList")
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class MarketsPortfolioModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
tokenActionsIntentsFactory: TokenActionsHandler.Factory,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase,
|
||||
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
|
||||
private val portfolioDataLoader: PortfolioDataLoader,
|
||||
private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase,
|
||||
private val saveMarketTokensUseCase: SaveMarketTokensUseCase,
|
||||
private val addToPortfolioManager: AddToPortfolioManager,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val userWalletImageFetcher: UserWalletImageFetcher,
|
||||
private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory,
|
||||
private val marketsPortfolioDelegateFactory: MarketsPortfolioDelegate.Factory,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val tokenActionsHandlerFactory: TokenActionsHandler.Factory,
|
||||
private val receiveAddressesFactory: ReceiveAddressesFactory,
|
||||
accountsFeatureToggles: AccountsFeatureToggles,
|
||||
newAddToPortfolioManagerFactory: NewAddToPortfolioManager.Factory,
|
||||
newMarketsPortfolioDelegateFactory: NewMarketsPortfolioDelegate.Factory,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
) : Model() {
|
||||
|
||||
private val _state: MutableStateFlow<MyPortfolioUM> = MutableStateFlow(value = MyPortfolioUM.Loading)
|
||||
val state: StateFlow<MyPortfolioUM> get() = _state
|
||||
val state: StateFlow<MyPortfolioUM>
|
||||
field = MutableStateFlow<MyPortfolioUM>(value = MyPortfolioUM.Loading)
|
||||
|
||||
private val params = paramsContainer.require<MarketsPortfolioComponent.Params>()
|
||||
|
||||
private val analyticsEventBuilder = PortfolioAnalyticsEvent.EventBuilder(
|
||||
tokenSymbol = params.token.symbol,
|
||||
source = params.analyticsParams?.source,
|
||||
)
|
||||
|
||||
val newAddToPortfolioManager: NewAddToPortfolioManager?
|
||||
val newMarketsPortfolioDelegate: NewMarketsPortfolioDelegate?
|
||||
private val currentAppCurrency = createAppCurrencyFlow()
|
||||
private val tokenActionsHandler = createTokenActionsHandler()
|
||||
|
||||
/** Multi-wallet [UserWalletId] that user uses to add new tokens in AddToPortfolio bottom sheet */
|
||||
private val selectedMultiWalletIdFlow = MutableStateFlow<UserWalletId?>(value = null)
|
||||
|
||||
private val portfolioBSVisibilityModelFlow = MutableStateFlow(value = PortfolioBSVisibilityModel())
|
||||
val addToPortfolioManager: AddToPortfolioManager = createAddToPortfolioManager()
|
||||
private val marketsPortfolioDelegate: MarketsPortfolioDelegate = createMarketsPortfolioDelegate()
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<MarketsPortfolioRoute> = SlotNavigation()
|
||||
val addToPortfolioCallback = object : AddToPortfolioComponent.Callback {
|
||||
|
|
@ -97,321 +62,78 @@ internal class MarketsPortfolioModel @Inject constructor(
|
|||
override fun onSuccess(addedToken: CryptoCurrency) = bottomSheetNavigation.dismiss()
|
||||
}
|
||||
|
||||
private val currentAppCurrency = getSelectedAppCurrencyUseCase()
|
||||
.map { maybeAppCurrency ->
|
||||
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
||||
}
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = AppCurrency.Default,
|
||||
)
|
||||
|
||||
private val tokenActionsHandler = tokenActionsIntentsFactory.create(
|
||||
currentAppCurrency = Provider { currentAppCurrency.value },
|
||||
onHandleQuickAction = { handledAction ->
|
||||
analyticsEventHandler.send(
|
||||
analyticsEventBuilder.quickActionClick(
|
||||
actionUM = handledAction.action,
|
||||
blockchainName = handledAction
|
||||
.cryptoCurrencyData
|
||||
.status
|
||||
.currency
|
||||
.network
|
||||
.name,
|
||||
),
|
||||
)
|
||||
configureReceiveAddresses(handledAction)
|
||||
},
|
||||
)
|
||||
|
||||
private val factory = MyPortfolioUMFactory(
|
||||
onAddClick = {
|
||||
onAddToPortfolioBSVisibilityChange(isShow = true)
|
||||
// === Analytics ===
|
||||
analyticsEventHandler.send(
|
||||
analyticsEventBuilder.addToPortfolioClicked(),
|
||||
)
|
||||
},
|
||||
addToPortfolioBSContentUMFactory = AddToPortfolioBSContentUMFactory(
|
||||
addToPortfolioManager = addToPortfolioManager,
|
||||
token = params.token,
|
||||
onAddToPortfolioVisibilityChange = ::onAddToPortfolioBSVisibilityChange,
|
||||
onWalletSelectorVisibilityChange = ::onWalletSelectorVisibilityChange,
|
||||
onNetworkSwitchClick = ::onNetworkSwitchClick,
|
||||
onAnotherWalletSelect = { walletId ->
|
||||
onWalletSelect(walletId)
|
||||
// === Analytics ===
|
||||
analyticsEventHandler.send(
|
||||
analyticsEventBuilder.addToPortfolioWalletChanged(),
|
||||
)
|
||||
},
|
||||
onContinueClick = { selectedWalletId, addedNetworks ->
|
||||
onContinueClick(selectedWalletId, addedNetworks)
|
||||
|
||||
// === Analytics ===
|
||||
analyticsEventHandler.send(
|
||||
analyticsEventBuilder.addToPortfolioContinue(
|
||||
blockchainNames = addedNetworks.mapNotNull {
|
||||
BlockchainUtils.getNetworkInfo(it.networkId)?.name
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
currentState = Provider { _state.value },
|
||||
tokenActionsHandler = tokenActionsHandler,
|
||||
updateTokens = { updateBlock ->
|
||||
updateTokensState { state ->
|
||||
state.copy(tokens = updateBlock(state.tokens))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
init {
|
||||
if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
newAddToPortfolioManager = newAddToPortfolioManagerFactory
|
||||
.create(
|
||||
modelScope,
|
||||
params.token,
|
||||
params.analyticsParams?.source?.let { NewAddToPortfolioManager.AnalyticsParams(it) },
|
||||
)
|
||||
newMarketsPortfolioDelegate = newMarketsPortfolioDelegateFactory.create(
|
||||
scope = modelScope,
|
||||
token = params.token,
|
||||
tokenActionsHandler = tokenActionsHandler,
|
||||
buttonState = newAddToPortfolioManager.state.map { state ->
|
||||
when (state) {
|
||||
is NewAddToPortfolioManager.State.AvailableToAdd -> {
|
||||
MyPortfolioUM.Tokens.AddButtonState.Available
|
||||
}
|
||||
NewAddToPortfolioManager.State.Init -> MyPortfolioUM.Tokens.AddButtonState.Loading
|
||||
NewAddToPortfolioManager.State.NothingToAdd -> MyPortfolioUM.Tokens.AddButtonState.Unavailable
|
||||
}
|
||||
},
|
||||
onAddClick = {
|
||||
analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioClicked())
|
||||
bottomSheetNavigation.activate(MarketsPortfolioRoute.AddToPortfolio)
|
||||
},
|
||||
)
|
||||
newMarketsPortfolioDelegate.combineData()
|
||||
.onEach { _state.value = it }
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(modelScope)
|
||||
} else {
|
||||
newAddToPortfolioManager = null
|
||||
newMarketsPortfolioDelegate = null
|
||||
// Subscribe on selected wallet flow to support actual selected wallet
|
||||
subscribeOnSelectedMultiWalletUpdates()
|
||||
|
||||
subscribeOnStateUpdates()
|
||||
}
|
||||
marketsPortfolioDelegate.combineData()
|
||||
.onEach { state.value = it }
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) {
|
||||
addToPortfolioManager.setAvailableNetworks(networks)
|
||||
newAddToPortfolioManager?.setTokenNetworks(networks)
|
||||
newMarketsPortfolioDelegate?.setTokenNetworks(networks)
|
||||
addToPortfolioManager.setTokenNetworks(networks)
|
||||
marketsPortfolioDelegate.setTokenNetworks(networks)
|
||||
}
|
||||
|
||||
fun setNoNetworksAvailable() {
|
||||
addToPortfolioManager.setAvailableNetworks(emptyList())
|
||||
newAddToPortfolioManager?.setTokenNetworks(emptyList())
|
||||
newMarketsPortfolioDelegate?.setTokenNetworks(emptyList())
|
||||
addToPortfolioManager.setTokenNetworks(emptyList())
|
||||
marketsPortfolioDelegate.setTokenNetworks(emptyList())
|
||||
}
|
||||
|
||||
private fun subscribeOnSelectedMultiWalletUpdates() {
|
||||
getSelectedWalletUseCase()
|
||||
.getOrElse { e ->
|
||||
Timber.e("Failed to load selected wallet: $e")
|
||||
error("Failed to load selected wallet")
|
||||
}
|
||||
.onEach { userWallet ->
|
||||
selectedMultiWalletIdFlow.value = userWallet.takeIf { it.isMultiCurrency }?.walletId
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeOnStateUpdates() {
|
||||
combine(
|
||||
flow = loadPortfolioDataWithArtworks(params.token.id),
|
||||
flow2 = getPortfolioUIDataFlow(),
|
||||
transform = { pair, portfolioUIData ->
|
||||
val (portfolioData, artworks) = pair
|
||||
factory.create(portfolioData, portfolioUIData, artworks)
|
||||
},
|
||||
)
|
||||
.onEach { _state.value = it }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun loadPortfolioDataWithArtworks(
|
||||
currencyRawId: CryptoCurrency.RawID,
|
||||
): Flow<Pair<PortfolioData, Map<UserWalletId, UserWalletItemUM.ImageState>>> {
|
||||
val wallets = Channel<Set<UserWallet>>()
|
||||
val portfolioFlow = portfolioDataLoader
|
||||
.load(currencyRawId)
|
||||
.onEach { wallets.trySend(it.walletsWithCurrencies.keys) }
|
||||
|
||||
val artworksFlow = wallets.receiveAsFlow()
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { userWalletImageFetcher.walletsImage(wallets = it, size = ArtworkSize.SMALL) }
|
||||
|
||||
return combine(
|
||||
flow = portfolioFlow,
|
||||
flow2 = artworksFlow,
|
||||
) { portfolioData, artworks -> portfolioData to artworks }
|
||||
}
|
||||
|
||||
private fun getPortfolioUIDataFlow(): Flow<PortfolioUIData> {
|
||||
return combine(
|
||||
flow = portfolioBSVisibilityModelFlow,
|
||||
flow2 = selectedMultiWalletIdFlow,
|
||||
flow3 = addToPortfolioManager.getAddToPortfolioData(),
|
||||
transform = { portfolioBSVisibilityModel, selectedWalletId, addToPortfolioData ->
|
||||
PortfolioUIData(
|
||||
portfolioBSVisibilityModel = portfolioBSVisibilityModel,
|
||||
selectedWalletId = selectedWalletId,
|
||||
addToPortfolioData = addToPortfolioData,
|
||||
isNeededColdWalletInteraction = isNeededColdWalletInteraction(selectedWalletId, addToPortfolioData),
|
||||
)
|
||||
},
|
||||
private fun createAddToPortfolioManager(): AddToPortfolioManager {
|
||||
return addToPortfolioManagerFactory.create(
|
||||
scope = modelScope,
|
||||
token = params.token,
|
||||
analyticsParams = params.analyticsParams?.source?.let { AddToPortfolioManager.AnalyticsParams(it) },
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun isNeededColdWalletInteraction(
|
||||
selectedWalletId: UserWalletId?,
|
||||
addToPortfolioData: AddToPortfolioManager.AddToPortfolioData,
|
||||
): Boolean {
|
||||
return if (selectedWalletId != null) {
|
||||
coldWalletAndHasMissedDerivationsUseCase.invoke(
|
||||
userWalletId = selectedWalletId,
|
||||
networksWithDerivationPath = addToPortfolioData.addedNetworks[selectedWalletId].orEmpty()
|
||||
.associate { it.networkId to null },
|
||||
)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun onNetworkSwitchClick(blockchainRowUM: BlockchainRowUM, isChecked: Boolean) {
|
||||
val selectedWalletId = selectedMultiWalletIdFlow.value
|
||||
|
||||
if (selectedWalletId == null) {
|
||||
Timber.e("Impossible to switch network when selected wallet is null")
|
||||
return
|
||||
}
|
||||
|
||||
if (isChecked) {
|
||||
modelScope.launch {
|
||||
val unsupportedState = checkCurrencyUnsupportedState(
|
||||
userWalletId = selectedWalletId,
|
||||
rawNetworkId = blockchainRowUM.id,
|
||||
isMainNetwork = blockchainRowUM.isMainNetwork,
|
||||
)
|
||||
if (unsupportedState != null) {
|
||||
showUnsupportedWarning(unsupportedState)
|
||||
} else {
|
||||
addToPortfolioManager.addNetwork(userWalletId = selectedWalletId, networkId = blockchainRowUM.id)
|
||||
private fun createMarketsPortfolioDelegate(): MarketsPortfolioDelegate {
|
||||
return marketsPortfolioDelegateFactory.create(
|
||||
scope = modelScope,
|
||||
token = params.token,
|
||||
tokenActionsHandler = tokenActionsHandler,
|
||||
buttonState = addToPortfolioManager.state.map { state ->
|
||||
when (state) {
|
||||
is AddToPortfolioManager.State.AvailableToAdd -> {
|
||||
MyPortfolioUM.Tokens.AddButtonState.Available
|
||||
}
|
||||
AddToPortfolioManager.State.Init -> MyPortfolioUM.Tokens.AddButtonState.Loading
|
||||
AddToPortfolioManager.State.NothingToAdd -> MyPortfolioUM.Tokens.AddButtonState.Unavailable
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addToPortfolioManager.removeNetwork(userWalletId = selectedWalletId, networkId = blockchainRowUM.id)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkCurrencyUnsupportedState(
|
||||
userWalletId: UserWalletId,
|
||||
rawNetworkId: String,
|
||||
isMainNetwork: Boolean,
|
||||
): CurrencyUnsupportedState? {
|
||||
return checkCurrencyUnsupportedUseCase(
|
||||
userWalletId = userWalletId,
|
||||
networkId = rawNetworkId,
|
||||
isMainNetwork = isMainNetwork,
|
||||
).getOrElse { throwable ->
|
||||
Timber.e(
|
||||
throwable,
|
||||
"""
|
||||
Failed to check currency unsupported state
|
||||
|- User wallet ID: $userWalletId
|
||||
|- Network ID: $rawNetworkId
|
||||
|- Is main network: $isMainNetwork
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
val message = SnackbarMessage(
|
||||
message = throwable.localizedMessage
|
||||
?.let(::stringReference)
|
||||
?: resourceReference(R.string.common_error),
|
||||
)
|
||||
messageSender.send(message)
|
||||
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun showUnsupportedWarning(unsupportedState: CurrencyUnsupportedState) {
|
||||
val message = DialogMessage(
|
||||
message = when (unsupportedState) {
|
||||
is CurrencyUnsupportedState.Token.NetworkTokensUnsupported -> resourceReference(
|
||||
id = R.string.alert_manage_tokens_unsupported_message,
|
||||
formatArgs = wrappedList(unsupportedState.networkName),
|
||||
)
|
||||
is CurrencyUnsupportedState.Token.UnsupportedCurve -> resourceReference(
|
||||
id = R.string.alert_manage_tokens_unsupported_curve_message,
|
||||
formatArgs = wrappedList(unsupportedState.networkName),
|
||||
)
|
||||
is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference(
|
||||
id = R.string.alert_manage_tokens_unsupported_curve_message,
|
||||
formatArgs = wrappedList(unsupportedState.networkName),
|
||||
)
|
||||
},
|
||||
onAddClick = {
|
||||
analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioClicked())
|
||||
bottomSheetNavigation.activate(MarketsPortfolioRoute.AddToPortfolio)
|
||||
},
|
||||
)
|
||||
|
||||
messageSender.send(message)
|
||||
}
|
||||
|
||||
private fun onWalletSelect(userWalletId: UserWalletId) {
|
||||
selectedMultiWalletIdFlow.update { prevUserWalletId ->
|
||||
prevUserWalletId?.let(addToPortfolioManager::removeAllChanges)
|
||||
|
||||
userWalletId
|
||||
}
|
||||
}
|
||||
|
||||
private fun onContinueClick(userWalletId: UserWalletId, addedNetworks: Set<TokenMarketInfo.Network>) {
|
||||
modelScope.launch {
|
||||
saveMarketTokensUseCase(
|
||||
userWalletId = userWalletId,
|
||||
tokenMarketParams = params.token,
|
||||
addedNetworks = addedNetworks,
|
||||
removedNetworks = emptySet(),
|
||||
private fun createAppCurrencyFlow(): StateFlow<AppCurrency> {
|
||||
return getSelectedAppCurrencyUseCase()
|
||||
.map { maybeAppCurrency ->
|
||||
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
||||
}
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = AppCurrency.Default,
|
||||
)
|
||||
|
||||
onAddToPortfolioBSVisibilityChange(isShow = false)
|
||||
|
||||
addToPortfolioManager.removeAllChanges(userWalletId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAddToPortfolioBSVisibilityChange(isShow: Boolean) {
|
||||
portfolioBSVisibilityModelFlow.update {
|
||||
it.copy(isAddToPortfolioBSVisible = isShow, isWalletSelectorBSVisible = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onWalletSelectorVisibilityChange(isShow: Boolean) {
|
||||
portfolioBSVisibilityModelFlow.update {
|
||||
it.copy(isAddToPortfolioBSVisible = true, isWalletSelectorBSVisible = isShow)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTokensState(block: (MyPortfolioUM.Tokens) -> MyPortfolioUM) {
|
||||
_state.update { stateToUpdate ->
|
||||
val tokensState = stateToUpdate as? MyPortfolioUM.Tokens ?: return@update stateToUpdate
|
||||
block(tokensState)
|
||||
}
|
||||
private fun createTokenActionsHandler(): TokenActionsHandler {
|
||||
return tokenActionsHandlerFactory.create(
|
||||
currentAppCurrency = Provider { currentAppCurrency.value },
|
||||
onHandleQuickAction = { handledAction ->
|
||||
val currency = handledAction.cryptoCurrencyData.status.currency
|
||||
analyticsEventHandler.send(
|
||||
analyticsEventBuilder.quickActionClick(
|
||||
actionUM = handledAction.action,
|
||||
blockchainName = currency.network.name,
|
||||
),
|
||||
)
|
||||
configureReceiveAddresses(handledAction)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun configureReceiveAddresses(quickAction: TokenActionsHandler.HandledQuickAction) {
|
||||
|
|
|
|||
|
|
@ -1,153 +0,0 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.model
|
||||
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.MyPortfolioUM
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.PortfolioTokenUM
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
/**
|
||||
* Factory for creating [MyPortfolioUM]
|
||||
*
|
||||
* @property onAddClick callback when user wants to add new token
|
||||
* @property addToPortfolioBSContentUMFactory factory for creating add to portfolio bottom sheet content
|
||||
* @property tokenActionsHandler token actions handler
|
||||
* @property currentState current state provider
|
||||
* @property updateTokens callback for updating tokens
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class MyPortfolioUMFactory(
|
||||
private val onAddClick: () -> Unit,
|
||||
private val addToPortfolioBSContentUMFactory: AddToPortfolioBSContentUMFactory,
|
||||
private val tokenActionsHandler: TokenActionsHandler,
|
||||
private val currentState: Provider<MyPortfolioUM>,
|
||||
private val updateTokens: ((ImmutableList<PortfolioTokenUM>) -> ImmutableList<PortfolioTokenUM>) -> Unit,
|
||||
) {
|
||||
|
||||
fun create(
|
||||
portfolioData: PortfolioData,
|
||||
portfolioUIData: PortfolioUIData,
|
||||
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
|
||||
): MyPortfolioUM {
|
||||
val addToPortfolioData = portfolioUIData.addToPortfolioData
|
||||
|
||||
val isOnlyUnavailableNetworks = addToPortfolioData.availableNetworks?.isEmpty() == true
|
||||
if (isOnlyUnavailableNetworks) return MyPortfolioUM.Unavailable
|
||||
|
||||
val walletsWithCurrencies = if (addToPortfolioData.availableNetworks == null) {
|
||||
portfolioData.walletsWithCurrencies
|
||||
} else {
|
||||
portfolioData.walletsWithCurrencies.filterAvailableNetworks(networks = addToPortfolioData.availableNetworks)
|
||||
}
|
||||
|
||||
val isPortfolioEmpty = walletsWithCurrencies.flatMap { it.value }.isEmpty()
|
||||
if (isPortfolioEmpty) {
|
||||
val hasMultiWallets = walletsWithCurrencies.filterKeys(UserWallet::isMultiCurrency).isNotEmpty()
|
||||
|
||||
return if (hasMultiWallets) {
|
||||
MyPortfolioUM.AddFirstToken(
|
||||
addToPortfolioBSConfig = createAddToPortfolioBSConfig(
|
||||
portfolioData = portfolioData,
|
||||
portfolioUIData = portfolioUIData,
|
||||
artworks = artworks,
|
||||
),
|
||||
onAddClick = onAddClick,
|
||||
)
|
||||
} else {
|
||||
MyPortfolioUM.UnavailableForWallet
|
||||
}
|
||||
}
|
||||
|
||||
return TokensPortfolioUMConverter(
|
||||
appCurrency = portfolioData.appCurrency,
|
||||
isBalanceHidden = portfolioData.isBalanceHidden,
|
||||
addButtonState = walletsWithCurrencies.getAddButtonState(
|
||||
availableNetworks = addToPortfolioData.availableNetworks,
|
||||
),
|
||||
bsConfig = createAddToPortfolioBSConfig(
|
||||
portfolioData = portfolioData,
|
||||
portfolioUIData = portfolioUIData,
|
||||
artworks = artworks,
|
||||
),
|
||||
onAddClick = onAddClick,
|
||||
quickActionsIntents = tokenActionsHandler,
|
||||
currentState = currentState,
|
||||
updateTokens = updateTokens,
|
||||
)
|
||||
.convert(walletsWithCurrencies)
|
||||
}
|
||||
|
||||
private fun createAddToPortfolioBSConfig(
|
||||
portfolioData: PortfolioData,
|
||||
portfolioUIData: PortfolioUIData,
|
||||
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
|
||||
): TangemBottomSheetConfig {
|
||||
val selectedWallet = portfolioData.walletsWithCurrencies.keys
|
||||
.firstOrNull { it.walletId == portfolioUIData.selectedWalletId }
|
||||
?: portfolioData.walletsWithCurrencies.keys.firstOrNull { it.isMultiCurrency }
|
||||
|
||||
val availableNetworks = portfolioUIData.addToPortfolioData.availableNetworks.orEmpty()
|
||||
|
||||
val alreadyAddedNetworks = portfolioData.walletsWithCurrencies
|
||||
.filterAvailableNetworks(availableNetworks)[selectedWallet]
|
||||
?.filter { !it.status.currency.isCustom }
|
||||
?.map { it.status.currency.network.backendId }
|
||||
?.toSet()
|
||||
|
||||
return addToPortfolioBSContentUMFactory.create(
|
||||
currentState = currentState().addToPortfolioBSConfig,
|
||||
portfolioData = portfolioData,
|
||||
portfolioUIData = portfolioUIData,
|
||||
selectedWallet = selectedWallet,
|
||||
alreadyAddedNetworks = alreadyAddedNetworks,
|
||||
artworks = artworks,
|
||||
)
|
||||
}
|
||||
|
||||
private fun Map<UserWallet, List<PortfolioData.CryptoCurrencyData>>.getAddButtonState(
|
||||
availableNetworks: Set<TokenMarketInfo.Network>?,
|
||||
): MyPortfolioUM.Tokens.AddButtonState {
|
||||
if (availableNetworks == null) return MyPortfolioUM.Tokens.AddButtonState.Loading
|
||||
|
||||
val networkIds = availableNetworks.map { it.networkId }
|
||||
|
||||
val isAllAvailableNetworksAdded = this
|
||||
// User can add currencies only in multi-currency wallets
|
||||
.filterKeys(UserWallet::isMultiCurrency)
|
||||
.mapValues { entry -> entry.value.map { it.status.currency.network.backendId } }
|
||||
// Each wallets contains all available networks?
|
||||
.all { it.value.containsAll(networkIds) }
|
||||
|
||||
return if (isAllAvailableNetworksAdded) {
|
||||
MyPortfolioUM.Tokens.AddButtonState.Unavailable
|
||||
} else {
|
||||
MyPortfolioUM.Tokens.AddButtonState.Available
|
||||
}
|
||||
}
|
||||
|
||||
/** Filter map values by available networks [networks] */
|
||||
private fun Map<UserWallet, List<PortfolioData.CryptoCurrencyData>>.filterAvailableNetworks(
|
||||
networks: Set<TokenMarketInfo.Network>,
|
||||
): Map<UserWallet, List<PortfolioData.CryptoCurrencyData>> {
|
||||
return mapValues { entry -> entry.value.filterAvailableNetworks(networks) }
|
||||
}
|
||||
|
||||
/** Filter list of [CryptoCurrencyStatus] by available networks [networks] */
|
||||
private fun List<PortfolioData.CryptoCurrencyData>.filterAvailableNetworks(
|
||||
networks: Set<TokenMarketInfo.Network>,
|
||||
): List<PortfolioData.CryptoCurrencyData> {
|
||||
val networkIds = networks.map(TokenMarketInfo.Network::networkId)
|
||||
|
||||
return mapNotNull { cryptoCurrencyData ->
|
||||
cryptoCurrencyData.takeIf { networkIds.contains(it.status.currency.network.backendId) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.model
|
||||
|
||||
/**
|
||||
* Model for portfolio bottom sheet visibility
|
||||
*
|
||||
* @property isAddToPortfolioBSVisible visibility of add to portfolio bottom sheet
|
||||
* @property isWalletSelectorBSVisible visibility of wallet selector bottom sheet
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal data class PortfolioBSVisibilityModel(
|
||||
val isAddToPortfolioBSVisible: Boolean = false,
|
||||
val isWalletSelectorBSVisible: Boolean = false,
|
||||
)
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.model
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
/**
|
||||
* Portfolio UI data. Combined data from all UI flows that required to setup portfolio
|
||||
*
|
||||
* @property portfolioBSVisibilityModel portfolio bottom sheet visibility model
|
||||
* @property selectedWalletId selected wallet id
|
||||
* @property addToPortfolioData add to portfolio data
|
||||
* @property isNeededColdWalletInteraction flag that indicates if user has missed derivations and has a cold wallet
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal data class PortfolioUIData(
|
||||
val portfolioBSVisibilityModel: PortfolioBSVisibilityModel,
|
||||
val selectedWalletId: UserWalletId?,
|
||||
val addToPortfolioData: AddToPortfolioManager.AddToPortfolioData,
|
||||
val isNeededColdWalletInteraction: Boolean,
|
||||
)
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.model
|
||||
|
||||
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.SelectNetworkUM
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* Converter from [TokenMarketParams] to [SelectNetworkUM]
|
||||
*
|
||||
* @property networksWithToggle map of networks with toggles
|
||||
* @property alreadyAddedNetworks already added networks
|
||||
* @property onNetworkSwitchClick callback is called when network switch is clicked
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class SelectNetworkUMConverter(
|
||||
private val networksWithToggle: Map<TokenMarketInfo.Network, Boolean>,
|
||||
private val alreadyAddedNetworks: Set<String>,
|
||||
private val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit,
|
||||
) : Converter<TokenMarketParams, SelectNetworkUM> {
|
||||
|
||||
override fun convert(value: TokenMarketParams): SelectNetworkUM {
|
||||
return SelectNetworkUM(
|
||||
tokenId = value.id.value,
|
||||
iconUrl = value.imageUrl,
|
||||
tokenName = value.name,
|
||||
tokenCurrencySymbol = value.symbol,
|
||||
networks = BlockchainRowUMConverter(alreadyAddedNetworks)
|
||||
.convertList(networksWithToggle.toList())
|
||||
.toImmutableList(),
|
||||
onNetworkSwitchClick = { um, isChecked -> onNetworkSwitchClick(um, isChecked) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,9 +2,12 @@ package com.tangem.features.feed.components.market.details.portfolio.impl.model
|
|||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent
|
||||
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.clipboard.ClipboardManager
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
|
|
@ -12,9 +15,8 @@ import com.tangem.core.ui.message.SnackbarMessage
|
|||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.offramp.GetOfframpUrlUseCase
|
||||
import com.tangem.domain.onramp.model.OnrampSource
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.domain.tokens.model.TokenActionsState
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM
|
||||
|
|
@ -30,7 +32,9 @@ internal class TokenActionsHandler @AssistedInject constructor(
|
|||
private val router: Router,
|
||||
private val clipboardManager: ClipboardManager,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val reduxStateHolder: ReduxStateHolder,
|
||||
private val getOfframpUrlUseCase: GetOfframpUrlUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
@Assisted private val currentAppCurrency: Provider<AppCurrency>,
|
||||
@Assisted private val onHandleQuickAction: (HandledQuickAction) -> Unit,
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
|
|
@ -104,12 +108,13 @@ internal class TokenActionsHandler @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun onSellClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
|
||||
reduxStateHolder.dispatch(
|
||||
TradeCryptoAction.Sell(
|
||||
cryptoCurrencyStatus = cryptoCurrencyData.status,
|
||||
appCurrencyCode = currentAppCurrency().code,
|
||||
),
|
||||
)
|
||||
getOfframpUrlUseCase(
|
||||
cryptoCurrencyStatus = cryptoCurrencyData.status,
|
||||
appCurrencyCode = currentAppCurrency().code,
|
||||
).onRight { url ->
|
||||
urlOpener.openUrl(url)
|
||||
analyticsEventHandler.send(OfframpAnalyticsEvent.ScreenOpened)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onExchangeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
|
||||
|
|
|
|||
|
|
@ -1,112 +0,0 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.model
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.MyPortfolioUM
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.PortfolioTokenUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.extensions.isZero
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* Converter from [Map] of [UserWallet] and [CryptoCurrencyStatus] to [MyPortfolioUM.Tokens]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
internal class TokensPortfolioUMConverter(
|
||||
private val appCurrency: AppCurrency,
|
||||
private val isBalanceHidden: Boolean,
|
||||
private val addButtonState: MyPortfolioUM.Tokens.AddButtonState,
|
||||
private val bsConfig: TangemBottomSheetConfig,
|
||||
private val onAddClick: () -> Unit,
|
||||
private val quickActionsIntents: TokenActionsHandler,
|
||||
private val currentState: Provider<MyPortfolioUM>,
|
||||
private val updateTokens: ((ImmutableList<PortfolioTokenUM>) -> ImmutableList<PortfolioTokenUM>) -> Unit,
|
||||
) : Converter<Map<UserWallet, List<PortfolioData.CryptoCurrencyData>>, MyPortfolioUM.Tokens> {
|
||||
|
||||
override fun convert(value: Map<UserWallet, List<PortfolioData.CryptoCurrencyData>>): MyPortfolioUM.Tokens {
|
||||
val currentTokensState = currentState() as? MyPortfolioUM.Tokens
|
||||
|
||||
return MyPortfolioUM.Tokens(
|
||||
tokens = value
|
||||
.flatMap { entry -> entry.value }
|
||||
.map { cryptoData ->
|
||||
PortfolioTokenUMConverter(
|
||||
appCurrency = appCurrency,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
onTokenItemClick = { toggleQuickActions(cryptoData) },
|
||||
tokenActionsHandler = quickActionsIntents,
|
||||
).convert(value = cryptoData) to cryptoData
|
||||
}
|
||||
.setQuickActionsVisibility(currentState = currentTokensState)
|
||||
.toImmutableList(),
|
||||
buttonState = addButtonState,
|
||||
addToPortfolioBSConfig = bsConfig,
|
||||
onAddClick = onAddClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<Pair<PortfolioTokenUM, PortfolioData.CryptoCurrencyData>>.setQuickActionsVisibility(
|
||||
currentState: MyPortfolioUM.Tokens?,
|
||||
): List<PortfolioTokenUM> {
|
||||
return when {
|
||||
// if there is only one token and it has empty balance, show quick actions for it
|
||||
currentState == null && this.size == 1 && isEmptyBalance(this.first().second) -> {
|
||||
this.map { (token, _) ->
|
||||
token.copy(isQuickActionsShown = true)
|
||||
}
|
||||
}
|
||||
// if there is no previous state, hide quick actions for all tokens
|
||||
currentState == null -> {
|
||||
this.map { (token, _) ->
|
||||
token.copy(isQuickActionsShown = false)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val previousList = currentState.tokens
|
||||
|
||||
// otherwise, keep previous state
|
||||
this.map { (token, _) ->
|
||||
token.copy(
|
||||
isQuickActionsShown = previousList
|
||||
.firstOrNull { it.matchWith(token) }
|
||||
?.isQuickActionsShown == true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isEmptyBalance(cryptoData: PortfolioData.CryptoCurrencyData): Boolean {
|
||||
return cryptoData.status.value.amount?.isZero() == true
|
||||
}
|
||||
|
||||
private fun toggleQuickActions(cryptoData: PortfolioData.CryptoCurrencyData) {
|
||||
updateTokens { tokenList ->
|
||||
tokenList.map { portfolioTokenUM ->
|
||||
portfolioTokenUM.copy(
|
||||
isQuickActionsShown = if (portfolioTokenUM.matchWith(cryptoData)) {
|
||||
!portfolioTokenUM.isQuickActionsShown
|
||||
} else {
|
||||
false
|
||||
},
|
||||
)
|
||||
}.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun PortfolioTokenUM.matchWith(token: PortfolioTokenUM): Boolean {
|
||||
return this.walletId == token.walletId && this.tokenItemState.id == token.tokenItemState.id
|
||||
}
|
||||
|
||||
private fun PortfolioTokenUM.matchWith(cryptoData: PortfolioData.CryptoCurrencyData): Boolean {
|
||||
return this.walletId == cryptoData.userWallet.walletId &&
|
||||
this.tokenItemState.id == cryptoData.status.currency.id.value
|
||||
}
|
||||
}
|
||||
|
|
@ -1,383 +0,0 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import com.tangem.common.ui.userwallet.UserWalletItem
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButton
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
|
||||
import com.tangem.core.ui.components.currency.icon.CoinIcon
|
||||
import com.tangem.core.ui.components.rows.ArrowRow
|
||||
import com.tangem.core.ui.components.rows.BlockchainRow
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.haptic.TangemHapticEffect
|
||||
import com.tangem.core.ui.res.LocalHapticManager
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.preview.PreviewAddToPortfolioBSContentProvider
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.AddToPortfolioBSContentUM
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.SelectNetworkUM
|
||||
import com.tangem.features.feed.impl.R
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Composable
|
||||
internal fun AddToPortfolioBottomSheet(config: TangemBottomSheetConfig) {
|
||||
TangemBottomSheet<AddToPortfolioBSContentUM>(
|
||||
config = config,
|
||||
containerColor = TangemTheme.colors.background.tertiary,
|
||||
addBottomInsets = false,
|
||||
titleText = resourceReference(R.string.common_add_to_portfolio),
|
||||
) { contentUM ->
|
||||
Content(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = contentUM,
|
||||
)
|
||||
|
||||
WalletSelectorBottomSheet(contentUM.walletSelectorConfig)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(state: AddToPortfolioBSContentUM, modifier: Modifier = Modifier) {
|
||||
var continueButtonAreaHeight by remember { mutableIntStateOf(0) }
|
||||
val density = LocalDensity.current
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
Box(modifier = modifier) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(state = scrollState)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
if (state.isWalletBlockVisible) {
|
||||
UserWalletItem(
|
||||
state = state.selectedWallet,
|
||||
blockColors = TangemBlockCardColors.copy(
|
||||
containerColor = TangemTheme.colors.background.action,
|
||||
disabledContainerColor = TangemTheme.colors.background.action,
|
||||
),
|
||||
)
|
||||
SpacerH12()
|
||||
}
|
||||
|
||||
NetworkSelection(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = state.selectNetworkUM,
|
||||
)
|
||||
|
||||
SpacerH12()
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = state.isScanCardNotificationVisible,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Column {
|
||||
ScanWalletWarning(modifier = Modifier.fillMaxWidth())
|
||||
SpacerH12()
|
||||
}
|
||||
|
||||
// Scroll to the bottom when the notification appears and the scroll is at the bottom
|
||||
LaunchedEffect(Unit) {
|
||||
if (scrollState.canScrollForward.not()) {
|
||||
delay(timeMillis = 500)
|
||||
scrollState.animateScrollTo(scrollState.maxValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SpacerH(with(density) { continueButtonAreaHeight.toDp() })
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = scrollState.canScrollForward,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
) {
|
||||
BottomFade(Modifier.align(Alignment.BottomCenter))
|
||||
}
|
||||
|
||||
ContinueButton(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.onGloballyPositioned {
|
||||
continueButtonAreaHeight = it.size.height
|
||||
},
|
||||
enabled = state.isContinueButtonEnabled,
|
||||
isTangemIconVisible = state.isScanCardNotificationVisible,
|
||||
onClick = state.onContinueButtonClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContinueButton(
|
||||
enabled: Boolean,
|
||||
isTangemIconVisible: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TangemButton(
|
||||
enabled = enabled,
|
||||
modifier = modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing16,
|
||||
)
|
||||
.navigationBarsPadding()
|
||||
.fillMaxWidth(),
|
||||
text = stringResourceSafe(R.string.common_continue),
|
||||
icon = if (enabled && isTangemIconVisible) {
|
||||
TangemButtonIconPosition.End(R.drawable.ic_tangem_24)
|
||||
} else {
|
||||
TangemButtonIconPosition.None
|
||||
},
|
||||
showProgress = false,
|
||||
size = TangemButtonSize.Default,
|
||||
colors = TangemButtonsDefaults.primaryButtonColors,
|
||||
textStyle = TangemTheme.typography.subtitle1,
|
||||
onClick = onClick,
|
||||
animateContentChange = true,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun NetworkSelection(state: SelectNetworkUM, modifier: Modifier = Modifier) {
|
||||
val hapticManager = LocalHapticManager.current
|
||||
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_select_network),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
},
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = TangemTheme.dimens.spacing14),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CoinIcon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size36),
|
||||
url = state.iconUrl,
|
||||
alpha = 1f,
|
||||
colorFilter = null,
|
||||
fallbackResId = R.drawable.ic_custom_token_44,
|
||||
)
|
||||
SpacerW12()
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.weight(1f, fill = false)
|
||||
.alignByBaseline(),
|
||||
text = state.tokenName,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
SpacerW6()
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.alignByBaseline(),
|
||||
text = state.tokenCurrencySymbol,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
overflow = TextOverflow.Visible,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
|
||||
state.networks.fastForEachIndexed { index, network ->
|
||||
ArrowRow(
|
||||
isLastItem = index == state.networks.lastIndex,
|
||||
content = {
|
||||
BlockchainRow(
|
||||
modifier = Modifier.padding(end = TangemTheme.dimens.spacing4),
|
||||
model = network,
|
||||
action = {
|
||||
TangemSwitch(
|
||||
checked = network.isSelected,
|
||||
checkedColor = if (network.isEnabled) {
|
||||
TangemTheme.colors.control.checked
|
||||
} else {
|
||||
TangemTheme.colors.icon.inactive
|
||||
},
|
||||
onCheckedChange = { checked ->
|
||||
if (checked) {
|
||||
hapticManager.perform(TangemHapticEffect.View.ToggleOn)
|
||||
} else {
|
||||
hapticManager.perform(TangemHapticEffect.View.ToggleOff)
|
||||
}
|
||||
|
||||
state.onNetworkSwitchClick(network, checked)
|
||||
},
|
||||
enabled = network.isEnabled,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScanWalletWarning(modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.button.disabled,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
)
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing10),
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.requiredSize(TangemTheme.dimens.size20),
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_tangem_24),
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_generate_addresses_notification),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun Preview(
|
||||
@PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
AddToPortfolioBottomSheet(
|
||||
config = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
content = content,
|
||||
onDismissRequest = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun PreviewContent(
|
||||
@PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
Content(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.tertiary)
|
||||
.fillMaxWidth(),
|
||||
state = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun PreviewContentRtl(
|
||||
@PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM,
|
||||
) {
|
||||
TangemThemePreview(rtl = true) {
|
||||
Content(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.tertiary)
|
||||
.fillMaxWidth(),
|
||||
state = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// For on device testing
|
||||
@Composable
|
||||
@Preview
|
||||
private fun PreviewContentTestOnDevice(
|
||||
@PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM,
|
||||
) {
|
||||
TangemThemePreview(
|
||||
alwaysShowBottomSheets = false,
|
||||
) {
|
||||
var isShow by remember { mutableStateOf(false) }
|
||||
|
||||
var contentState by remember {
|
||||
mutableStateOf(content)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
contentState = content.copy(
|
||||
onContinueButtonClick = {
|
||||
contentState = contentState.copy(
|
||||
isScanCardNotificationVisible = !contentState.isScanCardNotificationVisible,
|
||||
)
|
||||
},
|
||||
isContinueButtonEnabled = true,
|
||||
selectedWallet = content.selectedWallet.copy(
|
||||
onClick = {
|
||||
contentState = contentState.copy(
|
||||
isContinueButtonEnabled = !contentState.isContinueButtonEnabled,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
AddToPortfolioBottomSheet(
|
||||
config = TangemBottomSheetConfig(
|
||||
isShown = isShow,
|
||||
content = contentState,
|
||||
onDismissRequest = { isShow = false },
|
||||
),
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = { isShow = !isShow },
|
||||
) {
|
||||
Text(text = "Toggle")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -70,11 +70,6 @@ internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) {
|
|||
is MyPortfolioUM.Content -> PortfolioList(state = state)
|
||||
}
|
||||
}
|
||||
|
||||
val bsConfig = state.addToPortfolioBSConfig
|
||||
if (bsConfig != null) {
|
||||
AddToPortfolioBottomSheet(config = bsConfig)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -1,86 +0,0 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.SimpleSettingsRow
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
fun TokenActionsBottomSheet(config: TangemBottomSheetConfig) {
|
||||
TangemBottomSheet<TokenActionsBSContentUM>(
|
||||
config = config,
|
||||
title = { content ->
|
||||
TangemBottomSheetTitle(content.title)
|
||||
},
|
||||
containerColor = TangemTheme.colors.background.tertiary,
|
||||
content = { Content(it) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(content: TokenActionsBSContentUM) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
) {
|
||||
content.actions.forEachIndexed { index, action ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.roundedShapeItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = content.actions.lastIndex,
|
||||
addDefaultPadding = false,
|
||||
)
|
||||
.background(TangemTheme.colors.background.action),
|
||||
) {
|
||||
SimpleSettingsRow(
|
||||
title = action.text.resolveReference(),
|
||||
icon = action.iconRes,
|
||||
redesign = true,
|
||||
onItemsClick = { content.onActionClick(action) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360, heightDp = 640)
|
||||
@Preview(widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview(
|
||||
alwaysShowBottomSheets = true,
|
||||
) {
|
||||
Box(Modifier.background(TangemTheme.colors.background.secondary)) {
|
||||
TokenActionsBottomSheet(
|
||||
TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = {},
|
||||
content = TokenActionsBSContentUM(
|
||||
title = "Wallet 1",
|
||||
actions = TokenActionsBSContentUM.Action.entries.toImmutableList(),
|
||||
onActionClick = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.common.ui.userwallet.UserWalletItem
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||
import com.tangem.core.ui.components.appbar.TangemTopAppBarHeight
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.preview.PreviewAddToPortfolioBSContentProvider
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.WalletSelectorBSContentUM
|
||||
import com.tangem.features.feed.impl.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
internal fun WalletSelectorBottomSheet(config: TangemBottomSheetConfig) {
|
||||
TangemBottomSheet<WalletSelectorBSContentUM>(
|
||||
config = config,
|
||||
containerColor = TangemTheme.colors.background.tertiary,
|
||||
addBottomInsets = false,
|
||||
title = { content ->
|
||||
TangemTopAppBar(
|
||||
title = resourceReference(R.string.common_choose_wallet),
|
||||
titleAlignment = Alignment.CenterHorizontally,
|
||||
startButton = TopAppBarButtonUM.Back(content.onBack),
|
||||
height = TangemTopAppBarHeight.BOTTOM_SHEET,
|
||||
)
|
||||
},
|
||||
) { content ->
|
||||
Content(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing8,
|
||||
),
|
||||
state = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(state: WalletSelectorBSContentUM, modifier: Modifier = Modifier) {
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
BlockCard(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
colors = TangemBlockCardColors.copy(
|
||||
containerColor = TangemTheme.colors.background.action,
|
||||
disabledContainerColor = TangemTheme.colors.background.action,
|
||||
),
|
||||
) {
|
||||
state.userWallets.forEach { state ->
|
||||
key(state.id) {
|
||||
UserWalletItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
blockColors = TangemBlockCardColors.copy(
|
||||
containerColor = TangemTheme.colors.background.action,
|
||||
disabledContainerColor = TangemTheme.colors.background.action,
|
||||
),
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
SpacerH(bottomBarHeight)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
WalletSelectorBottomSheet(
|
||||
config = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = {},
|
||||
content = WalletSelectorBSContentUM(
|
||||
userWallets = persistentListOf(
|
||||
PreviewAddToPortfolioBSContentProvider().userWallet.copy(
|
||||
endIcon = UserWalletItemUM.EndIcon.None,
|
||||
),
|
||||
PreviewAddToPortfolioBSContentProvider().userWallet.copy(
|
||||
endIcon = UserWalletItemUM.EndIcon.Checkmark,
|
||||
),
|
||||
PreviewAddToPortfolioBSContentProvider().userWallet.copy(
|
||||
endIcon = UserWalletItemUM.EndIcon.None,
|
||||
),
|
||||
),
|
||||
onBack = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewContent() {
|
||||
TangemThemePreview {
|
||||
Content(
|
||||
state = WalletSelectorBSContentUM(
|
||||
userWallets = persistentListOf(
|
||||
PreviewAddToPortfolioBSContentProvider().userWallet.copy(
|
||||
endIcon = UserWalletItemUM.EndIcon.None,
|
||||
),
|
||||
PreviewAddToPortfolioBSContentProvider().userWallet.copy(
|
||||
endIcon = UserWalletItemUM.EndIcon.Checkmark,
|
||||
),
|
||||
PreviewAddToPortfolioBSContentProvider().userWallet.copy(
|
||||
endIcon = UserWalletItemUM.EndIcon.None,
|
||||
),
|
||||
),
|
||||
onBack = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.ui.preview
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.AddToPortfolioBSContentUM
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.SelectNetworkUM
|
||||
import com.tangem.features.feed.impl.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider<AddToPortfolioBSContentUM> {
|
||||
|
||||
private val blockchainRow = BlockchainRowUM(
|
||||
id = "1",
|
||||
name = "Etherium 3",
|
||||
type = "TEST",
|
||||
iconResId = R.drawable.ic_eth_16,
|
||||
isMainNetwork = false,
|
||||
isSelected = false,
|
||||
)
|
||||
|
||||
val userWallet = UserWalletItemUM(
|
||||
id = "1",
|
||||
name = stringReference("Wallet 1"),
|
||||
information = UserWalletItemUM.Information.Loaded(TextReference.Str("3 cards")),
|
||||
balance = UserWalletItemUM.Balance.Loading,
|
||||
isEnabled = true,
|
||||
endIcon = UserWalletItemUM.EndIcon.Arrow,
|
||||
onClick = {},
|
||||
)
|
||||
|
||||
override val values: Sequence<AddToPortfolioBSContentUM>
|
||||
get() = sequenceOf(
|
||||
AddToPortfolioBSContentUM(
|
||||
selectedWallet = userWallet,
|
||||
selectNetworkUM = SelectNetworkUM(
|
||||
tokenId = "etherium",
|
||||
tokenName = "Etherium",
|
||||
tokenCurrencySymbol = "ETH",
|
||||
networks = persistentListOf(
|
||||
blockchainRow.copy(
|
||||
type = "MAIN",
|
||||
isMainNetwork = true,
|
||||
isSelected = true,
|
||||
),
|
||||
blockchainRow,
|
||||
blockchainRow,
|
||||
),
|
||||
onNetworkSwitchClick = { _, _ -> },
|
||||
iconUrl = null,
|
||||
),
|
||||
isScanCardNotificationVisible = true,
|
||||
isWalletBlockVisible = true,
|
||||
isContinueButtonEnabled = true,
|
||||
onContinueButtonClick = {},
|
||||
walletSelectorConfig = TangemBottomSheetConfig.Empty,
|
||||
),
|
||||
AddToPortfolioBSContentUM(
|
||||
selectedWallet = userWallet,
|
||||
selectNetworkUM = SelectNetworkUM(
|
||||
tokenId = "etherium",
|
||||
tokenName = "Etherium Etherium Etherium Etherium",
|
||||
tokenCurrencySymbol = "ETH",
|
||||
networks = persistentListOf(
|
||||
blockchainRow.copy(
|
||||
type = "MAIN",
|
||||
isMainNetwork = true,
|
||||
isSelected = true,
|
||||
).copy(name = "Etherium Etherium Etherium Etherium"),
|
||||
*Array(25) { blockchainRow },
|
||||
),
|
||||
|
||||
onNetworkSwitchClick = { _, _ -> },
|
||||
iconUrl = null,
|
||||
),
|
||||
isScanCardNotificationVisible = true,
|
||||
isWalletBlockVisible = false,
|
||||
isContinueButtonEnabled = false,
|
||||
onContinueButtonClick = {},
|
||||
walletSelectorConfig = TangemBottomSheetConfig.Empty,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ package com.tangem.features.feed.components.market.details.portfolio.impl.ui.pre
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.tangem.common.ui.account.AccountIconPreviewData
|
||||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState
|
||||
|
|
@ -22,25 +21,19 @@ internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider<MyPortfol
|
|||
MyPortfolioUM.Tokens(
|
||||
tokens = persistentListOf(sampleToken, sampleToken),
|
||||
buttonState = MyPortfolioUM.Tokens.AddButtonState.Available,
|
||||
addToPortfolioBSConfig = TangemBottomSheetConfig.Empty,
|
||||
onAddClick = {},
|
||||
),
|
||||
MyPortfolioUM.Tokens(
|
||||
tokens = persistentListOf(sampleToken, sampleToken.copy(isQuickActionsShown = true)),
|
||||
buttonState = MyPortfolioUM.Tokens.AddButtonState.Unavailable,
|
||||
addToPortfolioBSConfig = TangemBottomSheetConfig.Empty,
|
||||
onAddClick = {},
|
||||
),
|
||||
MyPortfolioUM.Tokens(
|
||||
tokens = persistentListOf(sampleToken.copy(isQuickActionsShown = true), sampleToken),
|
||||
buttonState = MyPortfolioUM.Tokens.AddButtonState.Loading,
|
||||
addToPortfolioBSConfig = TangemBottomSheetConfig.Empty,
|
||||
onAddClick = {},
|
||||
),
|
||||
MyPortfolioUM.AddFirstToken(
|
||||
addToPortfolioBSConfig = TangemBottomSheetConfig.Empty,
|
||||
onAddClick = {},
|
||||
),
|
||||
MyPortfolioUM.AddFirstToken(onAddClick = {}),
|
||||
MyPortfolioUM.Content(
|
||||
items = persistentListOf(
|
||||
walletPortfolioHeader,
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.ui.state
|
||||
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
|
||||
internal data class AddToPortfolioBSContentUM(
|
||||
val selectedWallet: UserWalletItemUM,
|
||||
val selectNetworkUM: SelectNetworkUM,
|
||||
val isWalletBlockVisible: Boolean,
|
||||
val isScanCardNotificationVisible: Boolean,
|
||||
val isContinueButtonEnabled: Boolean,
|
||||
val onContinueButtonClick: () -> Unit,
|
||||
val walletSelectorConfig: TangemBottomSheetConfig,
|
||||
) : TangemBottomSheetConfigContent
|
||||
|
|
@ -1,16 +1,12 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.ui.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
internal sealed class MyPortfolioUM {
|
||||
|
||||
abstract val addToPortfolioBSConfig: TangemBottomSheetConfig?
|
||||
|
||||
data class Tokens(
|
||||
override val addToPortfolioBSConfig: TangemBottomSheetConfig,
|
||||
val tokens: ImmutableList<PortfolioTokenUM>,
|
||||
val buttonState: AddButtonState,
|
||||
val onAddClick: () -> Unit,
|
||||
|
|
@ -27,25 +23,15 @@ internal sealed class MyPortfolioUM {
|
|||
val items: ImmutableList<PortfolioListItem>,
|
||||
val buttonState: Tokens.AddButtonState,
|
||||
val onAddClick: () -> Unit,
|
||||
) : MyPortfolioUM() {
|
||||
|
||||
override val addToPortfolioBSConfig: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty
|
||||
}
|
||||
) : MyPortfolioUM()
|
||||
|
||||
data class AddFirstToken(
|
||||
override val addToPortfolioBSConfig: TangemBottomSheetConfig,
|
||||
val onAddClick: () -> Unit,
|
||||
) : MyPortfolioUM()
|
||||
|
||||
data object Loading : MyPortfolioUM() {
|
||||
override val addToPortfolioBSConfig: TangemBottomSheetConfig? = null
|
||||
}
|
||||
data object Loading : MyPortfolioUM()
|
||||
|
||||
data object Unavailable : MyPortfolioUM() {
|
||||
override val addToPortfolioBSConfig: TangemBottomSheetConfig? = null
|
||||
}
|
||||
data object Unavailable : MyPortfolioUM()
|
||||
|
||||
data object UnavailableForWallet : MyPortfolioUM() {
|
||||
override val addToPortfolioBSConfig: TangemBottomSheetConfig? = null
|
||||
}
|
||||
data object UnavailableForWallet : MyPortfolioUM()
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.ui.state
|
||||
|
||||
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class SelectNetworkUM(
|
||||
val tokenId: String,
|
||||
val iconUrl: String?,
|
||||
val tokenName: String,
|
||||
val tokenCurrencySymbol: String,
|
||||
val networks: ImmutableList<BlockchainRowUM>,
|
||||
val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit,
|
||||
)
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.ui.state
|
||||
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class WalletSelectorBSContentUM(
|
||||
val userWallets: ImmutableList<UserWalletItemUM>,
|
||||
val onBack: () -> Unit,
|
||||
) : TangemBottomSheetConfigContent
|
||||
|
|
@ -7,9 +7,6 @@ internal class DefaultFeedFeatureToggle(
|
|||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : FeedFeatureToggle {
|
||||
|
||||
override val isFeedEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled("FEED_ENABLED")
|
||||
|
||||
override val isEarnBlockEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled("EARN_BLOCK_ENABLED")
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.features.feed.model.converter
|
||||
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
|
|
|||
|
|
@ -32,12 +32,10 @@ import com.tangem.features.feed.model.earn.filters.state.EarnFilterNetworkUMConv
|
|||
import com.tangem.features.feed.model.earn.filters.state.EarnFilterTypeConverter
|
||||
import com.tangem.features.feed.model.earn.filters.state.EarnFilterTypeUMConverter
|
||||
import com.tangem.features.feed.model.earn.state.EarnStateController
|
||||
import com.tangem.features.feed.model.earn.state.transformers.EarnFilterSelectedStateTransformer
|
||||
import com.tangem.features.feed.model.earn.state.transformers.UpdateBestOpportunitiesStateTransformer
|
||||
import com.tangem.features.feed.model.earn.state.transformers.UpdateEarnUMInitialStateTransformer
|
||||
import com.tangem.features.feed.model.earn.state.transformers.UpdateMostlyUsedStateTransformer
|
||||
import com.tangem.features.feed.model.earn.state.transformers.*
|
||||
import com.tangem.features.feed.model.earn.statemanager.EarnListBatchFlowManager
|
||||
import com.tangem.features.feed.model.earn.statemanager.EarnListStateManager
|
||||
import com.tangem.features.feed.ui.earn.state.EarnBestOpportunitiesUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnUM
|
||||
|
|
@ -69,8 +67,8 @@ internal class EarnModel @Inject constructor(
|
|||
private val earnNetworks = MutableStateFlow<EarnNetworks>(Either.Right(emptyList()))
|
||||
private val earnListConfigProvider = Provider {
|
||||
createEarnTokensListConfig(
|
||||
selectedTypeFilter = stateController.value.selectedTypeFilter,
|
||||
selectedNetworkFilter = stateController.value.selectedNetworkFilter,
|
||||
selectedTypeFilter = stateController.value.earnFilterUM.selectedTypeFilter,
|
||||
selectedNetworkFilter = stateController.value.earnFilterUM.selectedNetworkFilter,
|
||||
earnNetworks = earnNetworks.value,
|
||||
)
|
||||
}
|
||||
|
|
@ -104,7 +102,6 @@ internal class EarnModel @Inject constructor(
|
|||
init {
|
||||
updateInitialState()
|
||||
fetchEarnNetworks()
|
||||
fetchTopEarnTokens()
|
||||
subscribeOnStoredFilters()
|
||||
subscribeOnNetworks()
|
||||
subscribeOnBatchFlow()
|
||||
|
|
@ -117,19 +114,22 @@ internal class EarnModel @Inject constructor(
|
|||
batchFlowManager.initialLoadingError,
|
||||
batchFlowManager.paginationStatus,
|
||||
) { items, error, paginationStatus ->
|
||||
val hasActiveFilters = state.value.selectedTypeFilter != EarnFilterTypeUM.All ||
|
||||
state.value.selectedNetworkFilter !is EarnFilterNetworkUM.AllNetworks
|
||||
error?.let(::handleBestOpportunitiesErrorAnalytics)
|
||||
val hasActiveFilters = state.value.earnFilterUM.selectedTypeFilter != EarnFilterTypeUM.All ||
|
||||
state.value.earnFilterUM.selectedNetworkFilter !is EarnFilterNetworkUM.AllNetworks
|
||||
EarnListStateManager.calculateState(
|
||||
items = items,
|
||||
error = error,
|
||||
paginationStatus = paginationStatus,
|
||||
hasActiveFilters = hasActiveFilters,
|
||||
onRetryClick = { batchFlowManager.reload() },
|
||||
onRetryClick = {
|
||||
batchFlowManager.reload()
|
||||
reloadEarnNetworks()
|
||||
},
|
||||
onLoadMore = { batchFlowManager.loadMore() },
|
||||
onClearFiltersClick = ::onClearFiltersClick,
|
||||
)
|
||||
}.onEach { bestOpportunitiesState ->
|
||||
) to error
|
||||
}.onEach { (bestOpportunitiesState, error) ->
|
||||
error?.let(::handleBestOpportunitiesErrorAnalytics)
|
||||
stateController.update(UpdateBestOpportunitiesStateTransformer(bestOpportunitiesState))
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
|
@ -156,23 +156,27 @@ internal class EarnModel @Inject constructor(
|
|||
|
||||
private fun subscribeOnStoredFilters() {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
getEarnFilterUseCase()
|
||||
.collect { filter ->
|
||||
val typeFilterUM = EarnFilterTypeConverter().convert(filter.earnFilterType)
|
||||
val networkFilterUM = EarnFilterNetworkConverter().convert(filter.earnFilterNetwork)
|
||||
stateController.update(
|
||||
EarnFilterSelectedStateTransformer(
|
||||
filterType = typeFilterUM,
|
||||
filterNetwork = networkFilterUM,
|
||||
),
|
||||
)
|
||||
batchFlowManager.reload()
|
||||
}
|
||||
combine(
|
||||
getEarnFilterUseCase(),
|
||||
earnNetworks,
|
||||
) { filter, networks ->
|
||||
val typeFilterUM = EarnFilterTypeConverter().convert(filter.earnFilterType)
|
||||
val networkFilterUM = EarnFilterNetworkConverter().convert(filter.earnFilterNetwork)
|
||||
stateController.update(
|
||||
EarnFilterSelectedStateTransformer(
|
||||
filterType = typeFilterUM,
|
||||
filterNetwork = networkFilterUM,
|
||||
earnNetworks = networks,
|
||||
),
|
||||
)
|
||||
batchFlowManager.reload()
|
||||
}.collect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchTopEarnTokens() {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
stateController.update(UpdateMostlyUsedStateLoadingTransformer())
|
||||
fetchTopEarnTokensUseCase()
|
||||
}
|
||||
}
|
||||
|
|
@ -183,13 +187,21 @@ internal class EarnModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun reloadEarnNetworks() {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
if (earnNetworks.value.isLeft()) {
|
||||
fetchEarnNetworks()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* start of clicks area */
|
||||
private fun onTypeFilterClick() {
|
||||
val currentState = state.value
|
||||
bottomSheetNavigation.activate(
|
||||
FeedBottomSheetRoute.TypeFilter(
|
||||
params = EarnTypeFilterComponent.Params(
|
||||
selectedFilter = EarnFilterTypeUMConverter().convert(currentState.selectedTypeFilter),
|
||||
selectedFilter = EarnFilterTypeUMConverter().convert(currentState.earnFilterUM.selectedTypeFilter),
|
||||
onFilterSelected = ::onTypeFilterOptionSelected,
|
||||
onDismiss = { bottomSheetNavigation.dismiss() },
|
||||
),
|
||||
|
|
@ -210,7 +222,7 @@ internal class EarnModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun createNetworkFilters(): List<EarnFilterNetwork> {
|
||||
val selectedFilter = state.value.selectedNetworkFilter
|
||||
val selectedFilter = state.value.earnFilterUM.selectedNetworkFilter
|
||||
return buildList {
|
||||
add(
|
||||
EarnFilterNetwork.AllNetworks(
|
||||
|
|
@ -277,11 +289,14 @@ internal class EarnModel @Inject constructor(
|
|||
modelScope.launch(dispatchers.default) {
|
||||
setEarnFilterUseCase(
|
||||
EarnFilter(
|
||||
earnFilterNetwork = EarnFilterNetworkUMConverter().convert(state.value.selectedNetworkFilter),
|
||||
earnFilterNetwork = EarnFilterNetworkUMConverter().convert(
|
||||
value = state.value.earnFilterUM.selectedNetworkFilter,
|
||||
),
|
||||
earnFilterType = type,
|
||||
),
|
||||
)
|
||||
bottomSheetNavigation.dismiss()
|
||||
reloadEarnNetworks()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -290,7 +305,7 @@ internal class EarnModel @Inject constructor(
|
|||
setEarnFilterUseCase(
|
||||
EarnFilter(
|
||||
earnFilterNetwork = filter,
|
||||
earnFilterType = EarnFilterTypeUMConverter().convert(state.value.selectedTypeFilter),
|
||||
earnFilterType = EarnFilterTypeUMConverter().convert(state.value.earnFilterUM.selectedTypeFilter),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -319,11 +334,13 @@ internal class EarnModel @Inject constructor(
|
|||
is ApiResponseError.HttpException -> error.code.numericCode to error.message.orEmpty()
|
||||
else -> null to ""
|
||||
}
|
||||
analyticsEventHandler.send(
|
||||
EarnAnalyticsEvent.BestOpportunitiesLoadError(
|
||||
code = code,
|
||||
message = message,
|
||||
),
|
||||
)
|
||||
if (state.value.bestOpportunities !is EarnBestOpportunitiesUM.Error) {
|
||||
analyticsEventHandler.send(
|
||||
EarnAnalyticsEvent.BestOpportunitiesLoadError(
|
||||
code = code,
|
||||
message = message,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,7 +23,9 @@ internal fun createEarnTokensListConfig(
|
|||
earnNetworks.fold(
|
||||
ifLeft = { null },
|
||||
ifRight = { networks ->
|
||||
networks.filter(EarnNetwork::isAdded).map(EarnNetwork::networkId)
|
||||
networks.filter(EarnNetwork::isAdded)
|
||||
.map(EarnNetwork::networkId)
|
||||
.ifEmpty { listOf(NO_ONE_NETWORK) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -34,4 +36,9 @@ internal fun createEarnTokensListConfig(
|
|||
networks = networks,
|
||||
isForEarn = isForEarn,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This id means that backend has to return empty result
|
||||
*/
|
||||
private const val NO_ONE_NETWORK = "-1"
|
||||
|
|
@ -26,7 +26,7 @@ internal sealed class EarnAnalyticsEvent(
|
|||
event = "Best Opportunities Filter Network Applied",
|
||||
params = mapOf(
|
||||
"Network Filter Type" to filterType.value,
|
||||
"NetworkId" to networkId.orEmpty(),
|
||||
"Network Id" to networkId.orEmpty(),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -26,8 +26,10 @@ internal class EarnStateController @Inject constructor() {
|
|||
return EarnUM(
|
||||
mostlyUsed = EarnListUM.Loading,
|
||||
bestOpportunities = EarnBestOpportunitiesUM.Loading,
|
||||
selectedTypeFilter = EarnFilterTypeUM.All,
|
||||
selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true),
|
||||
earnFilterUM = EarnFilterUM(
|
||||
selectedTypeFilter = EarnFilterTypeUM.All,
|
||||
selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true),
|
||||
),
|
||||
onBackClick = {},
|
||||
onNetworkFilterClick = {},
|
||||
onTypeFilterClick = {},
|
||||
|
|
|
|||
|
|
@ -1,18 +1,24 @@
|
|||
package com.tangem.features.feed.model.earn.state.transformers
|
||||
|
||||
import com.tangem.domain.models.earn.EarnNetworks
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnUM
|
||||
|
||||
internal class EarnFilterSelectedStateTransformer(
|
||||
private val earnNetworks: EarnNetworks,
|
||||
private val filterType: EarnFilterTypeUM,
|
||||
private val filterNetwork: EarnFilterNetworkUM,
|
||||
) : EarnUMTransformer {
|
||||
|
||||
override fun transform(prevState: EarnUM): EarnUM {
|
||||
return prevState.copy(
|
||||
selectedTypeFilter = filterType,
|
||||
selectedNetworkFilter = filterNetwork,
|
||||
earnFilterUM = prevState.earnFilterUM.copy(
|
||||
selectedTypeFilter = filterType,
|
||||
selectedNetworkFilter = filterNetwork,
|
||||
isNetworkFilterEnabled = earnNetworks.isRight { networks -> networks.isNotEmpty() },
|
||||
isTypeFilterEnabled = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
package com.tangem.features.feed.model.earn.state.transformers
|
||||
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnUM
|
||||
|
||||
internal class EarnNetworkFilterSelectedStateTransformer(
|
||||
private val filter: EarnFilterNetworkUM,
|
||||
) : EarnUMTransformer {
|
||||
|
||||
override fun transform(prevState: EarnUM): EarnUM {
|
||||
return prevState.copy(selectedNetworkFilter = filter)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.feed.model.earn.state.transformers
|
||||
|
||||
import com.tangem.features.feed.ui.earn.state.EarnListUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnUM
|
||||
|
||||
internal class UpdateMostlyUsedStateLoadingTransformer : EarnUMTransformer {
|
||||
|
||||
override fun transform(prevState: EarnUM): EarnUM {
|
||||
return prevState.copy(mostlyUsed = EarnListUM.Loading)
|
||||
}
|
||||
}
|
||||
|
|
@ -271,7 +271,7 @@ internal class FeedComponentModel @Inject constructor(
|
|||
earnListUM = if (feedFeatureToggle.isEarnBlockEnabled) {
|
||||
EarnListUM.Loading
|
||||
} else {
|
||||
null
|
||||
EarnListUM.Empty
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ internal class FeedStateController @Inject constructor(
|
|||
earnListUM = if (feedFeatureToggle.isEarnBlockEnabled) {
|
||||
EarnListUM.Loading
|
||||
} else {
|
||||
null
|
||||
EarnListUM.Empty
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ internal class UpdateEarnStateTransformer(
|
|||
}
|
||||
|
||||
private fun handleEmptyState(currentState: FeedListUM): FeedListUM {
|
||||
return currentState.copy(earnListUM = null)
|
||||
return currentState.copy(earnListUM = EarnListUM.Empty)
|
||||
}
|
||||
|
||||
private fun handleErrorState(currentState: FeedListUM, result: EarnError): FeedListUM {
|
||||
|
|
@ -69,6 +69,8 @@ internal class UpdateEarnStateTransformer(
|
|||
currentState: FeedListUM,
|
||||
earnTokensWithCurrency: List<EarnTokenWithCurrency>,
|
||||
): FeedListUM {
|
||||
if (earnTokensWithCurrency.isEmpty()) return currentState.copy(earnListUM = EarnListUM.Empty)
|
||||
|
||||
val newItems = earnTokensWithCurrency
|
||||
.sortedWith(
|
||||
compareByDescending<EarnTokenWithCurrency> {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.features.feed.model.news.list.statemanager
|
||||
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.domain.models.news.ShortArticle
|
||||
import com.tangem.domain.news.model.NewsListBatchingContext
|
||||
import com.tangem.domain.news.model.NewsListConfig
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.features.feed.model.news.list.statemanager
|
||||
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.features.feed.model.news.list.analytics.NewsListAnalyticsEvent
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package com.tangem.features.feed.ui.earn
|
|||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.material3.Icon
|
||||
|
|
@ -14,21 +13,21 @@ import androidx.compose.runtime.mutableStateOf
|
|||
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.Color
|
||||
import androidx.compose.ui.layout.onFirstVisible
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.SmallButtonShimmer
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerWMax
|
||||
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.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.list.InfiniteListHandler
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
|
|
@ -39,6 +38,7 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.feed.ui.earn.components.EarnItemPlaceholder
|
||||
import com.tangem.features.feed.ui.earn.components.EarnListItem
|
||||
import com.tangem.features.feed.ui.earn.components.MostlyUsedCard
|
||||
import com.tangem.features.feed.ui.earn.components.MostlyUsedPlaceholder
|
||||
import com.tangem.features.feed.ui.earn.state.*
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -91,12 +91,7 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) {
|
|||
SpacerH(12.dp)
|
||||
BestOpportunitiesFilters(
|
||||
state = state.bestOpportunities,
|
||||
selectedNetworkFilterText = when (state.selectedNetworkFilter) {
|
||||
is EarnFilterNetworkUM.AllNetworks -> TextReference.Res(R.string.earn_filter_all_networks)
|
||||
is EarnFilterNetworkUM.MyNetworks -> TextReference.Res(R.string.earn_filter_my_networks)
|
||||
is EarnFilterNetworkUM.Network -> TextReference.Str(state.selectedNetworkFilter.text)
|
||||
},
|
||||
selectedTypeFilterText = state.selectedTypeFilterText,
|
||||
earnFilterUM = state.earnFilterUM,
|
||||
onNetworkFilterClick = state.onNetworkFilterClick,
|
||||
onTypeFilterClick = state.onTypeFilterClick,
|
||||
)
|
||||
|
|
@ -113,8 +108,8 @@ private fun MostlyUsedContent(state: EarnListUM, onScroll: () -> Unit) {
|
|||
AnimatedContent(
|
||||
targetState = state,
|
||||
contentKey = { it::class.java },
|
||||
) { st ->
|
||||
when (st) {
|
||||
) { animatedState ->
|
||||
when (animatedState) {
|
||||
is EarnListUM.Loading -> {
|
||||
MostlyUsedPlaceholder()
|
||||
}
|
||||
|
|
@ -127,7 +122,7 @@ private fun MostlyUsedContent(state: EarnListUM, onScroll: () -> Unit) {
|
|||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = st.items,
|
||||
items = animatedState.items,
|
||||
key = { _, item -> "${item.tokenName}-${item.network}" },
|
||||
) { index, item ->
|
||||
val cardModifier = Modifier.conditional(
|
||||
|
|
@ -151,84 +146,33 @@ private fun MostlyUsedContent(state: EarnListUM, onScroll: () -> Unit) {
|
|||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 12.dp,
|
||||
),
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
)
|
||||
.padding(vertical = 32.dp, horizontal = 12.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
UnableToLoadData(onRetryClick = st.onRetryClicked)
|
||||
UnableToLoadData(onRetryClick = animatedState.onRetryClicked)
|
||||
}
|
||||
}
|
||||
EarnListUM.Empty -> Unit // no need to handle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MostlyUsedCard(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.width(148.dp)
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(12.dp),
|
||||
) {
|
||||
CurrencyIcon(
|
||||
modifier = Modifier.size(32.dp),
|
||||
state = item.currencyIconState,
|
||||
shouldDisplayNetwork = true,
|
||||
networkBadgeSize = 12.dp,
|
||||
networkBadgeBackground = TangemTheme.colors.background.action,
|
||||
)
|
||||
|
||||
SpacerH(8.dp)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(weight = 1f, fill = false),
|
||||
text = item.tokenName.resolveReference(),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
SpacerW(4.dp)
|
||||
Text(
|
||||
text = item.symbol.resolveReference(),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
|
||||
SpacerH(2.dp)
|
||||
|
||||
Text(
|
||||
text = item.earnValue.resolveReference(),
|
||||
color = TangemTheme.colors.text.accent,
|
||||
style = TangemTheme.typography.caption1,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BestOpportunitiesFilters(
|
||||
state: EarnBestOpportunitiesUM,
|
||||
selectedNetworkFilterText: TextReference,
|
||||
selectedTypeFilterText: TextReference,
|
||||
earnFilterUM: EarnFilterUM,
|
||||
onNetworkFilterClick: () -> Unit,
|
||||
onTypeFilterClick: () -> Unit,
|
||||
) {
|
||||
when (state) {
|
||||
is EarnBestOpportunitiesUM.Loading -> FilterButtonsShimmer()
|
||||
else -> FilterButtons(
|
||||
selectedNetworkFilterText = selectedNetworkFilterText,
|
||||
selectedTypeFilterText = selectedTypeFilterText,
|
||||
isEnabled = state is EarnBestOpportunitiesUM.Content || state is EarnBestOpportunitiesUM.EmptyFiltered,
|
||||
earnFilterUM = earnFilterUM,
|
||||
onNetworkFilterClick = onNetworkFilterClick,
|
||||
onTypeFilterClick = onTypeFilterClick,
|
||||
)
|
||||
|
|
@ -307,9 +251,7 @@ private fun LazyListScope.bestOpportunitiesItems(state: EarnBestOpportunitiesUM)
|
|||
|
||||
@Composable
|
||||
private fun FilterButtons(
|
||||
selectedNetworkFilterText: TextReference,
|
||||
selectedTypeFilterText: TextReference,
|
||||
isEnabled: Boolean,
|
||||
earnFilterUM: EarnFilterUM,
|
||||
onNetworkFilterClick: () -> Unit,
|
||||
onTypeFilterClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -319,10 +261,14 @@ private fun FilterButtons(
|
|||
) {
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = selectedNetworkFilterText,
|
||||
text = when (earnFilterUM.selectedNetworkFilter) {
|
||||
is EarnFilterNetworkUM.AllNetworks -> TextReference.Res(R.string.earn_filter_all_networks)
|
||||
is EarnFilterNetworkUM.MyNetworks -> TextReference.Res(R.string.earn_filter_my_networks)
|
||||
is EarnFilterNetworkUM.Network -> TextReference.Str(earnFilterUM.selectedNetworkFilter.text)
|
||||
},
|
||||
onClick = onNetworkFilterClick,
|
||||
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24),
|
||||
isEnabled = isEnabled,
|
||||
isEnabled = earnFilterUM.isNetworkFilterEnabled,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -330,10 +276,10 @@ private fun FilterButtons(
|
|||
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = selectedTypeFilterText,
|
||||
text = earnFilterUM.selectedTypeFilter.text,
|
||||
onClick = onTypeFilterClick,
|
||||
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24),
|
||||
isEnabled = isEnabled,
|
||||
isEnabled = earnFilterUM.isTypeFilterEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -496,7 +442,7 @@ private fun EarnContentLoadingPreview() {
|
|||
) {
|
||||
EarnContent(
|
||||
state = previewEarnUM(
|
||||
mostlyUsed = EarnListUM.Loading,
|
||||
mostlyUsed = EarnListUM.Error(onRetryClicked = {}),
|
||||
bestOpportunities = EarnBestOpportunitiesUM.Loading,
|
||||
),
|
||||
)
|
||||
|
|
@ -588,8 +534,12 @@ private fun previewEarnUM(
|
|||
): EarnUM = EarnUM(
|
||||
mostlyUsed = mostlyUsed,
|
||||
bestOpportunities = bestOpportunities,
|
||||
selectedTypeFilter = EarnFilterTypeUM.All,
|
||||
selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true),
|
||||
earnFilterUM = EarnFilterUM(
|
||||
selectedTypeFilter = EarnFilterTypeUM.All,
|
||||
selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true),
|
||||
isTypeFilterEnabled = true,
|
||||
isNetworkFilterEnabled = true,
|
||||
),
|
||||
onBackClick = {},
|
||||
onNetworkFilterClick = {},
|
||||
onTypeFilterClick = {},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,196 @@
|
|||
package com.tangem.features.feed.ui.earn.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.ds.opportunities.OpportunitiesBG
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.*
|
||||
import com.tangem.features.feed.ui.earn.state.EarnListItemUM
|
||||
|
||||
@Composable
|
||||
internal fun MostlyUsedCard(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
if (LocalRedesignEnabled.current) {
|
||||
MostlyUsedCardV2(
|
||||
modifier = modifier,
|
||||
item = item,
|
||||
onClick = onClick,
|
||||
)
|
||||
} else {
|
||||
MostlyUsedCardV1(
|
||||
modifier = modifier,
|
||||
item = item,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MostlyUsedCardV2(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
OpportunitiesBG(
|
||||
modifier = modifier
|
||||
.width(148.dp)
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.clickable(onClick = onClick),
|
||||
icon = TangemIconUM.Currency(item.currencyIconState),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
CurrencyIcon(
|
||||
modifier = Modifier.size(32.dp),
|
||||
state = item.currencyIconState,
|
||||
shouldDisplayNetwork = true,
|
||||
networkBadgeSize = 12.dp,
|
||||
networkBadgeBackground = TangemTheme.colors.background.action,
|
||||
)
|
||||
|
||||
SpacerH(22.dp)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(weight = 1f, fill = false),
|
||||
text = item.tokenName.resolveReference(),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography2.bodySemibold16,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
SpacerW(4.dp)
|
||||
Text(
|
||||
text = item.symbol.resolveReference(),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
|
||||
SpacerH(2.dp)
|
||||
|
||||
Text(
|
||||
text = item.earnValue.resolveReference(),
|
||||
color = TangemTheme.colors2.text.status.positive,
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MostlyUsedCardV1(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.width(148.dp)
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(12.dp),
|
||||
) {
|
||||
CurrencyIcon(
|
||||
modifier = Modifier.size(32.dp),
|
||||
state = item.currencyIconState,
|
||||
shouldDisplayNetwork = true,
|
||||
networkBadgeSize = 12.dp,
|
||||
networkBadgeBackground = TangemTheme.colors.background.action,
|
||||
)
|
||||
|
||||
SpacerH(8.dp)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(weight = 1f, fill = false),
|
||||
text = item.tokenName.resolveReference(),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
SpacerW(4.dp)
|
||||
Text(
|
||||
text = item.symbol.resolveReference(),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
|
||||
SpacerH(2.dp)
|
||||
|
||||
Text(
|
||||
text = item.earnValue.resolveReference(),
|
||||
color = TangemTheme.colors.text.accent,
|
||||
style = TangemTheme.typography.caption1,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun EarnListItemPreviewV1() {
|
||||
TangemThemePreview {
|
||||
MostlyUsedCardV1(
|
||||
EarnListItemUM(
|
||||
network = stringReference("Ethereum"),
|
||||
symbol = stringReference("USDT"),
|
||||
tokenName = stringReference("Tether"),
|
||||
currencyIconState = CurrencyIconState.TokenIcon(
|
||||
url = null,
|
||||
topBadgeIconResId = R.drawable.img_eth_22,
|
||||
fallbackTint = TangemColorPalette.Black,
|
||||
fallbackBackground = TangemColorPalette.Meadow,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
),
|
||||
earnValue = stringReference("APY 6.54%"),
|
||||
earnType = stringReference("Yield"),
|
||||
onItemClick = {},
|
||||
),
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun EarnListItemPreviewV2() {
|
||||
TangemThemePreviewRedesign {
|
||||
MostlyUsedCardV2(
|
||||
EarnListItemUM(
|
||||
network = stringReference("Ethereum"),
|
||||
symbol = stringReference("USDT"),
|
||||
tokenName = stringReference("Tether"),
|
||||
currencyIconState = CurrencyIconState.TokenIcon(
|
||||
url = null,
|
||||
topBadgeIconResId = R.drawable.img_eth_22,
|
||||
fallbackTint = TangemColorPalette.Black,
|
||||
fallbackBackground = TangemColorPalette.Meadow,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
),
|
||||
earnValue = stringReference("APY 6.54%"),
|
||||
earnType = stringReference("Yield"),
|
||||
onItemClick = {},
|
||||
),
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.feed.ui.earn.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
internal data class EarnFilterUM(
|
||||
val selectedTypeFilter: EarnFilterTypeUM,
|
||||
val selectedNetworkFilter: EarnFilterNetworkUM,
|
||||
val isTypeFilterEnabled: Boolean = true,
|
||||
val isNetworkFilterEnabled: Boolean = true,
|
||||
)
|
||||
|
|
@ -10,6 +10,7 @@ internal sealed interface EarnListUM {
|
|||
data object Loading : EarnListUM
|
||||
data class Content(val items: ImmutableList<EarnListItemUM>) : EarnListUM
|
||||
data class Error(val onRetryClicked: () -> Unit) : EarnListUM
|
||||
data object Empty : EarnListUM
|
||||
}
|
||||
|
||||
@Immutable
|
||||
|
|
|
|||
|
|
@ -1,20 +1,14 @@
|
|||
package com.tangem.features.feed.ui.earn.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
internal data class EarnUM(
|
||||
val mostlyUsed: EarnListUM,
|
||||
val bestOpportunities: EarnBestOpportunitiesUM,
|
||||
val selectedTypeFilter: EarnFilterTypeUM,
|
||||
val selectedNetworkFilter: EarnFilterNetworkUM,
|
||||
val earnFilterUM: EarnFilterUM,
|
||||
val onBackClick: () -> Unit,
|
||||
val onNetworkFilterClick: () -> Unit,
|
||||
val onTypeFilterClick: () -> Unit,
|
||||
val onSliderScroll: () -> Unit,
|
||||
) {
|
||||
|
||||
val selectedTypeFilterText: TextReference
|
||||
get() = selectedTypeFilter.text
|
||||
}
|
||||
)
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.feed.ui.feed.components
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -8,12 +9,18 @@ import androidx.compose.ui.Modifier
|
|||
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.SpacerW
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Composable
|
||||
internal fun Header(onSeeAllClick: () -> Unit, isLoading: Boolean = false, title: @Composable () -> Unit) {
|
||||
internal fun Header(
|
||||
onSeeAllClick: () -> Unit,
|
||||
isLoading: Boolean,
|
||||
shouldShowSeeAll: Boolean,
|
||||
title: @Composable () -> Unit,
|
||||
) {
|
||||
AnimatedContent(isLoading) { animatedState ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
|
@ -25,13 +32,18 @@ internal fun Header(onSeeAllClick: () -> Unit, isLoading: Boolean = false, title
|
|||
if (animatedState) {
|
||||
RectangleShimmer(modifier = Modifier.size(width = 104.dp, height = 18.dp))
|
||||
} else {
|
||||
title()
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = TextReference.Res(R.string.common_see_all),
|
||||
onClick = onSeeAllClick,
|
||||
),
|
||||
)
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
title()
|
||||
}
|
||||
SpacerW(8.dp)
|
||||
AnimatedVisibility(shouldShowSeeAll) {
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = TextReference.Res(R.string.common_see_all),
|
||||
onClick = onSeeAllClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.core.ui.R
|
||||
|
|
@ -23,8 +24,8 @@ import com.tangem.features.feed.ui.earn.state.EarnListUM
|
|||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
internal fun EarnBlock(onSeeAllClick: () -> Unit, earnListUM: EarnListUM?, modifier: Modifier = Modifier) {
|
||||
if (earnListUM == null) return
|
||||
internal fun EarnBlock(onSeeAllClick: () -> Unit, earnListUM: EarnListUM, modifier: Modifier = Modifier) {
|
||||
if (earnListUM is EarnListUM.Empty) return
|
||||
|
||||
Column(modifier = modifier) {
|
||||
Header(
|
||||
|
|
@ -33,10 +34,13 @@ internal fun EarnBlock(onSeeAllClick: () -> Unit, earnListUM: EarnListUM?, modif
|
|||
text = stringResourceSafe(R.string.markets_earn_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
},
|
||||
onSeeAllClick = onSeeAllClick,
|
||||
isLoading = earnListUM is EarnListUM.Loading,
|
||||
shouldShowSeeAll = earnListUM is EarnListUM.Content,
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
|
|
@ -53,6 +57,7 @@ internal fun EarnBlock(onSeeAllClick: () -> Unit, earnListUM: EarnListUM?, modif
|
|||
is EarnListUM.Content -> EarnContentBlock(items = earnListUM.items)
|
||||
is EarnListUM.Error -> EarnErrorBlock(onRetryClick = earnListUM.onRetryClicked)
|
||||
EarnListUM.Loading -> EarnListPlaceholder(placeholderCount = PLACEHOLDER_ITEM_COUNT)
|
||||
EarnListUM.Empty -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.markets.MarketsListItemPlaceholder
|
||||
import com.tangem.common.ui.news.DefaultLoadingArticle
|
||||
import com.tangem.common.ui.news.TrendingLoadingArticle
|
||||
import com.tangem.features.feed.ui.feed.components.articles.DefaultLoadingArticle
|
||||
import com.tangem.features.feed.ui.feed.components.articles.TrendingLoadingArticle
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
|
|
|
|||
|
|
@ -4,12 +4,7 @@ import androidx.compose.animation.AnimatedContent
|
|||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
|
|
@ -19,6 +14,7 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.common.ui.markets.MarketsListItem
|
||||
|
|
@ -55,9 +51,13 @@ internal fun MarketBlock(marketChart: MarketChartUM?, feedListCallbacks: FeedLis
|
|||
text = stringResourceSafe(R.string.markets_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
},
|
||||
onSeeAllClick = { feedListCallbacks.onMarketOpenClick(SortByTypeUM.Rating) },
|
||||
shouldShowSeeAll = currentChart is MarketChartUM.Content,
|
||||
isLoading = currentChart is MarketChartUM.Loading,
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
|
|
@ -88,9 +88,13 @@ internal fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCall
|
|||
text = stringResourceSafe(R.string.markets_pulse_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
},
|
||||
onSeeAllClick = { onSeeAllClick() },
|
||||
shouldShowSeeAll = true,
|
||||
isLoading = marketChartConfig.marketCharts[marketChartConfig.currentSortByType] is MarketChartUM.Loading,
|
||||
)
|
||||
|
||||
LazyRow(
|
||||
|
|
|
|||
|
|
@ -2,28 +2,26 @@ package com.tangem.features.feed.ui.feed.components
|
|||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.onFirstVisible
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.news.ArticleCard
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.common.ui.news.ShowMoreArticlesCard
|
||||
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,16 +29,18 @@ import com.tangem.core.ui.components.UnableToLoadData
|
|||
import com.tangem.core.ui.components.block.BlockCard
|
||||
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.state.FeedListCallbacks
|
||||
import com.tangem.features.feed.ui.feed.state.NewsUM
|
||||
import com.tangem.features.feed.ui.feed.state.NewsUMState
|
||||
import com.tangem.features.feed.ui.feed.state.*
|
||||
|
||||
private const val FOURTH_ITEM_INDEX = 3
|
||||
internal const val FOURTH_ITEM_INDEX = 3
|
||||
private const val GRADIENT_START = 0f
|
||||
private const val GRADIENT_END = 0.5f
|
||||
private val LinearGradientFirstPart = Color(0xFF635EEC)
|
||||
private val LinearGradientSecondPart = Color(0xFFE05AED)
|
||||
private const val LINEAR_GRADIENT_FIRST_PART_V2 = 0xFFA3A0FF
|
||||
private const val LINEAR_GRADIENT_SECOND_PART_V2 = 0xFFF79DFF
|
||||
|
||||
private const val LINEAR_GRADIENT_FIRST_PART_V1 = 0xFF635EEC
|
||||
private const val LINEAR_GRADIENT_SECOND_PART_V1 = 0xFFE05AED
|
||||
|
||||
@Composable
|
||||
internal fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) {
|
||||
|
|
@ -64,13 +64,23 @@ internal fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trend
|
|||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) {
|
||||
val listState = rememberLazyListState()
|
||||
val articlesReadStatus = remember(news.content) {
|
||||
news.content.map { it.isViewed }
|
||||
val isRedesignEnabled = LocalRedesignEnabled.current
|
||||
val gradientStart = remember(isRedesignEnabled) {
|
||||
if (isRedesignEnabled) {
|
||||
Color(LINEAR_GRADIENT_FIRST_PART_V2)
|
||||
} else {
|
||||
Color(LINEAR_GRADIENT_FIRST_PART_V1)
|
||||
}
|
||||
}
|
||||
LaunchedEffect(articlesReadStatus) {
|
||||
listState.requestScrollToItem(0)
|
||||
|
||||
val gradientEnd = remember(isRedesignEnabled) {
|
||||
if (isRedesignEnabled) {
|
||||
Color(LINEAR_GRADIENT_SECOND_PART_V2)
|
||||
} else {
|
||||
Color(LINEAR_GRADIENT_SECOND_PART_V1)
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Header(
|
||||
title = {
|
||||
|
|
@ -95,8 +105,8 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM,
|
|||
withStyle(
|
||||
SpanStyle().copy(
|
||||
brush = Brush.linearGradient(
|
||||
GRADIENT_START to LinearGradientFirstPart,
|
||||
GRADIENT_END to LinearGradientSecondPart,
|
||||
GRADIENT_START to gradientStart,
|
||||
GRADIENT_END to gradientEnd,
|
||||
),
|
||||
),
|
||||
) {
|
||||
|
|
@ -104,10 +114,14 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM,
|
|||
}
|
||||
},
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
},
|
||||
onSeeAllClick = { feedListCallbacks.onOpenAllNews(false) },
|
||||
isLoading = news.newsUMState == NewsUMState.LOADING,
|
||||
shouldShowSeeAll = news.newsUMState == NewsUMState.CONTENT,
|
||||
)
|
||||
SpacerH(12.dp)
|
||||
|
||||
|
|
@ -125,48 +139,19 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM,
|
|||
}
|
||||
}
|
||||
|
||||
LazyRow(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
state = listState,
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = news.content,
|
||||
key = { _, article -> article.id },
|
||||
contentType = { _, _ -> "article" },
|
||||
) { index, article ->
|
||||
val articleModifier = if (index == FOURTH_ITEM_INDEX) {
|
||||
Modifier.onFirstVisible(
|
||||
minFractionVisible = 0.5f,
|
||||
callback = feedListCallbacks.onSliderScroll,
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
ArticleCard(
|
||||
articleConfigUM = article,
|
||||
onArticleClick = { feedListCallbacks.onArticleClick(article.id) },
|
||||
modifier = articleModifier
|
||||
.heightIn(min = 164.dp)
|
||||
.width(216.dp),
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
NewsSlider(
|
||||
NewsSliderConfig(
|
||||
callbacks = NewsSliderCallbacks(
|
||||
onOpenAllNews = { feedListCallbacks.onOpenAllNews(true) },
|
||||
onSliderScroll = feedListCallbacks.onSliderScroll,
|
||||
onSliderEndReached = feedListCallbacks.onSliderEndReached,
|
||||
onArticleClick = feedListCallbacks.onArticleClick,
|
||||
),
|
||||
content = news.content,
|
||||
shouldShowSeeAllNewsItem = true,
|
||||
),
|
||||
)
|
||||
|
||||
item(contentType = "show_more") {
|
||||
ShowMoreArticlesCard(
|
||||
modifier = Modifier
|
||||
.width(216.dp)
|
||||
.heightIn(min = 164.dp)
|
||||
.onFirstVisible(
|
||||
minFractionVisible = 0.5f,
|
||||
callback = feedListCallbacks.onSliderEndReached,
|
||||
),
|
||||
onClick = { feedListCallbacks.onOpenAllNews(true) },
|
||||
)
|
||||
}
|
||||
}
|
||||
SpacerH(32.dp)
|
||||
}
|
||||
}
|
||||
|
|
@ -181,10 +166,14 @@ private fun NewsErrorBlock(onRetryClick: () -> Unit) {
|
|||
text = stringResourceSafe(R.string.common_news),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
},
|
||||
onSeeAllClick = {},
|
||||
shouldShowSeeAll = false,
|
||||
isLoading = false,
|
||||
)
|
||||
SpacerH(12.dp)
|
||||
BlockCard(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
package com.tangem.features.feed.ui.feed.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onFirstVisible
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.HorizontalFadeWithBlur
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.components.haze.hazeSourceTangem
|
||||
import com.tangem.core.ui.extensions.conditionalCompose
|
||||
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.ui.feed.components.articles.ArticleCard
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ShowMoreArticlesCard
|
||||
import com.tangem.features.feed.ui.feed.state.NewsSliderConfig
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun NewsSlider(newsSliderConfig: NewsSliderConfig) {
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
val isRedesignEnabled = LocalRedesignEnabled.current
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.conditionalCompose(
|
||||
condition = isRedesignEnabled,
|
||||
modifier = {
|
||||
hazeSourceTangem(zIndex = -1f)
|
||||
},
|
||||
)
|
||||
.background(color = background),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
state = rememberLazyListState(),
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = newsSliderConfig.content,
|
||||
key = { index, _ -> index },
|
||||
contentType = { _, _ -> "article" },
|
||||
) { index, article ->
|
||||
val articleModifier = if (index == FOURTH_ITEM_INDEX) {
|
||||
Modifier.onFirstVisible(
|
||||
minFractionVisible = 0.5f,
|
||||
callback = newsSliderConfig.callbacks.onSliderScroll,
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
ArticleCard(
|
||||
articleConfigUM = article,
|
||||
onArticleClick = { newsSliderConfig.callbacks.onArticleClick(article.id) },
|
||||
modifier = articleModifier
|
||||
.width(228.dp)
|
||||
.heightIn(min = 172.dp)
|
||||
.fillMaxHeight(),
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
|
||||
if (newsSliderConfig.shouldShowSeeAllNewsItem) {
|
||||
item(contentType = "show_more") {
|
||||
ShowMoreArticlesCard(
|
||||
modifier = Modifier
|
||||
.width(228.dp)
|
||||
.heightIn(min = 172.dp)
|
||||
.onFirstVisible(
|
||||
minFractionVisible = 0.5f,
|
||||
callback = newsSliderConfig.callbacks.onSliderEndReached,
|
||||
),
|
||||
onClick = newsSliderConfig.callbacks.onOpenAllNews,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isRedesignEnabled) {
|
||||
HorizontalFadeWithBlur(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.heightIn(min = 172.dp)
|
||||
.fillMaxHeight()
|
||||
.width(100.dp),
|
||||
backgroundColor = background,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.features.feed.ui.feed.components.articles
|
||||
|
||||
import androidx.compose.material3.CardColors
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
|
||||
@Composable
|
||||
fun ArticleCard(
|
||||
articleConfigUM: ArticleConfigUM,
|
||||
onArticleClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
colors: CardColors = TangemBlockCardColors,
|
||||
) {
|
||||
if (LocalRedesignEnabled.current) {
|
||||
ArticleCardV2(
|
||||
articleConfigUM = articleConfigUM,
|
||||
onArticleClick = onArticleClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
ArticleCardV1(
|
||||
articleConfigUM = articleConfigUM,
|
||||
onArticleClick = onArticleClick,
|
||||
modifier = modifier,
|
||||
colors = colors,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,273 @@
|
|||
package com.tangem.features.feed.ui.feed.components.articles
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.CardColors
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerHMax
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
|
||||
@Composable
|
||||
internal fun ArticleCardV1(
|
||||
articleConfigUM: ArticleConfigUM,
|
||||
onArticleClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
colors: CardColors = TangemBlockCardColors,
|
||||
) {
|
||||
if (articleConfigUM.isTrending) {
|
||||
TrendingArticle(
|
||||
articleConfigUM = articleConfigUM,
|
||||
modifier = modifier,
|
||||
onArticleClick = onArticleClick,
|
||||
colors = colors,
|
||||
)
|
||||
} else {
|
||||
DefaultArticle(
|
||||
articleConfigUM = articleConfigUM,
|
||||
modifier = modifier,
|
||||
onArticleClick = onArticleClick,
|
||||
colors = colors,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrendingArticle(
|
||||
articleConfigUM: ArticleConfigUM,
|
||||
onArticleClick: () -> Unit,
|
||||
colors: CardColors,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(colors.containerColor)
|
||||
.clickable { onArticleClick() },
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.matchParentSize(),
|
||||
painter = painterResource(R.drawable.bg_trending_article),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillWidth,
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 24.dp, horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
text = stringResourceSafe(R.string.feed_trending_now),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
|
||||
Text(
|
||||
text = articleConfigUM.title,
|
||||
color = if (articleConfigUM.isViewed) {
|
||||
TangemTheme.colors.text.tertiary
|
||||
} else {
|
||||
TangemTheme.colors.text.primary1
|
||||
},
|
||||
style = TangemTheme.typography.h3,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
SpacerH(8.dp)
|
||||
|
||||
ArticleInfo(
|
||||
score = articleConfigUM.score,
|
||||
createdAt = articleConfigUM.createdAt.resolveReference(),
|
||||
)
|
||||
|
||||
SpacerH(32.dp)
|
||||
|
||||
Tags(
|
||||
modifier = Modifier.padding(horizontal = 46.dp),
|
||||
tags = articleConfigUM.tags.toImmutableList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun ShowMoreArticlesCardV1(modifier: Modifier = Modifier, onClick: () -> Unit) {
|
||||
BlockCard(
|
||||
modifier = modifier,
|
||||
onClick = onClick,
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(vertical = 31.dp, horizontal = 12.dp),
|
||||
) {
|
||||
Image(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_show_more_news_48),
|
||||
contentDescription = stringResourceSafe(R.string.common_show_more),
|
||||
)
|
||||
|
||||
SpacerH(16.dp)
|
||||
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.news_all_news),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.news_stay_in_the_loop),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DefaultArticle(
|
||||
articleConfigUM: ArticleConfigUM,
|
||||
onArticleClick: () -> Unit,
|
||||
colors: CardColors,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(colors.containerColor)
|
||||
.clickable { onArticleClick() }
|
||||
.padding(12.dp),
|
||||
) {
|
||||
ArticleInfo(
|
||||
score = articleConfigUM.score,
|
||||
createdAt = articleConfigUM.createdAt.resolveReference(),
|
||||
)
|
||||
|
||||
SpacerH(8.dp)
|
||||
|
||||
Text(
|
||||
text = articleConfigUM.title,
|
||||
color = if (articleConfigUM.isViewed) {
|
||||
TangemTheme.colors.text.tertiary
|
||||
} else {
|
||||
TangemTheme.colors.text.primary1
|
||||
},
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
SpacerH(16.dp)
|
||||
|
||||
SpacerHMax()
|
||||
|
||||
Tags(tags = articleConfigUM.tags.toImmutableList())
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun TagsPreview() {
|
||||
TangemThemePreview {
|
||||
Tags(
|
||||
tags = persistentListOf(
|
||||
LabelUM(TextReference.Str("Hype")),
|
||||
LabelUM(TextReference.Str("BTC")),
|
||||
LabelUM(TextReference.Str("Supply")),
|
||||
LabelUM(TextReference.Str("Demand")),
|
||||
LabelUM(TextReference.Str("Best rate")),
|
||||
LabelUM(TextReference.Str("Breaking news")),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360, backgroundColor = 0xFF000000)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ArticleCardsPreview() {
|
||||
val tags = listOf(
|
||||
LabelUM(TextReference.Str("Hype")),
|
||||
LabelUM(TextReference.Str("BTC")),
|
||||
LabelUM(TextReference.Str("Supply")),
|
||||
LabelUM(TextReference.Str("Demand")),
|
||||
LabelUM(TextReference.Str("Breaking news")),
|
||||
).toImmutableSet()
|
||||
|
||||
val config = ArticleConfigUM(
|
||||
id = 1,
|
||||
title = "Bitcoin ETFs log 4th straight day of inflows (+\$550M)",
|
||||
score = 9.5f,
|
||||
createdAt = TextReference.Str("1h ago"),
|
||||
isTrending = true,
|
||||
tags = tags,
|
||||
isViewed = false,
|
||||
)
|
||||
|
||||
TangemThemePreview {
|
||||
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
ArticleCard(
|
||||
articleConfigUM = config,
|
||||
onArticleClick = {},
|
||||
)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
ArticleCard(
|
||||
articleConfigUM = config.copy(isViewed = true),
|
||||
onArticleClick = {},
|
||||
)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
ArticleCard(
|
||||
articleConfigUM = config.copy(isTrending = false),
|
||||
onArticleClick = {},
|
||||
)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
ArticleCard(
|
||||
articleConfigUM = config.copy(isTrending = false, isViewed = true),
|
||||
onArticleClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,405 @@
|
|||
package com.tangem.features.feed.ui.feed.components.articles
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
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.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_NIGHT_NO
|
||||
import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_NIGHT_YES
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.LocalIsInDarkTheme
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
|
||||
@Composable
|
||||
internal fun ArticleCardV2(
|
||||
articleConfigUM: ArticleConfigUM,
|
||||
onArticleClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (articleConfigUM.isTrending) {
|
||||
TrendingArticle(
|
||||
modifier = modifier,
|
||||
articleConfigUM = articleConfigUM,
|
||||
onArticleClick = onArticleClick,
|
||||
)
|
||||
} else {
|
||||
DefaultArticle(
|
||||
modifier = modifier,
|
||||
articleConfigUM = articleConfigUM,
|
||||
onArticleClick = onArticleClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrendingArticle(
|
||||
articleConfigUM: ArticleConfigUM,
|
||||
onArticleClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TrendingArticleBackground(
|
||||
modifier = modifier,
|
||||
onClick = onArticleClick,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
DayAndRatingInfo(rating = stringReference("${articleConfigUM.score}"))
|
||||
|
||||
SpacerH(8.dp)
|
||||
|
||||
Text(
|
||||
text = articleConfigUM.title,
|
||||
color = if (articleConfigUM.isViewed) {
|
||||
TangemTheme.colors2.text.neutral.tertiary
|
||||
} else {
|
||||
TangemTheme.colors2.text.neutral.primary
|
||||
},
|
||||
style = TangemTheme.typography2.headingSemibold20,
|
||||
textAlign = TextAlign.Start,
|
||||
)
|
||||
|
||||
SpacerH(18.dp)
|
||||
|
||||
Text(
|
||||
text = articleConfigUM.createdAt.resolveReference(),
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
|
||||
SpacerH(18.dp)
|
||||
|
||||
Tags(tags = articleConfigUM.tags.toImmutableList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun ShowMoreArticlesCardV2(modifier: Modifier = Modifier, onClick: () -> Unit) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(color = TangemTheme.colors2.surface.level3)
|
||||
.clickable(onClick = onClick)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = TangemTheme.colors2.border.neutral.primary,
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
)
|
||||
.padding(vertical = 41.dp, horizontal = 16.dp),
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.size(40.dp),
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_show_more_news_48),
|
||||
contentDescription = stringResourceSafe(R.string.common_show_more),
|
||||
)
|
||||
|
||||
SpacerH(10.dp)
|
||||
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.news_all_news),
|
||||
style = TangemTheme.typography2.bodyRegular16,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
)
|
||||
|
||||
SpacerH(4.dp)
|
||||
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.news_stay_in_the_loop),
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DefaultArticle(
|
||||
articleConfigUM: ArticleConfigUM,
|
||||
onArticleClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(color = TangemTheme.colors2.surface.level3)
|
||||
.clickable(onClick = onArticleClick)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = TangemTheme.colors2.border.neutral.primary,
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
)
|
||||
.padding(16.dp),
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
RatingInfo(
|
||||
rating = stringReference("${articleConfigUM.score}"),
|
||||
isTrending = false,
|
||||
)
|
||||
}
|
||||
|
||||
SpacerH(8.dp)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = articleConfigUM.title,
|
||||
color = if (articleConfigUM.isViewed) {
|
||||
TangemTheme.colors2.text.neutral.tertiary
|
||||
} else {
|
||||
TangemTheme.colors2.text.neutral.primary
|
||||
},
|
||||
style = TangemTheme.typography2.bodyRegular16,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
SpacerH(8.dp)
|
||||
|
||||
Text(
|
||||
text = articleConfigUM.createdAt.resolveReference(),
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
|
||||
SpacerH(8.dp)
|
||||
|
||||
Tags(tags = articleConfigUM.tags.toImmutableList())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
private fun TrendingArticleBackground(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable BoxScope.() -> Unit,
|
||||
) {
|
||||
val isDarkTheme = LocalIsInDarkTheme.current
|
||||
|
||||
val bgColor = remember(isDarkTheme) {
|
||||
if (isDarkTheme) {
|
||||
Color(TRENDING_NIGHT_BG)
|
||||
} else {
|
||||
Color(TRENDING_LIGHT_BG)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.drawBehind {
|
||||
drawRect(bgColor)
|
||||
|
||||
val w = size.width
|
||||
val h = size.height
|
||||
val radiusScale = (w + h) / 2f
|
||||
|
||||
drawRect(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color(0xFF7C16F1).copy(alpha = .8f),
|
||||
Color.Transparent,
|
||||
),
|
||||
center = Offset(w / 2f, 2.4f * h),
|
||||
radius = radiusScale * 1.57f,
|
||||
),
|
||||
)
|
||||
|
||||
drawRect(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color(0xFF3360FF).copy(alpha = .7f),
|
||||
Color.Transparent,
|
||||
),
|
||||
center = Offset(w / 2f, 2.95f * h),
|
||||
radius = radiusScale * 1.9f,
|
||||
),
|
||||
)
|
||||
|
||||
drawRect(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color(0xFFFF9408).copy(alpha = .45f),
|
||||
Color.Transparent,
|
||||
),
|
||||
center = Offset(-0.38f * w, 2.1f * h),
|
||||
radius = radiusScale * 1.41f,
|
||||
),
|
||||
)
|
||||
|
||||
drawRect(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color(0xFFFC2424).copy(alpha = .5f),
|
||||
Color.Transparent,
|
||||
),
|
||||
center = Offset(1.188f * w, 2.37f * h),
|
||||
radius = radiusScale * 1.41f,
|
||||
),
|
||||
)
|
||||
}
|
||||
.border(width = 1.dp, color = bgColor.copy(.1f))
|
||||
.clickable(onClick = onClick),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DayAndRatingInfo(rating: TextReference, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
RatingInfo(rating = rating, isTrending = true)
|
||||
|
||||
SpacerW(8.dp)
|
||||
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.feed_trending_now),
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RatingInfo(rating: TextReference, isTrending: Boolean) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_wrapped_circle_star_16),
|
||||
tint = if (isTrending) {
|
||||
TangemTheme.colors2.fill.status.attention
|
||||
} else {
|
||||
TangemTheme.colors2.markers.iconGray
|
||||
},
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
SpacerW(2.dp)
|
||||
|
||||
Text(
|
||||
text = rating.resolveReference(),
|
||||
color = if (isTrending) {
|
||||
TangemTheme.colors2.text.status.attention
|
||||
} else {
|
||||
TangemTheme.colors2.text.neutral.secondary
|
||||
},
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
)
|
||||
}
|
||||
|
||||
private const val TRENDING_NIGHT_BG = 0xFF1F1F1F
|
||||
private const val TRENDING_LIGHT_BG = 0xFFFFFFFF
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun TagsPreview() {
|
||||
TangemThemePreviewRedesign {
|
||||
Tags(
|
||||
tags = persistentListOf(
|
||||
LabelUM(TextReference.Str("Hype")),
|
||||
LabelUM(TextReference.Str("BTC")),
|
||||
LabelUM(TextReference.Str("Supply")),
|
||||
LabelUM(TextReference.Str("Demand")),
|
||||
LabelUM(TextReference.Str("Best rate")),
|
||||
LabelUM(TextReference.Str("Breaking news")),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360, uiMode = UI_MODE_NIGHT_YES)
|
||||
@Preview(widthDp = 360, uiMode = UI_MODE_NIGHT_NO)
|
||||
@Composable
|
||||
private fun ArticleCardsPreview() {
|
||||
val tags = listOf(
|
||||
LabelUM(TextReference.Str("Hype")),
|
||||
LabelUM(TextReference.Str("BTC")),
|
||||
LabelUM(TextReference.Str("Supply")),
|
||||
LabelUM(TextReference.Str("Demand")),
|
||||
LabelUM(TextReference.Str("Breaking news")),
|
||||
).toImmutableSet()
|
||||
|
||||
val config = ArticleConfigUM(
|
||||
id = 1,
|
||||
title = "Bitcoin ETFs log 4th straight day of inflows (+\$550M)",
|
||||
score = 9.5f,
|
||||
createdAt = TextReference.Str("1h ago"),
|
||||
isTrending = true,
|
||||
tags = tags,
|
||||
isViewed = false,
|
||||
)
|
||||
|
||||
TangemThemePreviewRedesign {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
ArticleCardV2(
|
||||
articleConfigUM = config,
|
||||
onArticleClick = {},
|
||||
)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
ArticleCardV2(
|
||||
articleConfigUM = config.copy(isViewed = true),
|
||||
onArticleClick = {},
|
||||
)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
ArticleCardV2(
|
||||
articleConfigUM = config.copy(isTrending = false),
|
||||
onArticleClick = {},
|
||||
)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
ArticleCardV2(
|
||||
articleConfigUM = config.copy(isTrending = false, isViewed = true),
|
||||
onArticleClick = {},
|
||||
)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
ShowMoreArticlesCardV2(onClick = {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.features.feed.ui.feed.components.articles
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
|
||||
@Immutable
|
||||
data class ArticleConfigUM(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val score: Float,
|
||||
val createdAt: TextReference,
|
||||
val isTrending: Boolean,
|
||||
val tags: ImmutableSet<LabelUM>,
|
||||
val isViewed: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.features.feed.ui.feed.components.articles
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.label.Label
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun ArticleHeader(
|
||||
title: String,
|
||||
createdAt: String,
|
||||
score: Float,
|
||||
tags: ImmutableList<LabelUM>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
ArticleInfo(
|
||||
score = score,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Text(
|
||||
text = title,
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
|
||||
if (tags.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
tags.forEach { tag ->
|
||||
Label(
|
||||
state = tag,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.tangem.features.feed.ui.feed.components.articles
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
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.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
internal fun ArticleInfo(score: Float, createdAt: String, modifier: Modifier = Modifier) {
|
||||
val dotColor = TangemTheme.colors.text.secondary
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Image(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_start_circle_12),
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = score.toString(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.size(4.dp)
|
||||
.drawWithCache {
|
||||
val radius = size.minDimension / 2f
|
||||
onDrawBehind {
|
||||
drawCircle(
|
||||
color = dotColor,
|
||||
radius = radius,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Text(
|
||||
text = createdAt,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
package com.tangem.features.feed.ui.feed.components.articles
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
|
||||
@Composable
|
||||
fun TrendingLoadingArticle(modifier: Modifier = Modifier) {
|
||||
if (LocalRedesignEnabled.current) {
|
||||
TrendingLoadingArticleV2(modifier)
|
||||
} else {
|
||||
TrendingLoadingArticleV1(modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrendingLoadingArticleV1(modifier: Modifier = Modifier) {
|
||||
BlockCard(
|
||||
modifier = modifier,
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 24.dp, horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
RectangleShimmer(modifier = Modifier.size(width = 96.dp, height = 24.dp), radius = 8.dp)
|
||||
SpacerH(12.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 285.dp, height = 18.dp), radius = 4.dp)
|
||||
SpacerH(6.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 190.dp, height = 18.dp), radius = 4.dp)
|
||||
SpacerH(14.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 110.dp, height = 18.dp), radius = 4.dp)
|
||||
SpacerH(32.dp)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrendingLoadingArticleV2(modifier: Modifier = Modifier) {
|
||||
BlockCard(
|
||||
modifier = modifier,
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors2.surface.level3),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
) {
|
||||
RectangleShimmer(modifier = Modifier.size(width = 46.dp, height = 16.dp), radius = TangemTheme.dimens2.x25)
|
||||
SpacerH(8.dp)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(24.dp),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
SpacerH(48.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 46.dp, height = 16.dp), radius = TangemTheme.dimens2.x25)
|
||||
SpacerH(12.dp)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.size(width = 58.dp, height = 24.dp),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.size(width = 58.dp, height = 24.dp),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.size(width = 58.dp, height = 24.dp),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DefaultLoadingArticle(modifier: Modifier = Modifier) {
|
||||
if (LocalRedesignEnabled.current) {
|
||||
DefaultLoadingArticleV2(modifier)
|
||||
} else {
|
||||
DefaultLoadingArticleV1(modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DefaultLoadingArticleV1(modifier: Modifier = Modifier) {
|
||||
BlockCard(
|
||||
modifier = modifier,
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
) {
|
||||
RectangleShimmer(modifier = Modifier.size(width = 46.dp, height = 16.dp), radius = TangemTheme.dimens2.x25)
|
||||
SpacerH(8.dp)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(20.dp),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
SpacerH(28.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 46.dp, height = 16.dp), radius = TangemTheme.dimens2.x25)
|
||||
SpacerH(8.dp)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.size(width = 72.dp, height = 24.dp),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.size(width = 62.dp, height = 24.dp),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.size(width = 32.dp, height = 24.dp),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DefaultLoadingArticleV2(modifier: Modifier = Modifier) {
|
||||
BlockCard(
|
||||
modifier = modifier,
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors2.surface.level3),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
) {
|
||||
RectangleShimmer(modifier = Modifier.size(width = 46.dp, height = 16.dp), radius = TangemTheme.dimens2.x25)
|
||||
SpacerH(8.dp)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(20.dp),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
SpacerH(44.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 46.dp, height = 16.dp), radius = TangemTheme.dimens2.x25)
|
||||
SpacerH(12.dp)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.size(width = 72.dp, height = 24.dp),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.size(width = 62.dp, height = 24.dp),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.size(width = 32.dp, height = 24.dp),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun TrendingLoadingArticlePreviewV1() {
|
||||
TangemThemePreview {
|
||||
TrendingLoadingArticle()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun TrendingLoadingArticlePreviewV2() {
|
||||
TangemThemePreviewRedesign {
|
||||
TrendingLoadingArticle()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun DefaultLoadingArticlePreviewV1() {
|
||||
TangemThemePreview {
|
||||
DefaultLoadingArticle()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun DefaultLoadingArticlePreviewV2() {
|
||||
TangemThemePreviewRedesign {
|
||||
DefaultLoadingArticle()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.features.feed.ui.feed.components.articles
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
|
||||
@Composable
|
||||
fun ShowMoreArticlesCard(modifier: Modifier = Modifier, onClick: () -> Unit) {
|
||||
val isRedesignEnabled: Boolean = LocalRedesignEnabled.current
|
||||
if (isRedesignEnabled) {
|
||||
ShowMoreArticlesCardV2(modifier = modifier, onClick = onClick)
|
||||
} else {
|
||||
ShowMoreArticlesCardV1(modifier = modifier, onClick = onClick)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
package com.tangem.features.feed.ui.feed.components.articles
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.Placeable
|
||||
import androidx.compose.ui.layout.SubcomposeLayout
|
||||
import androidx.compose.ui.layout.SubcomposeMeasureScope
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.label.Label
|
||||
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.ds.badge.*
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.utils.StringsSigns
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
private val TAG_SPACING = 4.dp
|
||||
|
||||
/**
|
||||
* A layout that displays a list of tags, automatically calculating how many can fit
|
||||
* in a single line. If not all tags fit, it shows an overflow indicator (e.g., "+N").
|
||||
*
|
||||
* @param tags The immutable list of [LabelUM] to display.
|
||||
* @param modifier The modifier to be applied to the layout.
|
||||
*/
|
||||
@Composable
|
||||
internal fun Tags(tags: ImmutableList<LabelUM>, modifier: Modifier = Modifier) {
|
||||
if (tags.isEmpty()) return
|
||||
|
||||
SubcomposeLayout(modifier = modifier) { constraints ->
|
||||
val spacingPx = TAG_SPACING.roundToPx()
|
||||
|
||||
val tagPlaceables = subcompose(ContentSlot.Tags) {
|
||||
tags.forEach { tag ->
|
||||
if (LocalRedesignEnabled.current) {
|
||||
TangemBadge(
|
||||
text = tag.text,
|
||||
tangemIconUM = when (val content = tag.leadingContent) {
|
||||
LabelLeadingContentUM.None -> null
|
||||
is LabelLeadingContentUM.Token -> TangemIconUM.Url(content.iconUrl)
|
||||
},
|
||||
shape = TangemBadgeShape.Rounded,
|
||||
size = TangemBadgeSize.X6,
|
||||
type = TangemBadgeType.Tinted,
|
||||
color = TangemBadgeColor.Gray,
|
||||
iconPosition = when (tag.leadingContent) {
|
||||
LabelLeadingContentUM.None -> TangemBadgeIconPosition.None
|
||||
is LabelLeadingContentUM.Token -> TangemBadgeIconPosition.Start
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Label(state = tag)
|
||||
}
|
||||
}
|
||||
}.map { it.measure(constraints) }
|
||||
|
||||
val layoutInfo = calculateLayoutInfo(
|
||||
maxWidth = constraints.maxWidth,
|
||||
spacingPx = spacingPx,
|
||||
tagPlaceables = tagPlaceables,
|
||||
)
|
||||
|
||||
val overflowPlaceable = layoutInfo.overflowCount.takeIf { it > 0 }?.let {
|
||||
subcompose(ContentSlot.Overflow) {
|
||||
OverflowLabel(count = it)
|
||||
}.first().measure(constraints)
|
||||
}
|
||||
|
||||
val visiblePlaceables = tagPlaceables.take(layoutInfo.visibleCount)
|
||||
val allVisibleItems = visiblePlaceables + listOfNotNull(overflowPlaceable)
|
||||
|
||||
val width = calculateRowWidth(
|
||||
placeables = allVisibleItems,
|
||||
spacingPx = spacingPx,
|
||||
).coerceIn(constraints.minWidth, constraints.maxWidth)
|
||||
|
||||
val height = allVisibleItems.maxOfOrNull { it.height } ?: 0
|
||||
|
||||
layout(width = width, height = height) {
|
||||
var xPosition = 0
|
||||
allVisibleItems.forEachIndexed { index, placeable ->
|
||||
if (index > 0) {
|
||||
xPosition += spacingPx
|
||||
}
|
||||
val yPosition = height - placeable.height
|
||||
placeable.placeRelative(xPosition, yPosition)
|
||||
xPosition += placeable.width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates how many tags can be displayed and how many are overflowing.
|
||||
*/
|
||||
private fun SubcomposeMeasureScope.calculateLayoutInfo(
|
||||
maxWidth: Int,
|
||||
spacingPx: Int,
|
||||
tagPlaceables: List<Placeable>,
|
||||
): TagsLayoutInfo {
|
||||
val totalWidthWithoutOverflow = calculateRowWidth(tagPlaceables, spacingPx)
|
||||
|
||||
if (totalWidthWithoutOverflow <= maxWidth) {
|
||||
return TagsLayoutInfo(visibleCount = tagPlaceables.size, overflowCount = 0)
|
||||
}
|
||||
|
||||
for (count in tagPlaceables.indices.reversed()) {
|
||||
val visibleTags = tagPlaceables.take(count)
|
||||
val overflowCount = tagPlaceables.size - count
|
||||
|
||||
val tempOverflowPlaceable = subcompose("overflow_temp_$overflowCount") {
|
||||
OverflowLabel(count = overflowCount)
|
||||
}.first().measure(Constraints())
|
||||
|
||||
val widthWithOverflow = calculateRowWidth(
|
||||
placeables = visibleTags + tempOverflowPlaceable,
|
||||
spacingPx = spacingPx,
|
||||
)
|
||||
|
||||
if (widthWithOverflow <= maxWidth) {
|
||||
return TagsLayoutInfo(visibleCount = count, overflowCount = overflowCount)
|
||||
}
|
||||
}
|
||||
|
||||
return TagsLayoutInfo(visibleCount = 0, overflowCount = tagPlaceables.size)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OverflowLabel(count: Int) {
|
||||
if (LocalRedesignEnabled.current) {
|
||||
TangemBadge(
|
||||
text = TextReference.Str("${StringsSigns.PLUS}$count"),
|
||||
shape = TangemBadgeShape.Rounded,
|
||||
size = TangemBadgeSize.X6,
|
||||
type = TangemBadgeType.Tinted,
|
||||
color = TangemBadgeColor.Gray,
|
||||
)
|
||||
} else {
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("${StringsSigns.PLUS}$count"),
|
||||
maxLines = 1,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateRowWidth(placeables: List<Placeable>, spacingPx: Int): Int {
|
||||
if (placeables.isEmpty()) return 0
|
||||
val tagsWidth = placeables.sumOf { it.width }
|
||||
val spacingWidth = spacingPx * (placeables.size - 1).coerceAtLeast(0)
|
||||
return tagsWidth + spacingWidth
|
||||
}
|
||||
|
||||
private data class TagsLayoutInfo(val visibleCount: Int, val overflowCount: Int)
|
||||
|
||||
private enum class ContentSlot { Tags, Overflow }
|
||||
|
|
@ -2,7 +2,7 @@ package com.tangem.features.feed.ui.feed.preview
|
|||
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.features.feed.ui.feed.state
|
|||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnListUM
|
||||
|
|
@ -18,7 +18,7 @@ internal data class FeedListUM(
|
|||
val trendingArticle: ArticleConfigUM?,
|
||||
val marketChartConfig: MarketChartConfig,
|
||||
val globalState: GlobalFeedState = GlobalFeedState.Content,
|
||||
val earnListUM: EarnListUM?,
|
||||
val earnListUM: EarnListUM,
|
||||
)
|
||||
|
||||
internal data class FeedListCallbacks(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.feed.ui.feed.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
internal data class NewsSliderConfig(
|
||||
val shouldShowSeeAllNewsItem: Boolean,
|
||||
val content: ImmutableList<ArticleConfigUM>,
|
||||
val callbacks: NewsSliderCallbacks,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
internal data class NewsSliderCallbacks(
|
||||
val onOpenAllNews: () -> Unit,
|
||||
val onSliderScroll: () -> Unit,
|
||||
val onSliderEndReached: () -> Unit,
|
||||
val onArticleClick: (id: Int) -> Unit,
|
||||
)
|
||||
|
|
@ -42,7 +42,6 @@ import com.tangem.core.ui.res.TangemThemePreview
|
|||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.market.detailed.components.*
|
||||
import com.tangem.core.ui.R as CoreR
|
||||
import com.tangem.features.feed.ui.market.detailed.preview.MarketsTokenDetailsPreview
|
||||
import com.tangem.features.feed.ui.market.detailed.state.ExchangesBottomSheetContent
|
||||
import com.tangem.features.feed.ui.market.detailed.state.InfoBottomSheetContent
|
||||
|
|
@ -50,13 +49,13 @@ 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.coroutines.flow.distinctUntilChanged
|
||||
import com.tangem.core.ui.R as CoreR
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
internal fun MarketsTokenDetailsContent(
|
||||
state: MarketsTokenDetailsUM,
|
||||
backgroundColor: Color,
|
||||
isAccountEnabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
portfolioBlock: @Composable ((Modifier) -> Unit)?,
|
||||
) {
|
||||
|
|
@ -65,7 +64,6 @@ internal fun MarketsTokenDetailsContent(
|
|||
backgroundColor = backgroundColor,
|
||||
state = state,
|
||||
portfolioBlock = portfolioBlock,
|
||||
isAccountEnabled = isAccountEnabled,
|
||||
)
|
||||
|
||||
when (state.bottomSheetConfig.content) {
|
||||
|
|
@ -80,7 +78,6 @@ internal fun MarketsTokenDetailsContent(
|
|||
private fun Content(
|
||||
state: MarketsTokenDetailsUM,
|
||||
backgroundColor: Color,
|
||||
isAccountEnabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
portfolioBlock: @Composable ((Modifier) -> Unit)?,
|
||||
) {
|
||||
|
|
@ -134,7 +131,6 @@ private fun Content(
|
|||
|
||||
tokenMarketDetailsBody(
|
||||
state = state.body,
|
||||
isAccountEnabled = isAccountEnabled,
|
||||
portfolioBlock = portfolioBlock,
|
||||
relatedNews = state.relatedNews,
|
||||
)
|
||||
|
|
@ -322,7 +318,6 @@ private fun MarketsTokenDetailsContent_Preview(
|
|||
state = params,
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
portfolioBlock = {},
|
||||
isAccountEnabled = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,34 +2,27 @@ package com.tangem.features.feed.ui.market.detailed.components
|
|||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onFirstVisible
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.news.ArticleCard
|
||||
import com.tangem.core.ui.components.UnableToLoadData
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.components.items.DescriptionItem
|
||||
import com.tangem.core.ui.components.items.DescriptionPlaceholder
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.feed.components.NewsSlider
|
||||
import com.tangem.features.feed.ui.feed.state.NewsSliderCallbacks
|
||||
import com.tangem.features.feed.ui.feed.state.NewsSliderConfig
|
||||
import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM
|
||||
import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM.RelatedNews
|
||||
|
||||
private const val FOURTH_ITEM_INDEX = 3
|
||||
|
||||
@Suppress("CanBeNonNullable") // TODO will be removed after [REDACTED_JIRA]
|
||||
@Suppress("CanBeNonNullable")
|
||||
internal fun LazyListScope.tokenMarketDetailsBody(
|
||||
state: MarketsTokenDetailsUM.Body,
|
||||
isAccountEnabled: Boolean,
|
||||
portfolioBlock: @Composable ((Modifier) -> Unit)?,
|
||||
relatedNews: RelatedNews,
|
||||
) {
|
||||
|
|
@ -45,9 +38,7 @@ internal fun LazyListScope.tokenMarketDetailsBody(
|
|||
}
|
||||
}
|
||||
|
||||
if (isAccountEnabled) {
|
||||
aboutCoinHeader()
|
||||
}
|
||||
aboutCoinHeader()
|
||||
|
||||
loadingInfoBlocks()
|
||||
}
|
||||
|
|
@ -66,9 +57,7 @@ internal fun LazyListScope.tokenMarketDetailsBody(
|
|||
relatedNews(relatedNews)
|
||||
}
|
||||
|
||||
if (isAccountEnabled) {
|
||||
aboutCoinHeader()
|
||||
}
|
||||
aboutCoinHeader()
|
||||
|
||||
infoBlocksList(state.infoBlocks)
|
||||
}
|
||||
|
|
@ -207,13 +196,6 @@ private fun LazyListScope.loadingInfoBlocks() {
|
|||
|
||||
private fun LazyListScope.relatedNews(relatedNews: RelatedNews) {
|
||||
item("related-news") {
|
||||
val listState = rememberLazyListState()
|
||||
val articlesReadStatus = remember(relatedNews.articles) {
|
||||
relatedNews.articles.map { it.isViewed }
|
||||
}
|
||||
LaunchedEffect(articlesReadStatus) {
|
||||
listState.requestScrollToItem(0)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -231,35 +213,18 @@ private fun LazyListScope.relatedNews(relatedNews: RelatedNews) {
|
|||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
|
||||
LazyRow(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
state = listState,
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = relatedNews.articles,
|
||||
key = { index, article -> article.id },
|
||||
) { index, article ->
|
||||
val articleModifier = if (index == FOURTH_ITEM_INDEX) {
|
||||
Modifier.onFirstVisible(
|
||||
minFractionVisible = 0.5f,
|
||||
callback = relatedNews.onScroll,
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
ArticleCard(
|
||||
articleConfigUM = article,
|
||||
onArticleClick = { relatedNews.onArticledClicked(article.id) },
|
||||
modifier = articleModifier
|
||||
.heightIn(min = 164.dp)
|
||||
.width(216.dp),
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
}
|
||||
NewsSlider(
|
||||
NewsSliderConfig(
|
||||
callbacks = NewsSliderCallbacks(
|
||||
onOpenAllNews = {}, // not applicable here
|
||||
onSliderScroll = relatedNews.onScroll,
|
||||
onSliderEndReached = {}, // not applicable here
|
||||
onArticleClick = relatedNews.onArticledClicked,
|
||||
),
|
||||
content = relatedNews.articles,
|
||||
shouldShowSeeAllNewsItem = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ 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.common.ui.news.ArticleConfigUM
|
||||
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
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ import androidx.compose.ui.unit.dp
|
|||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.CachePolicy
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.common.ui.news.ArticleHeader
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleHeader
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
|
||||
|
|
|
|||
|
|
@ -1,23 +1,45 @@
|
|||
package com.tangem.features.feed.ui.news.details.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.VerticalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
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.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
|
||||
@Composable
|
||||
fun NewsDetailsPlaceholder(background: Color, modifier: Modifier = Modifier) {
|
||||
if (LocalRedesignEnabled.current) {
|
||||
NewsDetailsPlaceholderV2(background, modifier)
|
||||
} else {
|
||||
NewsDetailsPlaceholderV1(background, modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun NewsDetailsPlaceholderV1(background: Color, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize().background(background).padding(16.dp),
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(background)
|
||||
.padding(16.dp),
|
||||
) {
|
||||
RectangleShimmer(modifier = Modifier.size(width = 112.dp, height = 20.dp))
|
||||
SpacerH(8.dp)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.fillMaxWidth().height(28.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(28.dp),
|
||||
)
|
||||
SpacerH(4.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(height = 28.dp, width = 208.dp))
|
||||
|
|
@ -29,29 +51,178 @@ fun NewsDetailsPlaceholder(background: Color, modifier: Modifier = Modifier) {
|
|||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.fillMaxWidth().height(20.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(20.dp),
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.fillMaxWidth().height(20.dp).padding(end = 30.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(20.dp)
|
||||
.padding(end = 30.dp),
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.fillMaxWidth().height(20.dp).padding(end = 30.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(20.dp)
|
||||
.padding(end = 30.dp),
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.fillMaxWidth().height(20.dp).padding(end = 24.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(20.dp)
|
||||
.padding(end = 24.dp),
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.fillMaxWidth().height(20.dp).padding(end = 70.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(20.dp)
|
||||
.padding(end = 70.dp),
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.fillMaxWidth().height(20.dp).padding(end = 30.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(20.dp)
|
||||
.padding(end = 30.dp),
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.fillMaxWidth().height(20.dp).padding(end = 96.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(20.dp)
|
||||
.padding(end = 96.dp),
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.fillMaxWidth().height(20.dp).padding(end = 100.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(20.dp)
|
||||
.padding(end = 100.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun NewsDetailsPlaceholderV2(background: Color, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(background)
|
||||
.padding(16.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.height(50.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(30.dp),
|
||||
) {
|
||||
Column {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.size(width = 50.dp, height = 20.dp),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
SpacerH(10.dp)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.size(width = 90.dp, height = 18.dp),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
}
|
||||
|
||||
VerticalDivider(color = TangemTheme.colors2.border.neutral.primary)
|
||||
|
||||
Column {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.size(width = 50.dp, height = 20.dp),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
SpacerH(10.dp)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.size(width = 90.dp, height = 18.dp),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SpacerH(36.dp)
|
||||
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(40.dp),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(40.dp)
|
||||
.padding(end = 106.dp),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
|
||||
SpacerH(36.dp)
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.size(width = 98.dp, height = 36.dp),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.size(width = 98.dp, height = 36.dp),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
}
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(20.dp)
|
||||
.padding(end = 22.dp),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
SpacerH(12.dp)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(20.dp)
|
||||
.padding(end = 66.dp),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
SpacerH(12.dp)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(20.dp)
|
||||
.padding(end = 18.dp),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
SpacerH(12.dp)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(20.dp)
|
||||
.padding(end = 46.dp),
|
||||
radius = TangemTheme.dimens2.x25,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun NewsDetailsPlaceholderPreviewV1() {
|
||||
TangemThemePreview {
|
||||
NewsDetailsPlaceholder(background = TangemTheme.colors.background.tertiary)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun NewsDetailsPlaceholderPreviewV2() {
|
||||
TangemThemePreviewRedesign {
|
||||
NewsDetailsPlaceholder(background = TangemTheme.colors2.surface.level3)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
package com.tangem.features.feed.ui.news.details.components
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.VerticalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.stringResourceSafe
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
internal fun QuickRecap(content: String, modifier: Modifier = Modifier) {
|
||||
if (LocalRedesignEnabled.current) {
|
||||
QuickRecapV2(content, modifier)
|
||||
} else {
|
||||
QuickRecapV1(content, modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun QuickRecapV1(content: String, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier.height(IntrinsicSize.Min),
|
||||
) {
|
||||
VerticalDivider(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(start = 8.dp),
|
||||
thickness = 2.dp,
|
||||
color = TangemTheme.colors.stroke.primary,
|
||||
)
|
||||
Column(modifier = Modifier.padding(start = 20.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_quick_recap_16),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.news_quick_recap),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.accent,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
text = content,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun QuickRecapV2(content: String, modifier: Modifier = Modifier) {
|
||||
Column(modifier = modifier.height(IntrinsicSize.Min)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Image(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_stars_20),
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
SpacerW(2.dp)
|
||||
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
withStyle(
|
||||
SpanStyle().copy(
|
||||
brush = Brush.linearGradient(
|
||||
GRADIENT_START to Color(LINEAR_GRADIENT_FIRST_PART),
|
||||
GRADIENT_END to Color(LINEAR_GRADIENT_SECOND_PART),
|
||||
),
|
||||
),
|
||||
) {
|
||||
append(stringResourceSafe(R.string.news_quick_recap))
|
||||
}
|
||||
},
|
||||
style = TangemTheme.typography2.bodyRegular14,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
|
||||
SpacerH(10.dp)
|
||||
|
||||
Box {
|
||||
VerticalDivider(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
thickness = 2.dp,
|
||||
color = Color(QUICK_RECAP_DIVIDER_COLOR),
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
text = content,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val QUICK_RECAP_DIVIDER_COLOR = 0xFFA99FFF
|
||||
private const val LINEAR_GRADIENT_FIRST_PART = 0xFFA3A0FF
|
||||
private const val LINEAR_GRADIENT_SECOND_PART = 0xFFF79DFF
|
||||
private const val GRADIENT_START = 0f
|
||||
private const val GRADIENT_END = 0.5f
|
||||
|
|
@ -12,13 +12,15 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.chip.Chip
|
||||
import com.tangem.core.ui.components.chip.entity.ChipUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.ds.tabs.TangemTab
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.feed.ui.news.list.components.NewsListLazyColumn
|
||||
import com.tangem.features.feed.ui.news.list.state.NewsListState
|
||||
|
|
@ -29,6 +31,7 @@ import kotlinx.collections.immutable.toImmutableSet
|
|||
@Composable
|
||||
internal fun NewsListContent(state: NewsListUM, modifier: Modifier = Modifier) {
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
val isRedesignEnabled = LocalRedesignEnabled.current
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
Column(
|
||||
|
|
@ -44,7 +47,17 @@ internal fun NewsListContent(state: NewsListUM, modifier: Modifier = Modifier) {
|
|||
items = state.filters,
|
||||
key = { it.id },
|
||||
) { filter ->
|
||||
Chip(state = filter)
|
||||
if (isRedesignEnabled) {
|
||||
TangemTab(
|
||||
text = filter.text,
|
||||
isChecked = filter.isSelected,
|
||||
onCheckedChange = {
|
||||
filter.onClick()
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Chip(state = filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.news.ArticleCard
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.common.ui.news.DefaultLoadingArticle
|
||||
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.components.articles.DefaultLoadingArticle
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.UnableToLoadData
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
|
|
@ -112,7 +112,9 @@ private fun Content(
|
|||
key = ArticleConfigUM::id,
|
||||
) { article ->
|
||||
ArticleCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier
|
||||
.heightIn(min = 152.dp)
|
||||
.fillMaxWidth(),
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
articleConfigUM = article,
|
||||
onArticleClick = {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.features.feed.ui.news.list.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.core.ui.components.chip.entity.ChipUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import dagger.assisted.AssistedInject
|
|||
|
||||
/**
|
||||
* Mocking it for release/external builds to exclude SumSub dependency
|
||||
* This will never be called if the FT [isTangemPayEnabled] is off
|
||||
*/
|
||||
@Suppress("UnusedPrivateProperty")
|
||||
internal class MockKycComponent @AssistedInject constructor(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.serialization)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.managetokens.component
|
|||
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
enum class ManageTokensSource(val analyticsName: String) {
|
||||
STORIES(analyticsName = "Stories"),
|
||||
|
|
@ -12,18 +13,15 @@ enum class ManageTokensSource(val analyticsName: String) {
|
|||
}
|
||||
|
||||
sealed interface ManageTokensMode {
|
||||
data class Wallet(val userWalletId: UserWalletId) : ManageTokensMode
|
||||
data class Account(val accountId: AccountId) : ManageTokensMode
|
||||
data class Account(val accountId: AccountId) : ManageTokensMode {
|
||||
constructor(userWalletId: UserWalletId) : this(AccountId.forMainCryptoPortfolio(userWalletId))
|
||||
}
|
||||
data object None : ManageTokensMode
|
||||
}
|
||||
|
||||
sealed interface AddCustomTokenMode {
|
||||
@Serializable
|
||||
data class AddCustomTokenMode(val accountId: AccountId) {
|
||||
val userWalletId: UserWalletId = accountId.userWalletId
|
||||
|
||||
val userWalletId: UserWalletId
|
||||
|
||||
data class Wallet(override val userWalletId: UserWalletId) : AddCustomTokenMode
|
||||
|
||||
data class Account(val accountId: AccountId) : AddCustomTokenMode {
|
||||
override val userWalletId: UserWalletId = accountId.userWalletId
|
||||
}
|
||||
constructor(userWalletId: UserWalletId) : this(AccountId.forMainCryptoPortfolio(userWalletId))
|
||||
}
|
||||
|
|
@ -4,47 +4,21 @@
|
|||
<CurrentIssues>
|
||||
<ID>BooleanPropertyNaming:AddCustomTokenUM.kt$AddCustomTokenUM$val showBackButton: Boolean</ID>
|
||||
<ID>BooleanPropertyNaming:CustomCurrencyValidator.kt$CustomCurrencyValidator.Status.Validated$val fillForm: Boolean</ID>
|
||||
<ID>BooleanPropertyNaming:CustomTokenFormModel.kt$CustomTokenFormModel$val needColdWalletInteraction = coldWalletAndHasMissedDerivationsUseCase.invoke( userWalletId = params.mode.userWalletId, networksWithDerivationPath = mapOf(currency.network.backendId to getDerivationPath().value), )</ID>
|
||||
<ID>BooleanPropertyNaming:ManageTokensListManager.kt$ManageTokensListManager$val loadUserTokensFromRemote = when (mode) { is ManageTokensMode.Wallet, is ManageTokensMode.Account, -> source == ManageTokensSource.ONBOARDING ManageTokensMode.None, -> false }</ID>
|
||||
<ID>BooleanPropertyNaming:ManageTokensModel.kt$ManageTokensModel$val needToInteractWithColdWallet = useCasesFacade.needColdWalletInteraction(networks)</ID>
|
||||
<ID>BooleanPropertyNaming:ManageTokensUM.kt$ManageTokensUM.ManageContent$val needToInteractWithColdWallet: Boolean</ID>
|
||||
<ID>BooleanPropertyNaming:OnboardingManageTokensModel.kt$OnboardingManageTokensModel$val showTangemIcon = useCasesFacade.needColdWalletInteraction(network = network)</ID>
|
||||
<ID>BooleanPropertyNaming:OnboardingManageTokensUM.kt$OnboardingManageTokensUM.ActionButtonConfig$abstract val showProgress: Boolean</ID>
|
||||
<ID>BooleanPropertyNaming:OnboardingManageTokensUM.kt$OnboardingManageTokensUM.ActionButtonConfig.Continue$val showTangemIcon: Boolean</ID>
|
||||
<ID>MaxChainedCallsOnSameLine:ChooseManagedTokensModel.kt$ChooseManagedTokensModel$params.initialCurrency.network.id.rawId.value</ID>
|
||||
<ID>MultilineLambdaItParameter:ChooseManagedTokenContent.kt${ add( CurrencyItemUM.Basic( id = ManagedCryptoCurrency.ID( value = "ID+$it", ), name = "Bitcoin", symbol = "BTC", icon = CurrencyIconState.Loading, networks = CurrencyItemUM.Basic.NetworksUM.Collapsed, onExpandClick = {}, ), ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:ChooseManagedTokensModel.kt$ChooseManagedTokensModel${ it.copy( notificationUM = null, ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:CurrencyItemMapper.kt${ it.toCurrencyNetworkModel( isSelected = it.network in addedIn, isEditable = false, onSelectedStateChange = { _, _ -> }, onLongTap = { _ -> }, ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:CustomCurrencyFormOperations.kt${ it[Field.CONTRACT_ADDRESS] = it.getValue(Field.CONTRACT_ADDRESS).copy( error = when (exception) { CustomTokenFormValidationException.ContractAddress.Invalid -> { resourceReference(R.string.custom_token_creation_error_invalid_contract_address) } }, ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:CustomCurrencyFormOperations.kt${ it[Field.DECIMALS] = it.getValue(Field.DECIMALS).copy( error = when (exception) { is CustomTokenFormValidationException.Decimals.Empty -> { null // Should not display this error } is CustomTokenFormValidationException.Decimals.Invalid -> { resourceReference( R.string.custom_token_creation_error_wrong_decimals, wrappedList(ValidateTokenFormUseCase.MAX_DECIMALS), ) } }, ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:CustomTokenFormContent.kt$PreviewCustomTokenFormComponentProvider${ it[Field.CONTRACT_ADDRESS] = it[Field.CONTRACT_ADDRESS]!!.copy( label = stringReference("Contract address"), value = "0x1234567890", error = stringReference("Contract address is invalid"), placeholder = stringReference("0x1234567890"), ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:CustomTokenFormContent.kt$PreviewCustomTokenFormComponentProvider${ it[Field.CONTRACT_ADDRESS] = it[Field.CONTRACT_ADDRESS]!!.copy( label = stringReference("Contract address"), value = "0x1234567890", placeholder = stringReference("0x1234567890"), ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:CustomTokenFormModel.kt$CustomTokenFormModel${ Timber.e(it, "Failed to add currency") showErrorDialog() return@resource }</ID>
|
||||
<ID>MultilineLambdaItParameter:CustomTokenFormModel.kt$CustomTokenFormModel${ Timber.e(it, "Failed to derive public keys") showErrorDialog() return@resource }</ID>
|
||||
<ID>MultilineLambdaItParameter:CustomTokenSelectorModel.kt$CustomTokenSelectorModel${ when (it) { is AccountStatus.CryptoPortfolio -> it.sameNodeAndNotMain() } }</ID>
|
||||
<ID>MultilineLambdaItParameter:ManageTokensListManager.kt$ManageTokensListManager${ Timber.e( it, """ Failed to check currency unsupported state |- Mode: $mode |- Source Network: $sourceNetwork """.trimIndent(), ) val message = SnackbarMessage( message = it.localizedMessage ?.let(::stringReference) ?: resourceReference(R.string.common_error), ) messageSender.send(message) null }</ID>
|
||||
<ID>MultilineLambdaItParameter:ManageTokensListManager.kt$ManageTokensListManager${ Timber.e( it, """ Failed to check linked tokens |- Mode: $mode |- Network ID: ${network.id} """.trimIndent(), ) val message = SnackbarMessage( message = it.localizedMessage ?.let(::stringReference) ?: resourceReference(R.string.common_error), ) messageSender.send(message) false }</ID>
|
||||
<ID>MultilineLambdaItParameter:ManageTokensModel.kt$ManageTokensModel${ Timber.e(it, "Failed to save changes") return@resource }</ID>
|
||||
<ID>MultilineLambdaItParameter:ManageTokensUseCasesFacade.kt$ManageTokensUseCasesFacade${ it is CryptoCurrency.Token && it.network.backendId == network.backendId && it.network.derivationPath == network.derivationPath }</ID>
|
||||
<ID>MultilineLambdaItParameter:OnboardingManageTokensModel.kt$OnboardingManageTokensModel${ Timber.e(it, "Failed to save changes") return@resource }</ID>
|
||||
<ID>MultilineLambdaItParameter:PreviewManageTokensComponent.kt$PreviewManageTokensComponent${ it.fastForEachIndexed { index, network -> if (index == networkIndex) { it[index] = network.copy( iconResId = if (isSelected) { R.drawable.img_eth_22 } else { R.drawable.ic_eth_16 }, isSelected = isSelected, ) } } }</ID>
|
||||
<ID>NamedArguments:ChangedCurrenciesManager.kt$ChangedCurrenciesManager$updateChangedItems(currency, network, currenciesToAdd, currenciesToRemove)</ID>
|
||||
<ID>NamedArguments:ChangedCurrenciesManager.kt$ChangedCurrenciesManager$updateChangedItems(currency, network, currenciesToRemove, currenciesToAdd)</ID>
|
||||
<ID>NamedArguments:ChooseManagedTokensModel.kt$ChooseManagedTokensModel$TokenSearched( params.analyticsCategoryName, token = null, blockchain = null, isTokenChosen = false, )</ID>
|
||||
<ID>NamedArguments:ManageTokensListManager.kt$ManageTokensListManager$selectNetwork(currencyBatch.key, currency, networkId, isSelected)</ID>
|
||||
<ID>NamedArguments:ManageTokensListManager.kt$ManageTokensListManager$sendSelectCurrencyAction(batchKey, currency.id, network, isSelected = false)</ID>
|
||||
<ID>NamedArguments:ManageTokensListManager.kt$ManageTokensListManager$sendSelectCurrencyAction(batchKey, currency.id, network, isSelected = true)</ID>
|
||||
<ID>NestedScopeFunctions:CustomTokenSelectorModel.kt$CustomTokenSelectorModel$let { recognizer.recognize(it) }</ID>
|
||||
<ID>NullableBooleanCheck:CustomCurrencyFormOperations.kt$this.tokenForm?.wasFilled ?: false</ID>
|
||||
<ID>PropertyUsedBeforeDeclaration:ChooseManagedTokensModel.kt$ChooseManagedTokensModel$bottomSheetNavigation</ID>
|
||||
<ID>PropertyUsedBeforeDeclaration:ChooseManagedTokensModel.kt$ChooseManagedTokensModel$uiState</ID>
|
||||
<ID>UnsafeCallOnNullableType:CustomTokenFormContent.kt$PreviewCustomTokenFormComponentProvider$it[Field.CONTRACT_ADDRESS]!!</ID>
|
||||
<ID>UnsafeCallOnNullableType:PreviewAddCustomTokenComponent.kt$PreviewAddCustomTokenComponent$config.selectedDerivationPath!!</ID>
|
||||
<ID>UnsafeCallOnNullableType:PreviewAddCustomTokenComponent.kt$PreviewAddCustomTokenComponent$config.selectedNetwork!!</ID>
|
||||
<ID>UseEmptyCounterpart:CustomTokenAnalyticsEvent.kt$CustomTokenAnalyticsEvent$mapOf()</ID>
|
||||
<ID>UseEmptyCounterpart:ManageTokensAnalyticEvent.kt$ManageTokensAnalyticEvent$mapOf()</ID>
|
||||
<ID>UseOrEmpty:ChangedCurrenciesManager.kt$ChangedCurrenciesManager$items[currency] ?: emptySet()</ID>
|
||||
<ID>UseOrEmpty:PreviewCustomTokenSelectorComponent.kt$PreviewCustomTokenSelectorComponent$d.id?.rawId?.value ?: ""</ID>
|
||||
<ID>VarCouldBeVal:CustomTokenFormModel.kt$CustomTokenFormModel$private var useCasesFacade: CustomTokenFormUseCasesFacade = customTokenFormUseCasesFacadeFactory.create(params.mode)</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
|
|||
|
|
@ -17,8 +17,6 @@ import com.tangem.core.ui.event.triggeredEvent
|
|||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.notifications.SetShouldShowNotificationUseCase
|
||||
import com.tangem.features.managetokens.choosetoken.entity.ChooseManageTokensBottomSheetConfig
|
||||
import com.tangem.features.managetokens.choosetoken.entity.ChooseManagedTokenUM
|
||||
|
|
@ -45,7 +43,6 @@ import kotlinx.coroutines.flow.*
|
|||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.isNotEmpty
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ModelScoped
|
||||
|
|
@ -55,7 +52,6 @@ internal class ChooseManagedTokensModel @Inject constructor(
|
|||
private val uiMessageSender: UiMessageSender,
|
||||
private val setShouldShowNotificationUseCase: SetShouldShowNotificationUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
accountsFeatureToggles: AccountsFeatureToggles,
|
||||
paramsContainer: ParamsContainer,
|
||||
manageTokensUseCasesFacadeFactory: ManageTokensUseCasesFacade.Factory,
|
||||
manageTokensListManagerFactory: ManageTokensListManager.Factory,
|
||||
|
|
@ -63,16 +59,15 @@ internal class ChooseManagedTokensModel @Inject constructor(
|
|||
|
||||
private val params: ChooseManagedTokensComponent.Params = paramsContainer.require()
|
||||
|
||||
private val manageTokensMode = if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
val accountId = AccountId.forMainCryptoPortfolio(userWalletId = params.userWalletId)
|
||||
ManageTokensMode.Account(accountId = accountId)
|
||||
} else {
|
||||
ManageTokensMode.Wallet(params.userWalletId)
|
||||
}
|
||||
private val manageTokensMode = ManageTokensMode.Account(params.userWalletId)
|
||||
|
||||
private val useCasesFacade: ManageTokensUseCasesFacade = manageTokensUseCasesFacadeFactory
|
||||
.create(mode = manageTokensMode)
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<ChooseManageTokensBottomSheetConfig> = SlotNavigation()
|
||||
val uiState: StateFlow<ChooseManagedTokenUM>
|
||||
field = MutableStateFlow<ChooseManagedTokenUM>(createReadContentModel())
|
||||
|
||||
private val manageTokensListManager = manageTokensListManagerFactory.create(
|
||||
scope = modelScope,
|
||||
source = ManageTokensSource.SEND_VIA_SWAP,
|
||||
|
|
@ -91,10 +86,6 @@ internal class ChooseManagedTokensModel @Inject constructor(
|
|||
},
|
||||
)
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<ChooseManageTokensBottomSheetConfig> = SlotNavigation()
|
||||
val uiState: StateFlow<ChooseManagedTokenUM>
|
||||
field = MutableStateFlow<ChooseManagedTokenUM>(createReadContentModel())
|
||||
|
||||
init {
|
||||
manageTokensListManager.uiItems
|
||||
.onEach { items -> updateItems(items) }
|
||||
|
|
@ -146,11 +137,7 @@ internal class ChooseManagedTokensModel @Inject constructor(
|
|||
private fun removeNotification() {
|
||||
modelScope.launch {
|
||||
setShouldShowNotificationUseCase(NotificationId.SendViaSwapTokenSelectorNotification.key, false)
|
||||
uiState.update {
|
||||
it.copy(
|
||||
notificationUM = null,
|
||||
)
|
||||
}
|
||||
uiState.update { it.copy(notificationUM = null) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -161,7 +148,7 @@ internal class ChooseManagedTokensModel @Inject constructor(
|
|||
if (!new.readContent.search.isActive && old.readContent.search.isActive) {
|
||||
analyticsEventHandler.send(
|
||||
CommonManageTokensAnalyticEvents.TokenSearched(
|
||||
params.analyticsCategoryName,
|
||||
categoryName = params.analyticsCategoryName,
|
||||
token = null,
|
||||
blockchain = null,
|
||||
isTokenChosen = false,
|
||||
|
|
@ -195,8 +182,9 @@ internal class ChooseManagedTokensModel @Inject constructor(
|
|||
val isToken = currency.id.value == params.initialCurrency.id.rawCurrencyId?.value
|
||||
|
||||
// Ensure that initial token network is filtered out and network list is empty
|
||||
val paramsRawId = params.initialCurrency.network.id.rawId
|
||||
val isEmptyNetworks = availableNetworks?.networks?.filterNot { network ->
|
||||
network.id == params.initialCurrency.network.id.rawId.value
|
||||
network.id == paramsRawId.value
|
||||
}.isNullOrEmpty()
|
||||
|
||||
// Filter out currency from display
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.AccountName
|
||||
import com.tangem.features.managetokens.component.AddCustomTokenComponent
|
||||
import com.tangem.features.managetokens.component.AddCustomTokenMode
|
||||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||
import com.tangem.features.managetokens.entity.managetokens.ManageTokensBottomSheetConfig
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.features.managetokens.model.ManageTokensModel
|
||||
import com.tangem.features.managetokens.ui.ManageTokensScreen
|
||||
|
|
@ -38,7 +38,7 @@ internal class DefaultManageTokensComponent @AssistedInject constructor(
|
|||
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = model.bottomSheetNavigation,
|
||||
serializer = ManageTokensBottomSheetConfig.serializer(),
|
||||
serializer = AccountId.serializer(),
|
||||
handleBackButton = false,
|
||||
childFactory = ::bottomSheetChild,
|
||||
)
|
||||
|
|
@ -57,13 +57,10 @@ internal class DefaultManageTokensComponent @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun bottomSheetChild(
|
||||
config: ManageTokensBottomSheetConfig,
|
||||
accountId: AccountId,
|
||||
componentContext: ComponentContext,
|
||||
): ComposableBottomSheetComponent {
|
||||
val mode = when (config) {
|
||||
is ManageTokensBottomSheetConfig.AddWalletCustomToken -> AddCustomTokenMode.Wallet(config.userWalletId)
|
||||
is ManageTokensBottomSheetConfig.AddAccountCustomToken -> AddCustomTokenMode.Account(config.accountId)
|
||||
}
|
||||
val mode = AddCustomTokenMode(accountId)
|
||||
return addCustomTokenComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = AddCustomTokenComponent.Params(
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
|
||||
internal class PreviewAddCustomTokenComponent(
|
||||
initialState: AddCustomTokenConfig = AddCustomTokenConfig(
|
||||
mode = AddCustomTokenMode.Wallet(UserWalletId(stringValue = "321")),
|
||||
mode = AddCustomTokenMode(UserWalletId(stringValue = "321")),
|
||||
step = AddCustomTokenConfig.Step.INITIAL_NETWORK_SELECTOR,
|
||||
),
|
||||
) : AddCustomTokenComponent {
|
||||
|
|
@ -61,8 +61,8 @@ internal class PreviewAddCustomTokenComponent(
|
|||
PreviewCustomTokenSelectorComponent(
|
||||
params = CustomTokenSelectorComponent.Params.DerivationPathSelector(
|
||||
mode = config.mode,
|
||||
selectedNetwork = config.selectedNetwork!!,
|
||||
selectedDerivationPath = config.selectedDerivationPath!!,
|
||||
selectedNetwork = requireNotNull(config.selectedNetwork),
|
||||
selectedDerivationPath = requireNotNull(config.selectedDerivationPath),
|
||||
onDerivationPathSelected = { _, _ -> },
|
||||
),
|
||||
).Content(modifier)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import kotlinx.collections.immutable.toImmutableList
|
|||
|
||||
internal class PreviewCustomTokenSelectorComponent(
|
||||
private val params: Params = Params.NetworkSelector(
|
||||
mode = AddCustomTokenMode.Wallet(UserWalletId(stringValue = "321")),
|
||||
mode = AddCustomTokenMode(UserWalletId(stringValue = "321")),
|
||||
selectedNetwork = null,
|
||||
onNetworkSelected = {},
|
||||
),
|
||||
|
|
@ -39,7 +39,7 @@ internal class PreviewCustomTokenSelectorComponent(
|
|||
)
|
||||
|
||||
DerivationPathUM(
|
||||
id = d.id?.rawId?.value ?: "",
|
||||
id = d.id?.rawId?.value.orEmpty(),
|
||||
value = d.value.value.orEmpty(),
|
||||
networkName = stringReference(d.name),
|
||||
isSelected = d.value == params.selectedDerivationPath?.value,
|
||||
|
|
|
|||
|
|
@ -40,9 +40,7 @@ internal class PreviewManageTokensComponent(
|
|||
popBack = {},
|
||||
items = items,
|
||||
topBar = when (params.mode) {
|
||||
is ManageTokensMode.Account,
|
||||
is ManageTokensMode.Wallet,
|
||||
-> ManageTokensTopBarUM.ManageContent(
|
||||
is ManageTokensMode.Account -> ManageTokensTopBarUM.ManageContent(
|
||||
title = resourceReference(id = R.string.main_manage_tokens),
|
||||
onBackButtonClick = {},
|
||||
endButton = TopAppBarButtonUM.Icon(
|
||||
|
|
@ -206,10 +204,10 @@ internal class PreviewManageTokensComponent(
|
|||
is CurrencyItemUM.Basic -> {
|
||||
val updatedNetworks = (item.networks as? CurrencyItemUM.Basic.NetworksUM.Expanded)
|
||||
?.copy(
|
||||
networks = item.networks.networks.toPersistentList().mutate {
|
||||
it.fastForEachIndexed { index, network ->
|
||||
networks = item.networks.networks.toPersistentList().mutate { networks ->
|
||||
networks.fastForEachIndexed { index, network ->
|
||||
if (index == networkIndex) {
|
||||
it[index] = network.copy(
|
||||
networks[index] = network.copy(
|
||||
iconResId = if (isSelected) {
|
||||
R.drawable.img_eth_22
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
package com.tangem.features.managetokens.entity.managetokens
|
||||
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal sealed class ManageTokensBottomSheetConfig {
|
||||
|
||||
@Serializable
|
||||
data class AddWalletCustomToken(
|
||||
val userWalletId: UserWalletId,
|
||||
) : ManageTokensBottomSheetConfig()
|
||||
|
||||
@Serializable
|
||||
data class AddAccountCustomToken(
|
||||
val accountId: AccountId,
|
||||
) : ManageTokensBottomSheetConfig()
|
||||
}
|
||||
|
|
@ -181,7 +181,7 @@ internal class CustomTokenFormModel @Inject constructor(
|
|||
isAlreadyAdded: Boolean,
|
||||
isCustom: Boolean,
|
||||
) = modelScope.launch {
|
||||
val needColdWalletInteraction = coldWalletAndHasMissedDerivationsUseCase.invoke(
|
||||
val isNeedColdWalletInteraction = coldWalletAndHasMissedDerivationsUseCase.invoke(
|
||||
userWalletId = params.mode.userWalletId,
|
||||
networksWithDerivationPath = mapOf(currency.network.backendId to getDerivationPath().value),
|
||||
)
|
||||
|
|
@ -195,7 +195,7 @@ internal class CustomTokenFormModel @Inject constructor(
|
|||
clearNotifications = true,
|
||||
clearFieldErrors = true,
|
||||
disableSecondaryFields = !isCustom,
|
||||
walletInteractionIcon = R.drawable.ic_tangem_24.takeIf { needColdWalletInteraction },
|
||||
walletInteractionIcon = R.drawable.ic_tangem_24.takeIf { isNeedColdWalletInteraction },
|
||||
)
|
||||
|
||||
if (fillForm) {
|
||||
|
|
@ -355,11 +355,6 @@ internal class CustomTokenFormModel @Inject constructor(
|
|||
return@resource
|
||||
}
|
||||
|
||||
useCasesFacade.derivePublicKeysUseCase(listOf(currency)).getOrElse {
|
||||
showErrorDialog(IllegalStateException("Failed to derive public keys"))
|
||||
return@resource
|
||||
}
|
||||
|
||||
useCasesFacade.addCryptoCurrenciesUseCase(currency).getOrElse { throwable ->
|
||||
showErrorDialog(throwable)
|
||||
return@resource
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import com.tangem.core.ui.extensions.wrappedList
|
|||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
import com.tangem.domain.managetokens.GetSupportedNetworksUseCase
|
||||
import com.tangem.domain.models.account.Account
|
||||
|
|
@ -52,7 +51,6 @@ internal class CustomTokenSelectorModel @Inject constructor(
|
|||
private val getSupportedNetworksUseCase: GetSupportedNetworksUseCase,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -171,7 +169,7 @@ internal class CustomTokenSelectorModel @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun getSupportedNetworks(mode: AddCustomTokenMode): List<Network> {
|
||||
return getSupportedNetworksUseCase(mode.userWalletId).getOrElse { e ->
|
||||
return getSupportedNetworksUseCase(mode.userWalletId).getOrElse { _ ->
|
||||
val message = SnackbarMessage(message = resourceReference(R.string.common_unknown_error))
|
||||
messageSender.send(message)
|
||||
|
||||
|
|
@ -194,19 +192,23 @@ internal class CustomTokenSelectorModel @Inject constructor(
|
|||
fun selectCustomDerivationPath(value: SelectedDerivationPath) {
|
||||
when (params) {
|
||||
is NetworkSelector -> return
|
||||
is DerivationPathSelector -> if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
params.checkAccountDerivation(value)
|
||||
} else {
|
||||
params.onDerivationPathSelected(value, null)
|
||||
}
|
||||
is DerivationPathSelector -> params.checkAccountDerivation(value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DerivationPathSelector.checkAccountDerivation(derivationPath: SelectedDerivationPath) =
|
||||
modelScope.launch {
|
||||
val account = derivationPath.id
|
||||
?.let { Blockchain.fromId(it.rawId.value) }?.let(::AccountNodeRecognizer)
|
||||
?.let { recognizer -> derivationPath.value.value?.let { recognizer.recognize(it) } }
|
||||
?.let { Blockchain.fromId(it.rawId.value) }
|
||||
?.let(::AccountNodeRecognizer)
|
||||
?.let { recognizer ->
|
||||
val derivationPathValue = derivationPath.value.value
|
||||
if (derivationPathValue != null) {
|
||||
recognizer.recognize(derivationPathValue)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
?.let { accountNode ->
|
||||
fun AccountStatus.CryptoPortfolio.sameNodeAndNotMain() = !this.account.isMainAccount &&
|
||||
this.account.derivationIndex.value.toLong() == accountNode
|
||||
|
|
|
|||
|
|
@ -18,12 +18,12 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent
|
||||
import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent
|
||||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||
import com.tangem.features.managetokens.component.ManageTokensMode
|
||||
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
|
||||
import com.tangem.features.managetokens.entity.managetokens.ManageTokensBottomSheetConfig
|
||||
import com.tangem.features.managetokens.entity.managetokens.ManageTokensTopBarUM
|
||||
import com.tangem.features.managetokens.entity.managetokens.ManageTokensUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
|
|
@ -66,7 +66,7 @@ internal class ManageTokensModel @Inject constructor(
|
|||
)
|
||||
|
||||
val state: MutableStateFlow<ManageTokensUM> = MutableStateFlow(getInitialState())
|
||||
val bottomSheetNavigation: SlotNavigation<ManageTokensBottomSheetConfig> = SlotNavigation()
|
||||
val bottomSheetNavigation: SlotNavigation<AccountId> = SlotNavigation()
|
||||
|
||||
init {
|
||||
manageTokensListManager.uiItems
|
||||
|
|
@ -101,15 +101,12 @@ internal class ManageTokensModel @Inject constructor(
|
|||
analyticsEventHandler.send(ManageTokensAnalyticEvent.ScreenOpened(params.source))
|
||||
|
||||
return when (params.mode) {
|
||||
is ManageTokensMode.Wallet,
|
||||
is ManageTokensMode.Account,
|
||||
-> createManageContentModel()
|
||||
is ManageTokensMode.Account -> createManageContentModel()
|
||||
ManageTokensMode.None -> createReadContentModel()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTopBarInitialState(): ManageTokensTopBarUM = when (params.mode) {
|
||||
is ManageTokensMode.Wallet -> manageContentTopBar()
|
||||
is ManageTokensMode.Account -> ManageTokensTopBarUM.ReadContent(
|
||||
title = resourceReference(id = R.string.main_manage_tokens),
|
||||
onBackButtonClick = router::pop,
|
||||
|
|
@ -196,9 +193,7 @@ internal class ManageTokensModel @Inject constructor(
|
|||
state.update { it.copySealed(topBar = manageContentTopBar()) }
|
||||
}
|
||||
}
|
||||
ManageTokensMode.None,
|
||||
is ManageTokensMode.Wallet,
|
||||
-> Unit // use init state
|
||||
ManageTokensMode.None -> Unit // use init state
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -299,12 +294,12 @@ internal class ManageTokensModel @Inject constructor(
|
|||
.flatten()
|
||||
.toSet()
|
||||
.associate { network -> network.backendId to network.derivationPath.value }
|
||||
val needToInteractWithColdWallet = useCasesFacade.needColdWalletInteraction(networks)
|
||||
val isNeedToInteractWithColdWallet = useCasesFacade.needColdWalletInteraction(networks)
|
||||
|
||||
state.update { state ->
|
||||
state.copySealed(
|
||||
hasChanges = currenciesToAdd.isNotEmpty() || currenciesToRemove.isNotEmpty(),
|
||||
needToInteractWithColdWallet = needToInteractWithColdWallet,
|
||||
needToInteractWithColdWallet = isNeedToInteractWithColdWallet,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -324,12 +319,8 @@ internal class ManageTokensModel @Inject constructor(
|
|||
private fun navigateToAddCustomToken() {
|
||||
analyticsEventHandler.send(CustomTokenAnalyticsEvent.ButtonCustomToken(params.source))
|
||||
when (val portfolio = params.mode) {
|
||||
is ManageTokensMode.Wallet ->
|
||||
bottomSheetNavigation
|
||||
.activate(ManageTokensBottomSheetConfig.AddWalletCustomToken(portfolio.userWalletId))
|
||||
is ManageTokensMode.Account ->
|
||||
bottomSheetNavigation
|
||||
.activate(ManageTokensBottomSheetConfig.AddAccountCustomToken(portfolio.accountId))
|
||||
bottomSheetNavigation.activate(portfolio.accountId)
|
||||
ManageTokensMode.None -> Unit
|
||||
}
|
||||
}
|
||||
|
|
@ -347,8 +338,8 @@ internal class ManageTokensModel @Inject constructor(
|
|||
useCasesFacade.saveManagedTokensUseCase(
|
||||
currenciesToAdd = manageTokensListManager.currenciesToAdd.value,
|
||||
currenciesToRemove = manageTokensListManager.currenciesToRemove.value,
|
||||
).getOrElse {
|
||||
Timber.e(it, "Failed to save changes")
|
||||
).getOrElse { throwable ->
|
||||
Timber.e(throwable, "Failed to save changes")
|
||||
return@resource
|
||||
}
|
||||
|
||||
|
|
|
|||
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