Updated on 2026-08-14
This commit is contained in:
commit
af09ce9ca4
877 changed files with 16091 additions and 6613 deletions
|
|
@ -5,7 +5,6 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
|
|
@ -14,30 +13,28 @@ import com.arkivanov.decompose.router.slot.dismiss
|
|||
import com.arkivanov.essenty.lifecycle.subscribe
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.feature.swap.choosetoken.api.ChooseTokenComponent
|
||||
import com.tangem.feature.swap.component.SwapFeeSelectorBlockComponent
|
||||
import com.tangem.feature.swap.model.SwapModel
|
||||
import com.tangem.feature.swap.models.AddToPortfolioRoute
|
||||
import com.tangem.feature.swap.router.SwapNavScreen
|
||||
import com.tangem.feature.swap.ui.SwapScreen
|
||||
import com.tangem.feature.swap.ui.SwapSelectTokenScreen
|
||||
import com.tangem.feature.swap.ui.SwapSuccessScreen
|
||||
import com.tangem.features.approval.api.GiveApprovalComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent
|
||||
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.swap.SwapComponent
|
||||
import com.tangem.utils.extensions.isZero
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
|
|
@ -45,19 +42,19 @@ internal class DefaultSwapComponent @AssistedInject constructor(
|
|||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: SwapComponent.Params,
|
||||
private val swapFeeSelectorBlockComponentFactory: SwapFeeSelectorBlockComponent.Factory,
|
||||
private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory,
|
||||
private val giveApprovalComponentFactory: GiveApprovalComponent.Factory,
|
||||
private val chooseTokenComponentFactory: ChooseTokenComponent.Factory,
|
||||
) : SwapComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: SwapModel = getOrCreateModel(params)
|
||||
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = model.bottomSheetNavigation,
|
||||
serializer = AddToPortfolioRoute.serializer(),
|
||||
key = BOTTOM_SHEET_SLOT_KEY,
|
||||
handleBackButton = false,
|
||||
childFactory = { _, context -> bottomSheetChild(context) },
|
||||
)
|
||||
// todo swap create InnerRouter
|
||||
private val chooseTokenComponent by lazy {
|
||||
chooseTokenComponentFactory.create(
|
||||
context = child("chooseTokenComponent"),
|
||||
params = ChooseTokenComponent.Params(model.chooseTokenBridge),
|
||||
)
|
||||
}
|
||||
|
||||
private val approvalSlot = childSlot(
|
||||
key = APPROVAL_SLOT_KEY,
|
||||
|
|
@ -130,7 +127,7 @@ internal class DefaultSwapComponent @AssistedInject constructor(
|
|||
|
||||
LaunchedEffect(fromCryptoCurrency, feePaidCryptoCurrency, shouldHideBlock) {
|
||||
if (shouldHideBlock) {
|
||||
Timber.e(
|
||||
TangemLogger.e(
|
||||
"Dismissing fee selector: " +
|
||||
"shouldHideBlock = $shouldHideBlock, amount = ${dataState.amount}, " +
|
||||
"isInsufficientFunds = ${model.uiState.isInsufficientFunds}",
|
||||
|
|
@ -140,13 +137,13 @@ internal class DefaultSwapComponent @AssistedInject constructor(
|
|||
}
|
||||
|
||||
val sendingCryptoCurrencyStatus = fromCryptoCurrency ?: run {
|
||||
Timber.e("Dismissing fee selector: fromCryptoCurrency is null")
|
||||
TangemLogger.e("Dismissing fee selector: fromCryptoCurrency is null")
|
||||
slotNavigation.dismiss()
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
val feeCurrencyStatus = feePaidCryptoCurrency ?: run {
|
||||
Timber.e("Dismissing fee selector: feePaidCryptoCurrency is null")
|
||||
TangemLogger.e("Dismissing fee selector: feePaidCryptoCurrency is null")
|
||||
slotNavigation.dismiss()
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
|
@ -161,7 +158,6 @@ internal class DefaultSwapComponent @AssistedInject constructor(
|
|||
|
||||
val feeSelectorChildStackState by childSlot.subscribeAsState()
|
||||
val feeSelectorBlockComponent = feeSelectorChildStackState.child?.instance
|
||||
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||
|
||||
Crossfade(
|
||||
modifier = Modifier.background(TangemTheme.colors.background.secondary),
|
||||
|
|
@ -189,38 +185,14 @@ internal class DefaultSwapComponent @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
SwapNavScreen.SelectToken -> {
|
||||
val tokenState = model.uiState.selectTokenState
|
||||
if (tokenState != null) {
|
||||
SwapSelectTokenScreen(state = tokenState, onBack = model.uiState.onBackClicked)
|
||||
} else {
|
||||
SwapScreen(
|
||||
stateHolder = model.uiState,
|
||||
feeSelectorBlockComponent = feeSelectorBlockComponent,
|
||||
)
|
||||
}
|
||||
}
|
||||
SwapNavScreen.SelectToken -> chooseTokenComponent.Content(Modifier)
|
||||
}
|
||||
}
|
||||
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
|
||||
val approvalSlotState by approvalSlot.subscribeAsState()
|
||||
approvalSlotState.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
@Suppress("UnsafeCallOnNullableType")
|
||||
private fun bottomSheetChild(componentContext: ComponentContext): ComposableBottomSheetComponent {
|
||||
return addToPortfolioComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = AddToPortfolioComponent.Params(
|
||||
addToPortfolioManager = model.addToPortfolioManager!!,
|
||||
callback = model.addToPortfolioCallback,
|
||||
shouldSkipTokenActionsScreen = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getApprovalParams(): GiveApprovalComponent.Params? {
|
||||
val permissionState = model.uiState.permissionState as? GiveTxPermissionState.ReadyForRequest
|
||||
?: return null
|
||||
|
|
@ -232,12 +204,13 @@ internal class DefaultSwapComponent @AssistedInject constructor(
|
|||
userWalletId = params.userWalletId,
|
||||
cryptoCurrencyStatus = fromCryptoCurrency,
|
||||
feeCryptoCurrencyStatus = feeCryptoCurrency,
|
||||
amount = permissionState.amount,
|
||||
amount = model.dataState.amount.orEmpty(),
|
||||
spenderAddress = requireNotNull(model.dataState.approveDataModel).spenderAddress,
|
||||
subtitle = resourceReference(
|
||||
id = R.string.give_permission_swap_subtitle,
|
||||
formatArgs = wrappedList(providerName, permissionState.currency),
|
||||
),
|
||||
isHoldToConfirm = model.isHoldToConfirmEnabled,
|
||||
callback = model.approvalCallback,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
package com.tangem.feature.swap.choosetoken.api
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
// todo swap make universal, encapsulate, move to some common module
|
||||
internal interface ChooseTokenBridge {
|
||||
|
||||
// todo swap new api
|
||||
val onCurrencyChosen: Channel<ChooseTokenResult>
|
||||
val onClose: Channel<Unit>
|
||||
|
||||
// todo swap legacy api, remove
|
||||
val onTokenSelected: Channel<Pair<String, ChooseTokenAnalyticsPayload.IsSearched>>
|
||||
val onNewTokenAdded: Channel<Pair<CryptoCurrency, ChooseTokenAnalyticsPayload.IsSearched>>
|
||||
|
||||
val searchQueryState: StateFlow<String>
|
||||
val currenciesGroup: Flow<CurrenciesGroup>
|
||||
|
||||
fun onTokenSelected(tokenId: Pair<String, ChooseTokenAnalyticsPayload.IsSearched>) {
|
||||
onTokenSelected.trySend(tokenId)
|
||||
onSearchQuery("")
|
||||
}
|
||||
|
||||
fun onNewTokenAdded(addedToken: Pair<CryptoCurrency, ChooseTokenAnalyticsPayload.IsSearched>) {
|
||||
onNewTokenAdded.trySend(addedToken)
|
||||
onSearchQuery("")
|
||||
}
|
||||
|
||||
fun onSearchQuery(query: String)
|
||||
|
||||
fun updateCurrenciesGroup(currenciesGroup: CurrenciesGroup)
|
||||
|
||||
fun onCurrencyChosen(result: ChooseTokenResult) {
|
||||
onCurrencyChosen.trySend(result)
|
||||
}
|
||||
|
||||
fun onClose() {
|
||||
onClose.trySend(Unit)
|
||||
onSearchQuery("")
|
||||
}
|
||||
|
||||
interface Factory {
|
||||
fun create(modelScope: CoroutineScope): ChooseTokenBridge
|
||||
}
|
||||
}
|
||||
|
||||
data class ChooseTokenResult(
|
||||
val addedCurrency: AccountCryptoCurrencyStatus,
|
||||
val userWallet: UserWallet,
|
||||
val analyticsPayload: Set<ChooseTokenAnalyticsPayload> = emptySet(),
|
||||
) {
|
||||
val status: CryptoCurrencyStatus get() = addedCurrency.status
|
||||
val account: Account.CryptoPortfolio get() = addedCurrency.account
|
||||
val walletId get() = userWallet.walletId
|
||||
}
|
||||
|
||||
sealed interface ChooseTokenAnalyticsPayload {
|
||||
|
||||
@Suppress("BooleanPropertyNaming")
|
||||
@JvmInline
|
||||
value class IsSearched(val value: Boolean) : ChooseTokenAnalyticsPayload
|
||||
}
|
||||
|
||||
internal interface ChooseTokenComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val bridge: ChooseTokenBridge,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, ChooseTokenComponent>
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.feature.swap.choosetoken.api
|
||||
|
||||
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import javax.inject.Inject
|
||||
|
||||
// todo swap move to some common module
|
||||
class SettingContextUseCase @Inject constructor(
|
||||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
) {
|
||||
|
||||
suspend fun invokeSync(): SettingContext = invoke().first()
|
||||
|
||||
operator fun invoke(): Flow<SettingContext> = combine(
|
||||
flow = isAccountsModeEnabledUseCase.invoke(),
|
||||
flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(),
|
||||
flow3 = getBalanceHidingSettingsUseCase.isBalanceHidden(),
|
||||
transform = { isAccountsModeEnabled, selectedAppCurrency, balanceHidingSettings ->
|
||||
SettingContext(
|
||||
isAccountsMode = isAccountsModeEnabled,
|
||||
appCurrency = selectedAppCurrency,
|
||||
isBalanceHidden = balanceHidingSettings,
|
||||
)
|
||||
},
|
||||
).distinctUntilChanged()
|
||||
}
|
||||
|
||||
data class SettingContext(
|
||||
val isAccountsMode: Boolean,
|
||||
val appCurrency: AppCurrency,
|
||||
val isBalanceHidden: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.feature.swap.choosetoken.impl
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.feature.swap.choosetoken.api.ChooseTokenAnalyticsPayload
|
||||
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridge
|
||||
import com.tangem.feature.swap.choosetoken.api.ChooseTokenResult
|
||||
import com.tangem.feature.swap.choosetoken.impl.model.ChooseTokenModel.Companion.DEBOUNCE_SEARCH_DELAY
|
||||
import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
internal class DefaultChooseTokenBridge @AssistedInject constructor(
|
||||
@Assisted private val modelScope: CoroutineScope,
|
||||
) : ChooseTokenBridge {
|
||||
|
||||
override val onCurrencyChosen: Channel<ChooseTokenResult> = Channel()
|
||||
|
||||
override val onTokenSelected: Channel<Pair<String, ChooseTokenAnalyticsPayload.IsSearched>> = Channel()
|
||||
override val onNewTokenAdded: Channel<Pair<CryptoCurrency, ChooseTokenAnalyticsPayload.IsSearched>> = Channel()
|
||||
override val onClose: Channel<Unit> = Channel()
|
||||
|
||||
private val onSearchQuery: Channel<String> = Channel()
|
||||
override val searchQueryState: StateFlow<String> = onSearchQuery.receiveAsFlow()
|
||||
.debounce(DEBOUNCE_SEARCH_DELAY)
|
||||
.stateIn(modelScope, SharingStarted.Eagerly, initialValue = "")
|
||||
|
||||
private val _currenciesGroupFlow = MutableStateFlow<CurrenciesGroup?>(null)
|
||||
override val currenciesGroup: Flow<CurrenciesGroup> = _currenciesGroupFlow.filterNotNull()
|
||||
|
||||
override fun onSearchQuery(query: String) {
|
||||
onSearchQuery.trySend(query)
|
||||
}
|
||||
|
||||
override fun updateCurrenciesGroup(currenciesGroup: CurrenciesGroup) {
|
||||
_currenciesGroupFlow.update { currenciesGroup }
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : ChooseTokenBridge.Factory {
|
||||
override fun create(modelScope: CoroutineScope): DefaultChooseTokenBridge
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package com.tangem.feature.swap.choosetoken.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.slot.childSlot
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.feature.swap.choosetoken.api.ChooseTokenComponent
|
||||
import com.tangem.feature.swap.choosetoken.impl.model.ChooseTokenModel
|
||||
import com.tangem.feature.swap.models.AddToPortfolioRoute
|
||||
import com.tangem.feature.swap.ui.SwapSelectTokenScreen
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultChooseTokenComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: ChooseTokenComponent.Params,
|
||||
private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory,
|
||||
) : AppComponentContext by appComponentContext, ChooseTokenComponent {
|
||||
|
||||
private val model: ChooseTokenModel = getOrCreateModel(params)
|
||||
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = model.bottomSheetNavigation,
|
||||
serializer = AddToPortfolioRoute.serializer(),
|
||||
key = BOTTOM_SHEET_SLOT_KEY,
|
||||
handleBackButton = false,
|
||||
childFactory = { _, context -> bottomSheetChild(context) },
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||
state?.let { stateHolder ->
|
||||
SwapSelectTokenScreen(state = stateHolder, onBack = { model.onBackClicked() })
|
||||
}
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
@Suppress("UnsafeCallOnNullableType")
|
||||
private fun bottomSheetChild(componentContext: ComponentContext): ComposableBottomSheetComponent {
|
||||
return addToPortfolioComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = AddToPortfolioComponent.Params(
|
||||
addToPortfolioManager = model.addToPortfolioManager!!,
|
||||
callback = model.addToPortfolioCallback,
|
||||
shouldSkipTokenActionsScreen = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : ChooseTokenComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: ChooseTokenComponent.Params,
|
||||
): DefaultChooseTokenComponent
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val BOTTOM_SHEET_SLOT_KEY = "choosePortfolioTokenBottomSheetSlot"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.feature.swap.choosetoken.impl.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridge
|
||||
import com.tangem.feature.swap.choosetoken.api.ChooseTokenComponent
|
||||
import com.tangem.feature.swap.choosetoken.impl.DefaultChooseTokenBridge
|
||||
import com.tangem.feature.swap.choosetoken.impl.DefaultChooseTokenComponent
|
||||
import com.tangem.feature.swap.choosetoken.impl.model.ChooseTokenModel
|
||||
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
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface ChooseTokenModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(ChooseTokenModel::class)
|
||||
fun provideChooseTokenModel(model: ChooseTokenModel): Model
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface ChooseTokenFeatureModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun provideChooseTokenComponentFactory(impl: DefaultChooseTokenComponent.Factory): ChooseTokenComponent.Factory
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun provideDefaultChooseTokenBridgeFactory(impl: DefaultChooseTokenBridge.Factory): ChooseTokenBridge.Factory
|
||||
}
|
||||
|
|
@ -0,0 +1,302 @@
|
|||
package com.tangem.feature.swap.choosetoken.impl.model
|
||||
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains
|
||||
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenMarketListConfig
|
||||
import com.tangem.domain.markets.toSerializableParam
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.feature.swap.choosetoken.api.ChooseTokenAnalyticsPayload
|
||||
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridge
|
||||
import com.tangem.feature.swap.choosetoken.api.ChooseTokenComponent
|
||||
import com.tangem.feature.swap.choosetoken.api.SettingContextUseCase
|
||||
import com.tangem.feature.swap.converters.TokensDataConverter
|
||||
import com.tangem.feature.swap.models.AddToPortfolioRoute
|
||||
import com.tangem.feature.swap.models.SwapSelectTokenStateHolder
|
||||
import com.tangem.feature.swap.models.market.MarketsListBatchFlowManager
|
||||
import com.tangem.feature.swap.models.market.state.SwapMarketState
|
||||
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.lib.crypto.BlockchainUtils
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ModelScoped
|
||||
internal class ChooseTokenModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory,
|
||||
private val excludedBlockchains: ExcludedBlockchains,
|
||||
private val getUserWalletsUseCase: GetWalletsUseCase,
|
||||
private val settingContextUseCase: SettingContextUseCase,
|
||||
private val marketsListBatchFlowManagerFactory: MarketsListBatchFlowManager.Factory,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<ChooseTokenComponent.Params>()
|
||||
private val bridge: ChooseTokenBridge = params.bridge
|
||||
private val searchQueryState = bridge.searchQueryState
|
||||
|
||||
private val addToPortfolioJobHolder = JobHolder()
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<AddToPortfolioRoute> = SlotNavigation()
|
||||
|
||||
private val visibleMarketItemIds = MutableStateFlow<List<CryptoCurrency.RawID>>(emptyList())
|
||||
private val visibleDefaultMarketItemIds = MutableStateFlow<List<CryptoCurrency.RawID>>(emptyList())
|
||||
|
||||
private val expandedAccountsFlow: MutableStateFlow<Map<AccountId, Boolean>> = MutableStateFlow(emptyMap())
|
||||
|
||||
var addToPortfolioManager: AddToPortfolioManager? = null
|
||||
val addToPortfolioCallback = object : AddToPortfolioComponent.Callback {
|
||||
override fun onDismiss() = bottomSheetNavigation.dismiss()
|
||||
|
||||
override fun onSuccess(addedToken: CryptoCurrency) {
|
||||
modelScope.launch {
|
||||
val newToken = addedToken to ChooseTokenAnalyticsPayload
|
||||
.IsSearched(searchQueryState.value.isNotEmpty())
|
||||
bridge.onNewTokenAdded(newToken)
|
||||
bottomSheetNavigation.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val defaultMarketsListManager by lazy {
|
||||
marketsListBatchFlowManagerFactory.create(
|
||||
batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main,
|
||||
order = TokenMarketListConfig.Order.Trending,
|
||||
currentSearchText = Provider { null },
|
||||
modelScope = modelScope,
|
||||
)
|
||||
}
|
||||
|
||||
private val searchMarketsListManager by lazy {
|
||||
marketsListBatchFlowManagerFactory.create(
|
||||
batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search,
|
||||
order = TokenMarketListConfig.Order.ByRating,
|
||||
currentSearchText = Provider { searchQueryState.value },
|
||||
modelScope = modelScope,
|
||||
)
|
||||
}
|
||||
|
||||
val state: StateFlow<SwapSelectTokenStateHolder?> = combineUI()
|
||||
|
||||
init {
|
||||
subscribeMarketTokens()
|
||||
}
|
||||
|
||||
private fun combineUI(): StateFlow<SwapSelectTokenStateHolder?> = combine(
|
||||
flow = bridge.currenciesGroup,
|
||||
flow2 = settingContextUseCase.invoke(),
|
||||
flow3 = marketsStateFlow(),
|
||||
flow4 = expandedAccountsFlow,
|
||||
transform = { currenciesGroup, settingContext, marketState, expandedAccounts ->
|
||||
val isAccountsMode = settingContext.isAccountsMode
|
||||
val appCurrency = settingContext.appCurrency
|
||||
val isBalanceHidden = settingContext.isBalanceHidden
|
||||
|
||||
TokensDataConverter(
|
||||
onSearchEntered = { query -> bridge.onSearchQuery(query) },
|
||||
onTokenClick = { tokenId ->
|
||||
val selected = tokenId to ChooseTokenAnalyticsPayload
|
||||
.IsSearched(searchQueryState.value.isNotEmpty())
|
||||
bridge.onTokenSelected(selected)
|
||||
},
|
||||
onAccountClick = { account ->
|
||||
expandedAccountsFlow.update { expandedList ->
|
||||
val hasSavedAccount = expandedList[account.accountId]
|
||||
val isExpanded = hasSavedAccount == true
|
||||
expandedList + (account.accountId to !isExpanded)
|
||||
}
|
||||
},
|
||||
expandedAccounts = expandedAccounts,
|
||||
tokensDataState = currenciesGroup,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
isAccountsMode = isAccountsMode,
|
||||
appCurrency = appCurrency,
|
||||
marketState = marketState,
|
||||
).transform()
|
||||
},
|
||||
)
|
||||
.flowOn(dispatchers.default)
|
||||
.stateIn(modelScope, SharingStarted.Eagerly, initialValue = null)
|
||||
|
||||
private fun marketsStateFlow() = searchQueryState
|
||||
// Switch between default and search market flows
|
||||
.map { it.isEmpty() }
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { isDefaultMode ->
|
||||
if (isDefaultMode) {
|
||||
visibleMarketItemIds.value = emptyList()
|
||||
createDefaultMarketsFlow()
|
||||
} else {
|
||||
visibleDefaultMarketItemIds.value = emptyList()
|
||||
createSearchMarketsFlow()
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeMarketTokens() {
|
||||
// Reload search markets when query changes
|
||||
searchQueryState
|
||||
.onEach { searchQuery ->
|
||||
if (searchQuery.isNotEmpty()) {
|
||||
searchMarketsListManager.reload(searchQuery)
|
||||
}
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
|
||||
// Initial load of default markets
|
||||
defaultMarketsListManager.reload()
|
||||
|
||||
visibleMarketItemIds
|
||||
.mapNotNull { rawIDS ->
|
||||
if (rawIDS.isNotEmpty()) {
|
||||
searchMarketsListManager.getBatchKeysByItemIds(rawIDS)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.transformLatest<Set<Int>, Unit> { visibleBatchKeys ->
|
||||
searchMarketsListManager.loadCharts(visibleBatchKeys)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
|
||||
visibleDefaultMarketItemIds
|
||||
.mapNotNull { rawIds ->
|
||||
if (rawIds.isNotEmpty()) {
|
||||
defaultMarketsListManager.getBatchKeysByItemIds(rawIds)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
.transformLatest<Set<Int>, Unit> { visibleBatchKeys ->
|
||||
defaultMarketsListManager.loadCharts(visibleBatchKeys)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun createDefaultMarketsFlow(): Flow<SwapMarketState> {
|
||||
val marketsTitle = TextReference.Res(R.string.feed_trending_now)
|
||||
return combine(
|
||||
defaultMarketsListManager.uiItems,
|
||||
defaultMarketsListManager.isInInitialLoadingErrorState,
|
||||
defaultMarketsListManager.totalCount,
|
||||
) { uiItems, isError, total ->
|
||||
when {
|
||||
isError -> SwapMarketState.LoadingError(
|
||||
onRetryClicked = { defaultMarketsListManager.reload() },
|
||||
marketsTitle = marketsTitle,
|
||||
shouldAssetsCount = false,
|
||||
)
|
||||
uiItems.isEmpty() -> SwapMarketState.DefaultLoading
|
||||
else -> SwapMarketState.Content(
|
||||
items = uiItems,
|
||||
loadMore = { defaultMarketsListManager.loadMore() },
|
||||
onItemClick = { item -> addToPortfolioItem(item) },
|
||||
visibleIdsChanged = { visibleDefaultMarketItemIds.value = it },
|
||||
total = total ?: uiItems.size,
|
||||
marketsTitle = marketsTitle,
|
||||
shouldAssetsCount = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSearchMarketsFlow(): Flow<SwapMarketState> {
|
||||
val marketsTitle = TextReference.Res(R.string.markets_common_title)
|
||||
return combine(
|
||||
flow = searchMarketsListManager.uiItems,
|
||||
flow2 = searchMarketsListManager.isInInitialLoadingErrorState,
|
||||
flow3 = searchMarketsListManager.isSearchNotFoundState,
|
||||
flow4 = searchMarketsListManager.totalCount,
|
||||
) { uiItems, isError, isSearchNotFound, total ->
|
||||
when {
|
||||
isError -> SwapMarketState.LoadingError(
|
||||
onRetryClicked = { searchMarketsListManager.reload(searchQueryState.value) },
|
||||
marketsTitle = marketsTitle,
|
||||
shouldAssetsCount = true,
|
||||
)
|
||||
isSearchNotFound -> SwapMarketState.SearchNothingFound
|
||||
uiItems.isEmpty() -> SwapMarketState.SearchLoading
|
||||
else -> SwapMarketState.Content(
|
||||
items = uiItems,
|
||||
loadMore = { searchMarketsListManager.loadMore() },
|
||||
onItemClick = { item -> addToPortfolioItem(item) },
|
||||
visibleIdsChanged = { visibleMarketItemIds.value = it },
|
||||
total = total ?: uiItems.size,
|
||||
marketsTitle = marketsTitle,
|
||||
shouldAssetsCount = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addToPortfolioItem(item: MarketsListItemUM) {
|
||||
modelScope.launch {
|
||||
val tokenMarket = defaultMarketsListManager.getTokenMarketById(item.id)
|
||||
?: searchMarketsListManager.getTokenMarketById(item.id)
|
||||
?: return@launch
|
||||
|
||||
val param = tokenMarket.toSerializableParam()
|
||||
val hasOnlyHotWallets = getUserWalletsUseCase.invokeSync().all { it is UserWallet.Hot }
|
||||
|
||||
val networks = tokenMarket.networks?.filter { network ->
|
||||
BlockchainUtils.isSupportedNetworkId(
|
||||
blockchainId = network.networkId,
|
||||
coinId = tokenMarket.id.value,
|
||||
contractAddress = network.contractAddress,
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
hotExcludedBlockchains = hotWalletExcludedBlockchains,
|
||||
hasOnlyHotWallets = hasOnlyHotWallets,
|
||||
)
|
||||
}?.map { network ->
|
||||
TokenMarketInfo.Network(
|
||||
networkId = network.networkId,
|
||||
isExchangeable = false,
|
||||
contractAddress = network.contractAddress,
|
||||
decimalCount = network.decimalCount,
|
||||
)
|
||||
}.orEmpty()
|
||||
|
||||
addToPortfolioManager = addToPortfolioManagerFactory
|
||||
.create(
|
||||
scope = modelScope,
|
||||
token = param,
|
||||
analyticsParams = AddToPortfolioManager.AnalyticsParams(source = ScreensSources.Swap.value),
|
||||
).apply {
|
||||
setTokenNetworks(networks)
|
||||
}
|
||||
|
||||
addToPortfolioManager?.state
|
||||
?.firstOrNull { it is AddToPortfolioManager.State.AvailableToAdd }
|
||||
?.run { bottomSheetNavigation.activate(AddToPortfolioRoute) }
|
||||
}.saveIn(addToPortfolioJobHolder)
|
||||
}
|
||||
|
||||
fun onBackClicked() {
|
||||
bridge.onClose()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DEBOUNCE_SEARCH_DELAY = 500L
|
||||
}
|
||||
}
|
||||
|
|
@ -15,27 +15,40 @@ import com.tangem.core.ui.format.bigdecimal.crypto
|
|||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.feature.swap.domain.models.ui.AccountSwapAvailability
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class AccountTokenItemConverter(
|
||||
private val appCurrency: AppCurrency,
|
||||
private val unavailableErrorText: TextReference,
|
||||
private val onItemClick: (String) -> Unit,
|
||||
private val expandedAccounts: Map<AccountId, Boolean>,
|
||||
private val onTokenItemClick: (String) -> Unit,
|
||||
private val onAccountItemClick: (Account.CryptoPortfolio) -> Unit,
|
||||
) : Converter<AccountSwapAvailability, TokensListItemUM.Portfolio> {
|
||||
|
||||
override fun convert(value: AccountSwapAvailability): TokensListItemUM.Portfolio {
|
||||
return TokensListPortfolioItemConverter(
|
||||
tokenItemUM = AccountCryptoPortfolioItemStateConverter(
|
||||
appCurrency = appCurrency,
|
||||
account = value.account,
|
||||
onItemClick = null,
|
||||
).convert(TotalFiatBalance.Failed),
|
||||
isExpanded = true,
|
||||
isCollapsable = false,
|
||||
account = value.account.copy(
|
||||
cryptoCurrencies = value.currencyList.map { it.cryptoCurrencyStatus.currency },
|
||||
),
|
||||
onItemClick = onAccountItemClick,
|
||||
).convert(
|
||||
TotalFiatBalance.Loaded(
|
||||
amount = value.currencyList.sumOf { it.cryptoCurrencyStatus.value.fiatAmount.orZero() },
|
||||
source = StatusSource.ONLY_CACHE,
|
||||
),
|
||||
),
|
||||
isExpanded = expandedAccounts[value.account.accountId] != false,
|
||||
isCollapsable = true,
|
||||
tokens = value.currencyList.map { accountSwapCurrency ->
|
||||
createAvailableItemConverter()
|
||||
.convert(accountSwapCurrency.cryptoCurrencyStatus)
|
||||
|
|
@ -57,7 +70,7 @@ internal class AccountTokenItemConverter(
|
|||
fiatAmountStateProvider = {
|
||||
createFiatAmountStateProvider(status = it, appCurrency = appCurrency, isAvailable = true)
|
||||
},
|
||||
onItemClick = { account, currencyStatus -> onItemClick(currencyStatus.currency.id.value) },
|
||||
onItemClick = { account, currencyStatus -> onTokenItemClick(currencyStatus.currency.id.value) },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,78 +3,78 @@ package com.tangem.feature.swap.converters
|
|||
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup
|
||||
import com.tangem.feature.swap.models.SwapSelectTokenStateHolder
|
||||
import com.tangem.feature.swap.models.SwapStateHolder
|
||||
import com.tangem.feature.swap.models.TokenListUMData
|
||||
import com.tangem.feature.swap.models.market.state.SwapMarketState
|
||||
import com.tangem.feature.swap.presentation.R
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.plus
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class TokensDataConverter(
|
||||
private val onSearchEntered: (String) -> Unit,
|
||||
private val onTokenSelected: (String) -> Unit,
|
||||
onTokenClick: (String) -> Unit,
|
||||
onAccountClick: (Account.CryptoPortfolio) -> Unit,
|
||||
private val expandedAccounts: Map<AccountId, Boolean>,
|
||||
private val tokensDataState: CurrenciesGroup,
|
||||
private val isBalanceHidden: Boolean,
|
||||
private val isAccountsMode: Boolean,
|
||||
appCurrencyProvider: Provider<AppCurrency>,
|
||||
) : Transformer<SwapStateHolder> {
|
||||
private val appCurrency: AppCurrency,
|
||||
private val marketState: SwapMarketState,
|
||||
) {
|
||||
|
||||
private val accountListItemConverter = AccountTokenItemConverter(
|
||||
appCurrency = appCurrencyProvider(),
|
||||
appCurrency = appCurrency,
|
||||
unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header),
|
||||
onItemClick = onTokenSelected,
|
||||
onTokenItemClick = onTokenClick,
|
||||
onAccountItemClick = onAccountClick,
|
||||
expandedAccounts = expandedAccounts,
|
||||
)
|
||||
|
||||
override fun transform(prevState: SwapStateHolder): SwapStateHolder {
|
||||
fun transform(): SwapSelectTokenStateHolder {
|
||||
val accountList = tokensDataState.accountCurrencyList
|
||||
val currentMarketsState = prevState.selectTokenState?.marketsState
|
||||
return prevState.copy(
|
||||
selectTokenState = SwapSelectTokenStateHolder(
|
||||
availableTokens = persistentListOf(),
|
||||
unavailableTokens = persistentListOf(),
|
||||
tokensListData = if (isAccountsMode) {
|
||||
val portfolioList = accountListItemConverter.convertList(accountList).toPersistentList()
|
||||
val totalTokensCount = portfolioList.sumOf { it.tokens.size }
|
||||
if (totalTokensCount > 0) {
|
||||
TokenListUMData.AccountList(
|
||||
tokensList = portfolioList,
|
||||
totalTokensCount = totalTokensCount,
|
||||
)
|
||||
} else {
|
||||
TokenListUMData.EmptyList
|
||||
}
|
||||
return SwapSelectTokenStateHolder(
|
||||
tokensListData = if (isAccountsMode) {
|
||||
val portfolioList = accountListItemConverter.convertList(accountList).toPersistentList()
|
||||
val totalTokensCount = portfolioList.sumOf { it.tokens.size }
|
||||
if (totalTokensCount > 0) {
|
||||
TokenListUMData.AccountList(
|
||||
tokensList = portfolioList,
|
||||
totalTokensCount = totalTokensCount,
|
||||
)
|
||||
} else {
|
||||
val tokensList = accountList.flatMap { (_, currencyList) ->
|
||||
currencyList.asSequence().map { accountSwapCurrency ->
|
||||
accountListItemConverter.createAvailableItemConverter()
|
||||
.convert(accountSwapCurrency.cryptoCurrencyStatus)
|
||||
}.map(TokensListItemUM::Token).toPersistentList()
|
||||
}.toPersistentList()
|
||||
TokenListUMData.EmptyList
|
||||
}
|
||||
} else {
|
||||
val tokensList = accountList.flatMap { (_, currencyList) ->
|
||||
currencyList.asSequence().map { accountSwapCurrency ->
|
||||
accountListItemConverter.createAvailableItemConverter()
|
||||
.convert(accountSwapCurrency.cryptoCurrencyStatus)
|
||||
}.map(TokensListItemUM::Token).toPersistentList()
|
||||
}.toPersistentList()
|
||||
|
||||
if (tokensList.isNotEmpty()) {
|
||||
TokenListUMData.TokenList(
|
||||
tokensList = persistentListOf(
|
||||
TokensListItemUM.GroupTitle(
|
||||
id = "available_tokens_title",
|
||||
text = resourceReference(R.string.exchange_tokens_available_tokens_header),
|
||||
),
|
||||
) + tokensList,
|
||||
totalTokensCount = tokensList.size,
|
||||
)
|
||||
} else {
|
||||
TokenListUMData.EmptyList
|
||||
}
|
||||
},
|
||||
marketsState = currentMarketsState,
|
||||
onSearchEntered = onSearchEntered,
|
||||
onTokenSelected = onTokenSelected,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
isAfterSearch = tokensDataState.isAfterSearch,
|
||||
),
|
||||
if (tokensList.isNotEmpty()) {
|
||||
TokenListUMData.TokenList(
|
||||
tokensList = persistentListOf(
|
||||
TokensListItemUM.GroupTitle(
|
||||
id = "available_tokens_title",
|
||||
text = resourceReference(R.string.exchange_tokens_available_tokens_header),
|
||||
),
|
||||
) + tokensList,
|
||||
totalTokensCount = tokensList.size,
|
||||
)
|
||||
} else {
|
||||
TokenListUMData.EmptyList
|
||||
}
|
||||
},
|
||||
marketsState = marketState,
|
||||
onSearchEntered = onSearchEntered,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
isAfterSearch = tokensDataState.isAfterSearch,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,12 +10,10 @@ import com.arkivanov.decompose.router.slot.SlotNavigation
|
|||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState.InProgress.getApproveTypeOrNull
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.core.analytics.api.AnalyticsErrorHandler
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
|
|
@ -44,23 +42,18 @@ import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
|
|||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains
|
||||
import com.tangem.domain.express.models.ExpressOperationType
|
||||
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
||||
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.BlockchainErrorInfo
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenMarketListConfig
|
||||
import com.tangem.domain.markets.toSerializableParam
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.isHotWallet
|
||||
import com.tangem.domain.pay.WithdrawalResult
|
||||
import com.tangem.domain.promo.ShouldShowStoriesUseCase
|
||||
import com.tangem.domain.promo.models.StoryContentIds
|
||||
|
|
@ -77,8 +70,8 @@ import com.tangem.domain.transaction.models.TransactionFeeExtended
|
|||
import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.feature.swap.analytics.SwapEvents
|
||||
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridge
|
||||
import com.tangem.feature.swap.component.SwapFeeSelectorBlockComponent
|
||||
import com.tangem.feature.swap.converters.SwapTransactionErrorStateConverter
|
||||
import com.tangem.feature.swap.domain.SwapInteractor
|
||||
|
|
@ -89,9 +82,11 @@ import com.tangem.feature.swap.domain.models.ExpressException
|
|||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.domain.*
|
||||
import com.tangem.feature.swap.domain.models.ui.*
|
||||
import com.tangem.feature.swap.models.*
|
||||
import com.tangem.feature.swap.models.market.SwapMarketsListBatchFlowManager
|
||||
import com.tangem.feature.swap.models.market.state.SwapMarketState
|
||||
import com.tangem.feature.swap.models.SwapAlertUM
|
||||
import com.tangem.feature.swap.models.SwapCardState
|
||||
import com.tangem.feature.swap.models.SwapStateHolder
|
||||
import com.tangem.feature.swap.models.TransactionCardType
|
||||
import com.tangem.feature.swap.models.UiActions
|
||||
import com.tangem.feature.swap.models.states.SwapNotificationUM
|
||||
import com.tangem.feature.swap.router.SwapNavScreen
|
||||
import com.tangem.feature.swap.router.SwapRouter
|
||||
|
|
@ -99,20 +94,16 @@ import com.tangem.feature.swap.ui.StateBuilder
|
|||
import com.tangem.feature.swap.utils.formatToUIRepresentation
|
||||
import com.tangem.features.approval.api.GiveApprovalComponent
|
||||
import com.tangem.features.approval.api.GiveApprovalFeatureToggles
|
||||
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.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadTrigger
|
||||
import com.tangem.features.swap.SwapComponent
|
||||
import com.tangem.features.swap.SwapFeatureToggles
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP
|
||||
import com.tangem.utils.coroutines.*
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.DecimalFormat
|
||||
|
|
@ -153,15 +144,11 @@ internal class SwapModel @Inject constructor(
|
|||
private val tangemPayWithdrawUseCase: TangemPayWithdrawUseCase,
|
||||
private val iGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork,
|
||||
private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger,
|
||||
private val getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase,
|
||||
private val swapFeatureToggles: SwapFeatureToggles,
|
||||
private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory,
|
||||
private val excludedBlockchains: ExcludedBlockchains,
|
||||
private val getUserWalletsUseCase: GetWalletsUseCase,
|
||||
private val getTangemPayCustomerIdUseCase: GetTangemPayCustomerIdUseCase,
|
||||
private val appsFlyerStore: AppsFlyerStore,
|
||||
private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles,
|
||||
private val messageSender: UiMessageSender,
|
||||
chooseTokenBridgeFactory: ChooseTokenBridge.Factory,
|
||||
giveApprovalFeatureToggles: GiveApprovalFeatureToggles,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -180,6 +167,9 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
private val swapInteractor = swapInteractorFactory.create(userWalletId)
|
||||
|
||||
val isHoldToConfirmEnabled: Boolean =
|
||||
holdToConfirmButtonFeatureToggles.isHoldToConfirmEnabled && userWallet.isHotWallet
|
||||
|
||||
private lateinit var initialFromStatus: CryptoCurrencyStatus
|
||||
private var initialToStatus: CryptoCurrencyStatus? = null
|
||||
|
||||
|
|
@ -188,6 +178,8 @@ internal class SwapModel @Inject constructor(
|
|||
|
||||
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
|
||||
|
||||
val chooseTokenBridge: ChooseTokenBridge = chooseTokenBridgeFactory.create(modelScope)
|
||||
|
||||
private val stateBuilder = StateBuilder(
|
||||
userWalletProvider = Provider { userWallet },
|
||||
actions = createUiActions(),
|
||||
|
|
@ -204,7 +196,6 @@ internal class SwapModel @Inject constructor(
|
|||
?: error("NumberFormat is not DecimalFormat"),
|
||||
)
|
||||
private val amountDebouncer = Debouncer()
|
||||
private val searchDebouncer = Debouncer()
|
||||
private val singleTaskScheduler = SingleTaskScheduler<Map<SwapProvider, SwapState>>()
|
||||
|
||||
val dataStateStateFlow = MutableStateFlow(SwapProcessDataState())
|
||||
|
|
@ -253,44 +244,13 @@ internal class SwapModel @Inject constructor(
|
|||
|
||||
private val fromTokenBalanceJobHolder = JobHolder()
|
||||
private val toTokenBalanceJobHolder = JobHolder()
|
||||
private val addToPortfolioJobHolder = JobHolder()
|
||||
|
||||
private var isAmountChangedByUser: Boolean = false
|
||||
private var lastPermissionNotificationTokens: Pair<String, String>? = null
|
||||
|
||||
private val searchQueryState = MutableStateFlow("")
|
||||
private val visibleMarketItemIds = MutableStateFlow<List<CryptoCurrency.RawID>>(emptyList())
|
||||
private val visibleDefaultMarketItemIds = MutableStateFlow<List<CryptoCurrency.RawID>>(emptyList())
|
||||
private var latestMarketsState: SwapMarketState? = null
|
||||
|
||||
private val defaultMarketsListManager by lazy {
|
||||
SwapMarketsListBatchFlowManager(
|
||||
getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase,
|
||||
batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main,
|
||||
order = TokenMarketListConfig.Order.Trending,
|
||||
currentAppCurrency = Provider { selectedAppCurrencyFlow.value },
|
||||
currentSearchText = Provider { null },
|
||||
modelScope = modelScope,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
private val searchMarketsListManager by lazy {
|
||||
SwapMarketsListBatchFlowManager(
|
||||
getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase,
|
||||
batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search,
|
||||
order = TokenMarketListConfig.Order.ByRating,
|
||||
currentAppCurrency = Provider { selectedAppCurrencyFlow.value },
|
||||
currentSearchText = Provider { searchQueryState.value },
|
||||
modelScope = modelScope,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
val currentScreen: SwapNavScreen
|
||||
get() = swapRouter.currentScreen
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<AddToPortfolioRoute> = SlotNavigation()
|
||||
val approvalSlotNavigation = SlotNavigation<Unit>()
|
||||
private val shouldUseGaslessApproval: Boolean = giveApprovalFeatureToggles.isGaslessApprovalEnabled
|
||||
|
||||
|
|
@ -318,35 +278,30 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
val addToPortfolioCallback = object : AddToPortfolioComponent.Callback {
|
||||
override fun onDismiss() = bottomSheetNavigation.dismiss()
|
||||
|
||||
override fun onSuccess(addedToken: CryptoCurrency) {
|
||||
modelScope.launch {
|
||||
bottomSheetNavigation.dismiss()
|
||||
analyticsEventHandler.send(
|
||||
SwapEvents.ChooseTokenScreenResult(isTokenChosen = true, token = addedToken.symbol),
|
||||
)
|
||||
analyticsEventHandler.send(
|
||||
SwapAnalyticsEvent.TokenSelected(
|
||||
token = addedToken.symbol,
|
||||
source = ScreensSources.Markets,
|
||||
isSearched = searchQueryState.value.isNotEmpty(),
|
||||
),
|
||||
)
|
||||
searchQueryState.value = ""
|
||||
getAccountCurrencyStatusUseCase.invoke(userWalletId, addedToken)
|
||||
.firstOrNull {
|
||||
it.status.value is CryptoCurrencyStatus.Loaded
|
||||
}?.let { (account, status) ->
|
||||
applyAddedToken(status, account)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var addToPortfolioManager: AddToPortfolioManager? = null
|
||||
|
||||
init {
|
||||
chooseTokenBridge.searchQueryState
|
||||
.onEach { query -> onSearchEntered(query) }
|
||||
.launchIn(modelScope)
|
||||
|
||||
chooseTokenBridge.onNewTokenAdded.receiveAsFlow()
|
||||
.onEach { (addedToken, isSearched) ->
|
||||
applyAddedToken(addedToken, isSearched.value)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
|
||||
chooseTokenBridge.onTokenSelected.receiveAsFlow()
|
||||
.onEach { (addedToken, isSearched) ->
|
||||
onTokenSelect(addedToken, isSearched.value)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
|
||||
chooseTokenBridge.onClose.receiveAsFlow()
|
||||
.onEach {
|
||||
analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(isTokenChosen = false))
|
||||
swapRouter.back()
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
|
||||
modelScope.launch {
|
||||
val storyId = StoryContentIds.STORY_FIRST_TIME_SWAP.id
|
||||
if (shouldShowStoriesUseCase.invokeSync(storyId)) {
|
||||
|
|
@ -420,32 +375,6 @@ internal class SwapModel @Inject constructor(
|
|||
uiState = stateBuilder.updateBalanceHiddenState(uiState, isBalanceHidden)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
|
||||
subscribeMarketTokens()
|
||||
|
||||
modelScope.launch {
|
||||
visibleMarketItemIds.mapNotNull { rawIDS ->
|
||||
if (rawIDS.isNotEmpty()) {
|
||||
searchMarketsListManager.getBatchKeysByItemIds(rawIDS)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.distinctUntilChanged().collectLatest { visibleBatchKeys ->
|
||||
searchMarketsListManager.loadCharts(visibleBatchKeys)
|
||||
}
|
||||
}
|
||||
|
||||
modelScope.launch {
|
||||
visibleDefaultMarketItemIds.mapNotNull { rawIds ->
|
||||
if (rawIds.isNotEmpty()) {
|
||||
defaultMarketsListManager.getBatchKeysByItemIds(rawIds)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.distinctUntilChanged().collectLatest { visibleBatchKeys ->
|
||||
defaultMarketsListManager.loadCharts(visibleBatchKeys)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onStart() {
|
||||
|
|
@ -471,41 +400,6 @@ internal class SwapModel @Inject constructor(
|
|||
analyticsEventHandler.send(SwapEvents.ChooseTokenScreenOpened(hasAvailableTokens = isAnyAvailableTokens))
|
||||
}
|
||||
|
||||
private fun subscribeMarketTokens() {
|
||||
if (swapFeatureToggles.isMarketListFeatureEnabled) {
|
||||
// Switch between default and search market flows
|
||||
searchQueryState
|
||||
.map { it.isEmpty() }
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { isDefaultMode ->
|
||||
if (isDefaultMode) {
|
||||
visibleMarketItemIds.value = emptyList()
|
||||
createDefaultMarketsFlow()
|
||||
} else {
|
||||
visibleDefaultMarketItemIds.value = emptyList()
|
||||
createSearchMarketsFlow()
|
||||
}
|
||||
}
|
||||
.onEach { marketsState ->
|
||||
latestMarketsState = marketsState
|
||||
applyMarketsState(marketsState)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
|
||||
// Reload search markets when query changes
|
||||
searchQueryState
|
||||
.onEach { searchQuery ->
|
||||
if (searchQuery.isNotEmpty()) {
|
||||
searchMarketsListManager.reload(searchQuery)
|
||||
}
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
|
||||
// Initial load of default markets
|
||||
defaultMarketsListManager.reload()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun initTokens(isReverseFromTo: Boolean) {
|
||||
modelScope.launch(dispatchers.main) {
|
||||
|
|
@ -547,18 +441,18 @@ internal class SwapModel @Inject constructor(
|
|||
val fromCryptoCurrency = dataState.fromCryptoCurrency
|
||||
|
||||
if (isApplied && fromCryptoCurrency != null) {
|
||||
Timber.i(
|
||||
TangemLogger.i(
|
||||
"updateFeePaidCryptoCurrencyFor: id = ${fromCryptoCurrency.currency.id}, " +
|
||||
"isReverseFromTo: $isReverseFromTo",
|
||||
)
|
||||
updateFeePaidCryptoCurrencyFor(fromCryptoCurrency)
|
||||
} else {
|
||||
Timber.e("updateFeePaidCryptoCurrencyFor failed: fromCryptoCurrency is null")
|
||||
TangemLogger.e("updateFeePaidCryptoCurrencyFor failed: fromCryptoCurrency is null")
|
||||
}
|
||||
|
||||
subscribeToCoinBalanceUpdatesIfNeeded()
|
||||
}.onFailure { error ->
|
||||
Timber.e(error)
|
||||
TangemLogger.e("Error", error)
|
||||
|
||||
applyInitialTokenChoice(
|
||||
state = TokensDataStateExpress.EMPTY,
|
||||
|
|
@ -582,26 +476,40 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun applyAddedToken(addedToken: CryptoCurrencyStatus, addedAccount: Account.CryptoPortfolio?) {
|
||||
modelScope.launch {
|
||||
runCatching(dispatchers.io) {
|
||||
swapInteractor.getTokensDataState(initialCurrencyFrom)
|
||||
}.onSuccess { state ->
|
||||
updateTokensState(state)
|
||||
private suspend fun applyAddedToken(addedToken: CryptoCurrency, isSearched: Boolean) {
|
||||
analyticsEventHandler.send(
|
||||
SwapEvents.ChooseTokenScreenResult(isTokenChosen = true, token = addedToken.symbol),
|
||||
)
|
||||
analyticsEventHandler.send(
|
||||
SwapAnalyticsEvent.TokenSelected(
|
||||
token = addedToken.symbol,
|
||||
source = ScreensSources.Markets,
|
||||
isSearched = isSearched,
|
||||
),
|
||||
)
|
||||
val status = getAccountCurrencyStatusUseCase.invoke(userWalletId, addedToken)
|
||||
// todo swap are sure?? about status.value is CryptoCurrencyStatus.Loaded
|
||||
.firstOrNull { it.status.value is CryptoCurrencyStatus.Loaded }
|
||||
?: return
|
||||
val (selectedAccount, selectedCurrency) = status
|
||||
|
||||
applyInitialTokenChoice(
|
||||
state = state,
|
||||
selectedCurrency = addedToken,
|
||||
selectedAccount = addedAccount,
|
||||
isReverseFromTo = isOrderReversed.value,
|
||||
)
|
||||
runCatching(dispatchers.io) {
|
||||
swapInteractor.getTokensDataState(initialCurrencyFrom)
|
||||
}.onSuccess { state ->
|
||||
updateTokensState(state)
|
||||
|
||||
subscribeToCoinBalanceUpdatesIfNeeded()
|
||||
applyInitialTokenChoice(
|
||||
state = state,
|
||||
selectedCurrency = selectedCurrency,
|
||||
selectedAccount = selectedAccount,
|
||||
isReverseFromTo = isOrderReversed.value,
|
||||
)
|
||||
|
||||
swapRouter.back()
|
||||
}.onFailure { error ->
|
||||
Timber.e(error)
|
||||
}
|
||||
subscribeToCoinBalanceUpdatesIfNeeded()
|
||||
|
||||
swapRouter.back()
|
||||
}.onFailure { error ->
|
||||
TangemLogger.e("Error", error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -635,7 +543,7 @@ internal class SwapModel @Inject constructor(
|
|||
): Boolean {
|
||||
// exceptional case
|
||||
if (selectedCurrency == null) {
|
||||
Timber.e("No available tokens to swap for ${initialCurrencyFrom.symbol}")
|
||||
TangemLogger.e("No available tokens to swap for ${initialCurrencyFrom.symbol}")
|
||||
analyticsEventHandler.send(SwapEvents.NoticeNoAvailableTokensToSwap())
|
||||
uiState = stateBuilder.createNoAvailableTokensToSwapState(
|
||||
uiStateHolder = uiState,
|
||||
|
|
@ -691,23 +599,7 @@ internal class SwapModel @Inject constructor(
|
|||
|
||||
private fun updateTokensState(tokenDataState: TokensDataStateExpress) {
|
||||
val tokensDataState = if (isOrderReversed.value) tokenDataState.fromGroup else tokenDataState.toGroup
|
||||
|
||||
uiState = stateBuilder.addTokensToStateV2(
|
||||
uiState = uiState,
|
||||
tokensDataState = tokensDataState,
|
||||
isAccountsMode = isAccountsMode,
|
||||
)
|
||||
latestMarketsState?.let(::applyMarketsState)
|
||||
}
|
||||
|
||||
private fun applyMarketsState(marketsState: SwapMarketState) {
|
||||
uiState.selectTokenState?.let { currentSelectState ->
|
||||
uiState = uiState.copy(
|
||||
selectTokenState = currentSelectState.copy(
|
||||
marketsState = marketsState,
|
||||
),
|
||||
)
|
||||
}
|
||||
chooseTokenBridge.updateCurrenciesGroup(tokensDataState)
|
||||
}
|
||||
|
||||
private fun startLoadingQuotes(
|
||||
|
|
@ -773,10 +665,10 @@ internal class SwapModel @Inject constructor(
|
|||
userWalletId = userWalletId,
|
||||
cryptoCurrencyStatus = fromToken,
|
||||
)
|
||||
.onLeft { Timber.e("Unable to get fee paid crypto currency status for ${fromToken.currency.id}") }
|
||||
.onLeft { TangemLogger.e("Unable to get fee paid crypto currency status for ${fromToken.currency.id}") }
|
||||
.onRight { currencyStatus ->
|
||||
if (currencyStatus == null) {
|
||||
Timber.e("Fee paid crypto currency status is null for ${fromToken.currency.id}")
|
||||
TangemLogger.e("Fee paid crypto currency status is null for ${fromToken.currency.id}")
|
||||
}
|
||||
}
|
||||
.getOrNull(),
|
||||
|
|
@ -836,11 +728,11 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
} else {
|
||||
feeSelectorRepository.state.value = FeeSelectorUM.Error(GetFeeError.UnknownError, isHidden = true)
|
||||
Timber.e("Accidentally empty quotes list")
|
||||
TangemLogger.e("Accidentally empty quotes list")
|
||||
}
|
||||
},
|
||||
onError = { error ->
|
||||
Timber.e("Error when loading quotes: $error")
|
||||
TangemLogger.e("Error when loading quotes: $error")
|
||||
feeSelectorRepository.state.value = FeeSelectorUM.Error(GetFeeError.UnknownError, isHidden = true)
|
||||
uiState = stateBuilder.addNotification(uiState, null) { startLoadingQuotesFromLastState() }
|
||||
},
|
||||
|
|
@ -1047,14 +939,14 @@ internal class SwapModel @Inject constructor(
|
|||
val provider = requireNotNull(dataState.selectedProvider) { "Selected provider is null" }
|
||||
val lastLoadedQuotesState = dataState.lastLoadedSwapStates[provider] as? SwapState.QuotesLoadedState
|
||||
if (lastLoadedQuotesState == null) {
|
||||
Timber.e("Last loaded quotes state is null")
|
||||
TangemLogger.e("Last loaded quotes state is null")
|
||||
return
|
||||
}
|
||||
val fromCurrency = requireNotNull(dataState.fromCryptoCurrency)
|
||||
val fee = getSelectedFee()
|
||||
|
||||
if (fee == null && tangemPayInput?.isWithdrawal != true) {
|
||||
Timber.e("onSwapClick: fee is null and isWithdrawal is ${tangemPayInput?.isWithdrawal}")
|
||||
TangemLogger.e("onSwapClick: fee is null and isWithdrawal is ${tangemPayInput?.isWithdrawal}")
|
||||
showAlert(resourceReference(R.string.swapping_fee_estimation_error_text))
|
||||
modelScope.launch {
|
||||
delay(SWAP_IN_PROGRESS_DELAY)
|
||||
|
|
@ -1081,7 +973,7 @@ internal class SwapModel @Inject constructor(
|
|||
when (swapTransactionState) {
|
||||
is SwapTransactionState.TxSent -> {
|
||||
if (fee == null) {
|
||||
Timber.e("onSwapClick: onSuccess: fee is null after swap")
|
||||
TangemLogger.e("onSwapClick: onSuccess: fee is null after swap")
|
||||
showAlert(resourceReference(R.string.swapping_fee_estimation_error_text))
|
||||
return@onSuccess
|
||||
}
|
||||
|
|
@ -1093,7 +985,7 @@ internal class SwapModel @Inject constructor(
|
|||
txHash = swapTransactionState.txHash,
|
||||
currency = fromCurrency.currency,
|
||||
).getOrElse {
|
||||
Timber.i("tx hash explore not supported")
|
||||
TangemLogger.i("tx hash explore not supported")
|
||||
""
|
||||
}
|
||||
|
||||
|
|
@ -1137,7 +1029,7 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
}.onFailure { error ->
|
||||
Timber.e(error)
|
||||
TangemLogger.e("Error", error)
|
||||
startLoadingQuotesFromLastState()
|
||||
showAlert()
|
||||
}
|
||||
|
|
@ -1232,7 +1124,7 @@ internal class SwapModel @Inject constructor(
|
|||
val feeForPermission = when (val fee = approveDataModel.fee) {
|
||||
TxFeeState.Empty -> {
|
||||
showAlert(resourceReference(R.string.swapping_fee_estimation_error_text))
|
||||
Timber.e("Fee should not be Empty")
|
||||
TangemLogger.e("Fee should not be Empty")
|
||||
return@launch
|
||||
}
|
||||
is TxFeeState.MultipleFeeState -> fee.priorityFee
|
||||
|
|
@ -1272,72 +1164,68 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
}.onFailure { showAlert() }
|
||||
}.onFailure { error ->
|
||||
Timber.e(error.message.orEmpty())
|
||||
TangemLogger.e(error.message.orEmpty())
|
||||
showAlert()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSearchEntered(searchQuery: String) {
|
||||
searchDebouncer.debounce(modelScope, DEBOUNCE_SEARCH_DELAY) {
|
||||
searchQueryState.value = searchQuery
|
||||
|
||||
val tokenDataState = dataState.tokensDataState ?: return@debounce
|
||||
val group = if (isOrderReversed.value) {
|
||||
tokenDataState.fromGroup
|
||||
} else {
|
||||
tokenDataState.toGroup
|
||||
}
|
||||
|
||||
val available = group.available.filter { swapAvailability ->
|
||||
swapAvailability.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) ||
|
||||
swapAvailability.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true)
|
||||
}
|
||||
val unavailable = group.unavailable.filter { swapAvailability ->
|
||||
swapAvailability.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) ||
|
||||
swapAvailability.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true)
|
||||
}
|
||||
val accountCurrencyList = group.accountCurrencyList.mapNotNull { accountSwapAvailability ->
|
||||
val filteredCurrencies = accountSwapAvailability.currencyList.filter { accountSwapCurrency ->
|
||||
val currency = accountSwapCurrency.cryptoCurrencyStatus.currency
|
||||
currency.name.contains(searchQuery, ignoreCase = true) ||
|
||||
currency.symbol.contains(searchQuery, ignoreCase = true)
|
||||
}
|
||||
|
||||
if (filteredCurrencies.isEmpty()) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
accountSwapAvailability.copy(
|
||||
currencyList = filteredCurrencies,
|
||||
)
|
||||
}
|
||||
|
||||
val filteredTokenDataState = if (isOrderReversed.value) {
|
||||
tokenDataState.copy(
|
||||
fromGroup = tokenDataState.fromGroup.copy(
|
||||
available = available,
|
||||
unavailable = unavailable,
|
||||
accountCurrencyList = accountCurrencyList,
|
||||
isAfterSearch = true,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
tokenDataState.copy(
|
||||
toGroup = tokenDataState.toGroup.copy(
|
||||
available = available,
|
||||
unavailable = unavailable,
|
||||
accountCurrencyList = accountCurrencyList,
|
||||
isAfterSearch = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
updateTokensState(filteredTokenDataState)
|
||||
val tokenDataState = dataState.tokensDataState ?: return
|
||||
val group = if (isOrderReversed.value) {
|
||||
tokenDataState.fromGroup
|
||||
} else {
|
||||
tokenDataState.toGroup
|
||||
}
|
||||
|
||||
val available = group.available.filter { swapAvailability ->
|
||||
swapAvailability.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) ||
|
||||
swapAvailability.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true)
|
||||
}
|
||||
val unavailable = group.unavailable.filter { swapAvailability ->
|
||||
swapAvailability.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) ||
|
||||
swapAvailability.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true)
|
||||
}
|
||||
val accountCurrencyList = group.accountCurrencyList.mapNotNull { accountSwapAvailability ->
|
||||
val filteredCurrencies = accountSwapAvailability.currencyList.filter { accountSwapCurrency ->
|
||||
val currency = accountSwapCurrency.cryptoCurrencyStatus.currency
|
||||
currency.name.contains(searchQuery, ignoreCase = true) ||
|
||||
currency.symbol.contains(searchQuery, ignoreCase = true)
|
||||
}
|
||||
|
||||
if (filteredCurrencies.isEmpty()) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
accountSwapAvailability.copy(
|
||||
currencyList = filteredCurrencies,
|
||||
)
|
||||
}
|
||||
|
||||
val filteredTokenDataState = if (isOrderReversed.value) {
|
||||
tokenDataState.copy(
|
||||
fromGroup = tokenDataState.fromGroup.copy(
|
||||
available = available,
|
||||
unavailable = unavailable,
|
||||
accountCurrencyList = accountCurrencyList,
|
||||
isAfterSearch = true,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
tokenDataState.copy(
|
||||
toGroup = tokenDataState.toGroup.copy(
|
||||
available = available,
|
||||
unavailable = unavailable,
|
||||
accountCurrencyList = accountCurrencyList,
|
||||
isAfterSearch = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
updateTokensState(filteredTokenDataState)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun onTokenSelect(id: String) {
|
||||
private fun onTokenSelect(id: String, isSearched: Boolean) {
|
||||
val tokens = dataState.tokensDataState ?: return
|
||||
val (foundToken, foundAccount) = getSelectedTokenAndAccount(tokens, id)
|
||||
|
||||
|
|
@ -1350,7 +1238,7 @@ internal class SwapModel @Inject constructor(
|
|||
SwapAnalyticsEvent.TokenSelected(
|
||||
token = symbol,
|
||||
source = ScreensSources.Portfolio,
|
||||
isSearched = searchQueryState.value.isNotEmpty(),
|
||||
isSearched = isSearched,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -1417,7 +1305,7 @@ internal class SwapModel @Inject constructor(
|
|||
return
|
||||
}
|
||||
modelScope.launch {
|
||||
Timber.i(
|
||||
TangemLogger.i(
|
||||
"updateFeePaidCryptoCurrencyFor: id = ${fromToken.currency.id}, " +
|
||||
"isOrderReversed: ${isOrderReversed.value}",
|
||||
)
|
||||
|
|
@ -1465,14 +1353,14 @@ internal class SwapModel @Inject constructor(
|
|||
coin: CryptoCurrency.Coin,
|
||||
isFromCurrency: Boolean,
|
||||
) {
|
||||
Timber.d("Subscribe to ${coin.id} balance updates")
|
||||
TangemLogger.d("Subscribe to ${coin.id} balance updates")
|
||||
|
||||
getAccountCurrencyStatusUseCase(
|
||||
userWalletId = userWalletId,
|
||||
currency = coin,
|
||||
).distinctUntilChanged { old, new -> old.status.value.amount == new.status.value.amount } // Check only balance changes
|
||||
.onEach { (account, currencyStatus) ->
|
||||
Timber.d("${coin.id} balance is ${currencyStatus.value.amount ?: "null"}")
|
||||
TangemLogger.d("${coin.id} balance is ${currencyStatus.value.amount ?: "null"}")
|
||||
|
||||
if (isFromCurrency) {
|
||||
dataState = dataState.copy(
|
||||
|
|
@ -1481,14 +1369,14 @@ internal class SwapModel @Inject constructor(
|
|||
cryptoCurrencyStatus = currencyStatus,
|
||||
)
|
||||
.onLeft {
|
||||
Timber.e(
|
||||
TangemLogger.e(
|
||||
"Coin balance: Unable to get fee paid crypto currency status for " +
|
||||
"${currencyStatus.currency.id}",
|
||||
)
|
||||
}
|
||||
.onRight { status ->
|
||||
if (status == null) {
|
||||
Timber.e(
|
||||
TangemLogger.e(
|
||||
"Coin balance: Fee paid crypto currency status is null " +
|
||||
"for ${currencyStatus.currency.id}",
|
||||
)
|
||||
|
|
@ -1542,7 +1430,7 @@ internal class SwapModel @Inject constructor(
|
|||
toAccount = newToAccount,
|
||||
)
|
||||
isOrderReversed.value = !isOrderReversed.value
|
||||
Timber.i(
|
||||
TangemLogger.i(
|
||||
"updateFeePaidCryptoCurrencyFor: id = ${newFromToken.currency.id}, " +
|
||||
"isOrderReversed: ${isOrderReversed.value}",
|
||||
)
|
||||
|
|
@ -1745,8 +1633,6 @@ internal class SwapModel @Inject constructor(
|
|||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
private fun createUiActions(): UiActions {
|
||||
return UiActions(
|
||||
onSearchEntered = { onSearchEntered(it) },
|
||||
onTokenSelected = { onTokenSelect(it) },
|
||||
onAmountChanged = { onAmountChanged(it) },
|
||||
onSwapClick = {
|
||||
onSwapClick()
|
||||
|
|
@ -1774,9 +1660,6 @@ internal class SwapModel @Inject constructor(
|
|||
if (bottomSheet != null && bottomSheet.isShown) {
|
||||
uiState = stateBuilder.dismissBottomSheet(uiState)
|
||||
} else {
|
||||
if (swapRouter.currentScreen == SwapNavScreen.SelectToken) {
|
||||
analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(isTokenChosen = false))
|
||||
}
|
||||
swapRouter.back()
|
||||
}
|
||||
onSearchEntered("")
|
||||
|
|
@ -1862,7 +1745,7 @@ internal class SwapModel @Inject constructor(
|
|||
onReceiveCardWarningClick = {
|
||||
val selectedProvider = dataState.selectedProvider ?: return@UiActions
|
||||
val currencySymbol = dataState.toCryptoCurrency?.currency?.symbol ?: return@UiActions
|
||||
val isPriceImpact = uiState.priceImpact is PriceImpact.Value
|
||||
val isPriceImpact = uiState.priceImpact.type != PriceImpact.Type.NONE
|
||||
showSwapInfoAlert(isPriceImpact, currencySymbol, selectedProvider)
|
||||
},
|
||||
onLinkClick = urlOpener::openUrl,
|
||||
|
|
@ -2224,110 +2107,6 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun createDefaultMarketsFlow(): Flow<SwapMarketState> {
|
||||
val marketsTitle = TextReference.Res(R.string.feed_trending_now)
|
||||
return combine(
|
||||
defaultMarketsListManager.uiItems,
|
||||
defaultMarketsListManager.isInInitialLoadingErrorState,
|
||||
defaultMarketsListManager.totalCount,
|
||||
) { uiItems, isError, total ->
|
||||
when {
|
||||
isError -> SwapMarketState.LoadingError(
|
||||
onRetryClicked = { defaultMarketsListManager.reload() },
|
||||
marketsTitle = marketsTitle,
|
||||
shouldAssetsCount = false,
|
||||
)
|
||||
uiItems.isEmpty() -> SwapMarketState.Loading(
|
||||
marketsTitle = marketsTitle,
|
||||
shouldAssetsCount = false,
|
||||
)
|
||||
else -> SwapMarketState.Content(
|
||||
items = uiItems,
|
||||
loadMore = { defaultMarketsListManager.loadMore() },
|
||||
onItemClick = { item -> addToPortfolioItem(item) },
|
||||
visibleIdsChanged = { visibleDefaultMarketItemIds.value = it },
|
||||
total = total ?: uiItems.size,
|
||||
marketsTitle = marketsTitle,
|
||||
shouldAssetsCount = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSearchMarketsFlow(): Flow<SwapMarketState> {
|
||||
val marketsTitle = TextReference.Res(R.string.markets_common_title)
|
||||
return combine(
|
||||
flow = searchMarketsListManager.uiItems,
|
||||
flow2 = searchMarketsListManager.isInInitialLoadingErrorState,
|
||||
flow3 = searchMarketsListManager.isSearchNotFoundState,
|
||||
flow4 = searchMarketsListManager.totalCount,
|
||||
) { uiItems, isError, isSearchNotFound, total ->
|
||||
when {
|
||||
isError -> SwapMarketState.LoadingError(
|
||||
onRetryClicked = { searchMarketsListManager.reload(searchQueryState.value) },
|
||||
marketsTitle = marketsTitle,
|
||||
shouldAssetsCount = true,
|
||||
)
|
||||
isSearchNotFound -> SwapMarketState.SearchNothingFound
|
||||
uiItems.isEmpty() -> SwapMarketState.Loading(
|
||||
marketsTitle = marketsTitle,
|
||||
shouldAssetsCount = true,
|
||||
)
|
||||
else -> SwapMarketState.Content(
|
||||
items = uiItems,
|
||||
loadMore = { searchMarketsListManager.loadMore() },
|
||||
onItemClick = { item -> addToPortfolioItem(item) },
|
||||
visibleIdsChanged = { visibleMarketItemIds.value = it },
|
||||
total = total ?: uiItems.size,
|
||||
marketsTitle = marketsTitle,
|
||||
shouldAssetsCount = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addToPortfolioItem(item: MarketsListItemUM) {
|
||||
modelScope.launch {
|
||||
val tokenMarket = defaultMarketsListManager.getTokenMarketById(item.id)
|
||||
?: searchMarketsListManager.getTokenMarketById(item.id)
|
||||
?: return@launch
|
||||
|
||||
val param = tokenMarket.toSerializableParam()
|
||||
val hasOnlyHotWallets = getUserWalletsUseCase.invokeSync().all { it is UserWallet.Hot }
|
||||
|
||||
val networks = tokenMarket.networks?.filter { network ->
|
||||
BlockchainUtils.isSupportedNetworkId(
|
||||
blockchainId = network.networkId,
|
||||
coinId = tokenMarket.id.value,
|
||||
contractAddress = network.contractAddress,
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
hotExcludedBlockchains = hotWalletExcludedBlockchains,
|
||||
hasOnlyHotWallets = hasOnlyHotWallets,
|
||||
)
|
||||
}?.map { network ->
|
||||
TokenMarketInfo.Network(
|
||||
networkId = network.networkId,
|
||||
isExchangeable = false,
|
||||
contractAddress = network.contractAddress,
|
||||
decimalCount = network.decimalCount,
|
||||
)
|
||||
}.orEmpty()
|
||||
|
||||
addToPortfolioManager = addToPortfolioManagerFactory
|
||||
.create(
|
||||
scope = modelScope,
|
||||
token = param,
|
||||
analyticsParams = AddToPortfolioManager.AnalyticsParams(source = ScreensSources.Swap.value),
|
||||
).apply {
|
||||
setTokenNetworks(networks)
|
||||
}
|
||||
|
||||
addToPortfolioManager?.state
|
||||
?.firstOrNull { it is AddToPortfolioManager.State.AvailableToAdd }
|
||||
?.run { bottomSheetNavigation.activate(AddToPortfolioRoute) }
|
||||
}.saveIn(addToPortfolioJobHolder)
|
||||
}
|
||||
|
||||
private fun CryptoCurrency.getNetworkInfo(): NetworkInfo {
|
||||
return NetworkInfo(
|
||||
name = this.network.name,
|
||||
|
|
@ -2354,10 +2133,7 @@ internal class SwapModel @Inject constructor(
|
|||
val feeStateUM = feeSelectorRepository.state.value as? FeeSelectorUM.Content
|
||||
|
||||
if (feeStateUM == null) {
|
||||
Timber.e(
|
||||
"getSelectedFeeState: FeeSelectorUM is not Content: ${feeSelectorRepository.state.value}, " +
|
||||
"returning Legacy state",
|
||||
)
|
||||
TangemLogger.e("getSelectedFeeState: FeeSelectorUM is not Content: $feeStateUM, returning Legacy state")
|
||||
return TxFeeSealedState.Legacy(
|
||||
txFeeState = TxFeeState.Empty,
|
||||
selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL,
|
||||
|
|
@ -2379,9 +2155,7 @@ internal class SwapModel @Inject constructor(
|
|||
val feeStateUM = feeSelectorRepository.state.value as? FeeSelectorUM.Content
|
||||
|
||||
if (feeStateUM == null) {
|
||||
Timber.e(
|
||||
"getSelectedFee: FeeSelectorUM is not Content: ${feeSelectorRepository.state.value}, returning null",
|
||||
)
|
||||
TangemLogger.e("getSelectedFee: FeeSelectorUM is not Content: $feeStateUM, returning null")
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
@ -2440,7 +2214,7 @@ internal class SwapModel @Inject constructor(
|
|||
|
||||
if (newState is FeeSelectorUM.Error) {
|
||||
modelScope.launch {
|
||||
Timber.e("onResult: FeeSelectorUM is Error, isHidden = true")
|
||||
TangemLogger.e("onResult: FeeSelectorUM is Error, isHidden = true")
|
||||
forceUpdateState.emit(newState.copy(isHidden = true))
|
||||
}
|
||||
return
|
||||
|
|
@ -2455,7 +2229,7 @@ internal class SwapModel @Inject constructor(
|
|||
newState.feeExtraInfo.feeCryptoCurrencyStatus.currency is CryptoCurrency.Coin
|
||||
|
||||
if (isFeeCurrencySameAsFromCurrency || isCoinFeeSelected) {
|
||||
Timber.e("onResult: Fee currency is same as from currency or coin fee selected, reloading quotes")
|
||||
TangemLogger.e("onResult: Fee currency is same as from currency or coin fee selected, reloading quotes")
|
||||
|
||||
// block swap button until fee is loaded
|
||||
uiState = uiState.copy(
|
||||
|
|
@ -2479,19 +2253,19 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
|
||||
override suspend fun loadFee(): Either<GetFeeError, TransactionFee> {
|
||||
Timber.e("loadFee: Start loading fee")
|
||||
TangemLogger.e("loadFee: Start loading fee")
|
||||
|
||||
val fromToken = dataState.fromCryptoCurrency ?: return Either.Left(GetFeeError.UnknownError)
|
||||
val toToken = dataState.toCryptoCurrency ?: return Either.Left(GetFeeError.UnknownError)
|
||||
val selectedProvider = dataStateStateFlow.first { it.selectedProvider != null }.selectedProvider!!
|
||||
|
||||
if (dataState.lastLoadedSwapStates[selectedProvider] !is SwapState.QuotesLoadedState) {
|
||||
Timber.e("loadFee: Quotes not loaded ${dataState.lastLoadedSwapStates[selectedProvider]}")
|
||||
TangemLogger.e("loadFee: Quotes not loaded ${dataState.lastLoadedSwapStates[selectedProvider]}")
|
||||
return Either.Left(GetFeeError.UnknownError)
|
||||
}
|
||||
|
||||
if (isPermissionNotificationShown()) {
|
||||
Timber.e("loadFee: Permission notification is shown, cannot load fee")
|
||||
TangemLogger.e("loadFee: Permission notification is shown, cannot load fee")
|
||||
return Either.Left(GetFeeError.UnknownError)
|
||||
}
|
||||
|
||||
|
|
@ -2505,10 +2279,10 @@ internal class SwapModel @Inject constructor(
|
|||
reduceBalanceBy = lastReducedBalanceBy.value,
|
||||
)
|
||||
.onLeft {
|
||||
Timber.e("loadFee: Failed to load fee with error $it")
|
||||
TangemLogger.e("loadFee: Failed to load fee with error $it")
|
||||
}
|
||||
.onRight {
|
||||
Timber.e("loadFee: Fee loaded successfully")
|
||||
TangemLogger.e("loadFee: Fee loaded successfully")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2529,7 +2303,6 @@ internal class SwapModel @Inject constructor(
|
|||
const val INITIAL_AMOUNT = ""
|
||||
const val UPDATE_DELAY = 10000L
|
||||
const val DEBOUNCE_AMOUNT_DELAY = 1000L
|
||||
const val DEBOUNCE_SEARCH_DELAY = 500L
|
||||
const val UPDATE_BALANCE_DELAY_MILLIS = 11000L
|
||||
const val SWAP_IN_PROGRESS_DELAY = 200L
|
||||
const val CHANGELLY_PROVIDER_ID = "changelly"
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ internal class SwapNotificationsFactory(
|
|||
maybeAddNetworkFeeCoverageWarning(quoteModel, selectedFeeType)
|
||||
maybeAddUnableCoverFeeWarning(quoteModel, fromToken, hideFee)
|
||||
maybeAddTransactionInProgressWarning(quoteModel)
|
||||
maybeAddPriceImpactNotification(quoteModel.priceImpact)
|
||||
}
|
||||
return warnings.toPersistentList()
|
||||
}
|
||||
|
|
@ -143,6 +144,18 @@ internal class SwapNotificationsFactory(
|
|||
}
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.maybeAddPriceImpactNotification(priceImpact: PriceImpact) {
|
||||
if (priceImpact.amountSignificance == PriceImpact.AmountSignificance.LOW) return
|
||||
|
||||
val notification = when (priceImpact.type) {
|
||||
PriceImpact.Type.HIGH -> SwapNotificationUM.Warning.TradeTooHigh
|
||||
PriceImpact.Type.MEDIUM -> SwapNotificationUM.Warning.HighPriceImpact
|
||||
else -> return
|
||||
}
|
||||
|
||||
add(notification)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun MutableList<NotificationUM>.maybeAddDomainWarnings(
|
||||
quoteModel: SwapState.QuotesLoadedState,
|
||||
|
|
|
|||
|
|
@ -1,43 +1,20 @@
|
|||
package com.tangem.feature.swap.models
|
||||
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.feature.swap.models.market.state.SwapMarketState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal data class SwapSelectTokenStateHolder(
|
||||
val availableTokens: ImmutableList<TokenToSelectState>,
|
||||
val unavailableTokens: ImmutableList<TokenToSelectState>,
|
||||
val marketsState: SwapMarketState? = null,
|
||||
val marketsState: SwapMarketState,
|
||||
val tokensListData: TokenListUMData,
|
||||
val isBalanceHidden: Boolean,
|
||||
val isAfterSearch: Boolean,
|
||||
val onSearchEntered: (String) -> Unit,
|
||||
val onTokenSelected: (String) -> Unit,
|
||||
)
|
||||
|
||||
internal sealed class TokenToSelectState {
|
||||
|
||||
data class Title(val title: TextReference) : TokenToSelectState()
|
||||
|
||||
data class TokenToSelect(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val symbol: String,
|
||||
val tokenIcon: CurrencyIconState,
|
||||
val isAvailable: Boolean = true,
|
||||
val addedTokenBalanceData: TokenBalanceData? = null,
|
||||
) : TokenToSelectState()
|
||||
}
|
||||
|
||||
internal data class TokenBalanceData(
|
||||
val amount: String?,
|
||||
val amountEquivalent: String?,
|
||||
val isBalanceHidden: Boolean,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
internal sealed interface TokenListUMData {
|
||||
|
||||
val tokensList: ImmutableList<TokensListItemUM>
|
||||
|
|
@ -64,11 +41,11 @@ internal sealed interface TokenListUMData {
|
|||
}
|
||||
|
||||
internal val SwapSelectTokenStateHolder.isNotFoundState: Boolean
|
||||
get() = availableTokens.isEmpty() && unavailableTokens.isEmpty() &&
|
||||
get() =
|
||||
tokensListData.tokensList.isEmpty() && isAfterSearch &&
|
||||
marketsState !is SwapMarketState.Content && marketsState !is SwapMarketState.Loading
|
||||
marketsState !is SwapMarketState.Content && marketsState !is SwapMarketState.Loading
|
||||
|
||||
internal val SwapSelectTokenStateHolder.isEmptyState: Boolean
|
||||
get() = availableTokens.isEmpty() && unavailableTokens.isEmpty() &&
|
||||
get() =
|
||||
tokensListData.tokensList.isEmpty() && !isAfterSearch &&
|
||||
marketsState !is SwapMarketState.Content && marketsState !is SwapMarketState.Loading
|
||||
marketsState !is SwapMarketState.Content && marketsState !is SwapMarketState.Loading
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.feature.swap.models
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState
|
||||
|
|
@ -28,7 +29,6 @@ internal data class SwapStateHolder(
|
|||
val priceImpact: PriceImpact,
|
||||
|
||||
val successState: SwapSuccessStateHolder? = null,
|
||||
val selectTokenState: SwapSelectTokenStateHolder? = null,
|
||||
val bottomSheetConfig: TangemBottomSheetConfig? = null,
|
||||
val swapButton: SwapButton,
|
||||
val shouldShowMaxAmount: Boolean,
|
||||
|
|
@ -48,7 +48,7 @@ sealed class SwapCardState {
|
|||
data class SwapCardData(
|
||||
@DrawableRes val networkIconRes: Int?,
|
||||
val type: TransactionCardType,
|
||||
val amountEquivalent: String?,
|
||||
val amountEquivalent: TextReference?,
|
||||
val token: CryptoCurrencyStatus?,
|
||||
val coinId: String?,
|
||||
val amountTextFieldValue: TextFieldValue?,
|
||||
|
|
@ -62,7 +62,7 @@ sealed class SwapCardState {
|
|||
|
||||
data class Empty(
|
||||
val type: TransactionCardType,
|
||||
val amountEquivalent: String?,
|
||||
val amountEquivalent: TextReference?,
|
||||
val amountTextFieldValue: TextFieldValue?,
|
||||
val canSelectAnotherToken: Boolean = false,
|
||||
) : SwapCardState()
|
||||
|
|
@ -76,6 +76,7 @@ data class SwapButton(
|
|||
val onClick: () -> Unit,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
sealed interface TransactionCardType {
|
||||
|
||||
val accountTitleUM: AccountTitleUM?
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ import com.tangem.feature.swap.domain.models.ui.TxFee
|
|||
import java.math.BigDecimal
|
||||
|
||||
data class UiActions(
|
||||
val onSearchEntered: (String) -> Unit,
|
||||
val onTokenSelected: (String) -> Unit,
|
||||
val onAmountChanged: (String) -> Unit,
|
||||
val onAmountSelected: (Boolean) -> Unit,
|
||||
val onSwapClick: () -> Unit,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.feature.swap.models.market
|
||||
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.*
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
|
|
@ -12,6 +13,9 @@ import com.tangem.utils.Provider
|
|||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
|
@ -19,14 +23,14 @@ import kotlinx.coroutines.*
|
|||
import kotlinx.coroutines.flow.*
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class SwapMarketsListBatchFlowManager(
|
||||
internal class MarketsListBatchFlowManager @AssistedInject constructor(
|
||||
getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase,
|
||||
private val batchFlowType: GetMarketsTokenListFlowUseCase.BatchFlowType,
|
||||
private val order: TokenMarketListConfig.Order,
|
||||
private val currentAppCurrency: Provider<AppCurrency>,
|
||||
private val currentSearchText: Provider<String?>,
|
||||
private val modelScope: CoroutineScope,
|
||||
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
@Assisted private val batchFlowType: GetMarketsTokenListFlowUseCase.BatchFlowType,
|
||||
@Assisted private val order: TokenMarketListConfig.Order,
|
||||
@Assisted private val currentSearchText: Provider<String?>,
|
||||
@Assisted private val modelScope: CoroutineScope,
|
||||
) {
|
||||
private val actionsFlow =
|
||||
MutableSharedFlow<BatchAction<Int, TokenMarketListConfig, TokenMarketUpdateRequest>>(replay = 1)
|
||||
|
|
@ -43,6 +47,9 @@ internal class SwapMarketsListBatchFlowManager(
|
|||
private val resultBatches = MutableStateFlow(ResultBatches())
|
||||
private val uiBatches = resultBatches.map { it.uiBatches }
|
||||
|
||||
private val appCurrency: StateFlow<AppCurrency> = getSelectedAppCurrencyUseCase.invokeOrDefault()
|
||||
.stateIn(modelScope, SharingStarted.Eagerly, AppCurrency.Default)
|
||||
|
||||
val uiItems: StateFlow<ImmutableList<MarketsListItemUM>>
|
||||
get() = uiBatches
|
||||
.map { batches ->
|
||||
|
|
@ -114,7 +121,7 @@ internal class SwapMarketsListBatchFlowManager(
|
|||
val items = resultBatches.uiBatches
|
||||
val previousList = resultBatches.processedItems
|
||||
|
||||
val converter = SwapMarketsTokenItemConverter(appCurrency = currentAppCurrency())
|
||||
val converter = SwapMarketsTokenItemConverter(appCurrency = appCurrency.value)
|
||||
|
||||
if (newList.isEmpty()) {
|
||||
return@update ResultBatches(processedItems = emptyList())
|
||||
|
|
@ -178,7 +185,7 @@ internal class SwapMarketsListBatchFlowManager(
|
|||
actionsFlow.emit(
|
||||
BatchAction.Reload(
|
||||
requestParams = TokenMarketListConfig(
|
||||
fiatPriceCurrency = currentAppCurrency().code,
|
||||
fiatPriceCurrency = appCurrency.value.code,
|
||||
searchText = if (currentSearchText() == null) {
|
||||
null
|
||||
} else {
|
||||
|
|
@ -220,7 +227,7 @@ internal class SwapMarketsListBatchFlowManager(
|
|||
keys = batchesKeysToLoad,
|
||||
updateRequest = TokenMarketUpdateRequest.UpdateChart(
|
||||
interval = TokenMarketListConfig.Interval.H24,
|
||||
currency = currentAppCurrency().code,
|
||||
currency = appCurrency.value.code,
|
||||
),
|
||||
async = true,
|
||||
operationId = batchesKeysToLoad.toString() + "h24",
|
||||
|
|
@ -250,4 +257,14 @@ internal class SwapMarketsListBatchFlowManager(
|
|||
val uiBatches: List<Batch<Int, List<MarketsListItemUM>>> = emptyList(),
|
||||
val processedItems: List<Batch<Int, List<TokenMarket>>>? = null,
|
||||
)
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(
|
||||
batchFlowType: GetMarketsTokenListFlowUseCase.BatchFlowType,
|
||||
order: TokenMarketListConfig.Order,
|
||||
currentSearchText: Provider<String?>,
|
||||
modelScope: CoroutineScope,
|
||||
): MarketsListBatchFlowManager
|
||||
}
|
||||
}
|
||||
|
|
@ -2,9 +2,9 @@ package com.tangem.feature.swap.models.market.state
|
|||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.core.ui.R
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
|
|
@ -38,4 +38,17 @@ internal sealed class SwapMarketState {
|
|||
override val marketsTitle: TextReference = TextReference.Res(R.string.markets_common_title)
|
||||
override val shouldAssetsCount: Boolean = true
|
||||
}
|
||||
|
||||
companion object {
|
||||
val DefaultLoading
|
||||
get() = Loading(
|
||||
marketsTitle = TextReference.Res(R.string.feed_trending_now),
|
||||
shouldAssetsCount = false,
|
||||
)
|
||||
val SearchLoading
|
||||
get() = Loading(
|
||||
marketsTitle = TextReference.Res(R.string.markets_common_title),
|
||||
shouldAssetsCount = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -194,6 +194,17 @@ internal object SwapNotificationUM {
|
|||
onClick = onConfirmClick,
|
||||
),
|
||||
)
|
||||
|
||||
data object HighPriceImpact : Warning(
|
||||
title = resourceReference(R.string.swapping_high_price_impact_title),
|
||||
subtitle = resourceReference(R.string.swapping_high_price_impact_text),
|
||||
)
|
||||
|
||||
data object TradeTooHigh : Warning(
|
||||
title = resourceReference(R.string.swapping_trade_too_large_title),
|
||||
subtitle = resourceReference(R.string.swapping_trade_too_large_text),
|
||||
iconResId = R.drawable.ic_alert_circle_24,
|
||||
)
|
||||
}
|
||||
|
||||
sealed class Info(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.feature.swap.ui
|
||||
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
|
|
@ -12,10 +13,8 @@ import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles
|
|||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.format.bigdecimal.anyDecimals
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
|
|
@ -23,7 +22,6 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
|||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.isHotWallet
|
||||
import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork
|
||||
import com.tangem.feature.swap.converters.TokensDataConverter
|
||||
import com.tangem.feature.swap.domain.models.ExpressDataError
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType
|
||||
|
|
@ -38,6 +36,7 @@ import com.tangem.feature.swap.models.states.*
|
|||
import com.tangem.feature.swap.presentation.R
|
||||
import com.tangem.feature.swap.utils.formatToUIRepresentation
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.StringsSigns.DASH_SIGN
|
||||
import com.tangem.utils.StringsSigns.TILDE_SIGN
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -128,7 +127,7 @@ internal class StateBuilder(
|
|||
onSuccess = actions.onSuccess,
|
||||
providerState = ProviderState.Empty(),
|
||||
shouldShowMaxAmount = false,
|
||||
priceImpact = PriceImpact.Empty(),
|
||||
priceImpact = PriceImpact.Empty,
|
||||
isInsufficientFunds = false,
|
||||
)
|
||||
}
|
||||
|
|
@ -142,7 +141,7 @@ internal class StateBuilder(
|
|||
sendCardData = SwapCardState.SwapCardData(
|
||||
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable),
|
||||
amountTextFieldValue = null,
|
||||
amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}",
|
||||
amountEquivalent = getFormattedFiatAmount(BigDecimal.ZERO),
|
||||
token = fromToken,
|
||||
tokenIconUrl = fromToken.currency.iconUrl,
|
||||
coinId = fromToken.currency.network.backendId,
|
||||
|
|
@ -155,7 +154,7 @@ internal class StateBuilder(
|
|||
),
|
||||
receiveCardData = SwapCardState.Empty(
|
||||
type = TransactionCardType.ReadOnly(),
|
||||
amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}",
|
||||
amountEquivalent = getFormattedFiatAmount(BigDecimal.ZERO),
|
||||
amountTextFieldValue = TextFieldValue(
|
||||
text = "0",
|
||||
),
|
||||
|
|
@ -170,7 +169,7 @@ internal class StateBuilder(
|
|||
onClick = { },
|
||||
),
|
||||
changeCardsButtonState = ChangeCardsButtonState.DISABLED,
|
||||
priceImpact = PriceImpact.Empty(),
|
||||
priceImpact = PriceImpact.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -190,7 +189,7 @@ internal class StateBuilder(
|
|||
amountTextFieldValue = TextFieldValue(
|
||||
text = "0",
|
||||
),
|
||||
amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}",
|
||||
amountEquivalent = getFormattedFiatAmount(BigDecimal.ZERO),
|
||||
token = fromToken,
|
||||
tokenIconUrl = fromToken.currency.iconUrl,
|
||||
coinId = fromToken.currency.network.backendId,
|
||||
|
|
@ -208,7 +207,7 @@ internal class StateBuilder(
|
|||
amountTextFieldValue = TextFieldValue(
|
||||
text = "0",
|
||||
),
|
||||
amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}",
|
||||
amountEquivalent = getFormattedFiatAmount(BigDecimal.ZERO),
|
||||
token = toToken,
|
||||
tokenIconUrl = toToken.currency.iconUrl,
|
||||
coinId = toToken.currency.network.backendId,
|
||||
|
|
@ -229,7 +228,7 @@ internal class StateBuilder(
|
|||
),
|
||||
changeCardsButtonState = ChangeCardsButtonState.DISABLED,
|
||||
providerState = ProviderState.Empty(),
|
||||
priceImpact = PriceImpact.Empty(),
|
||||
priceImpact = PriceImpact.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -297,7 +296,7 @@ internal class StateBuilder(
|
|||
providerState = ProviderState.Loading(),
|
||||
permissionState = uiStateHolder.permissionState,
|
||||
changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS,
|
||||
priceImpact = PriceImpact.Empty(),
|
||||
priceImpact = PriceImpact.Empty,
|
||||
shouldShowMaxAmount = shouldShowMaxAmount(fromToken, toToken),
|
||||
)
|
||||
}
|
||||
|
|
@ -363,6 +362,7 @@ internal class StateBuilder(
|
|||
)
|
||||
}
|
||||
}
|
||||
val priceImpact = quoteModel.priceImpact
|
||||
return uiStateHolder.copy(
|
||||
sendCardData = SwapCardState.SwapCardData(
|
||||
type = sendInput,
|
||||
|
|
@ -389,7 +389,26 @@ internal class StateBuilder(
|
|||
.formatToUIRepresentation()
|
||||
.appendApproximateSign(),
|
||||
),
|
||||
amountEquivalent = getFormattedFiatAmount(quoteModel.toTokenInfo.amountFiat),
|
||||
amountEquivalent = if (priceImpact.type.ordinal > PriceImpact.Type.LOW.ordinal) {
|
||||
combinedReference(
|
||||
getFormattedFiatAmount(quoteModel.toTokenInfo.amountFiat),
|
||||
stringReference(StringsSigns.WHITE_SPACE),
|
||||
styledStringReference(
|
||||
value = "(${StringsSigns.MINUS}${priceImpact.value.format { percent() }})",
|
||||
spanStyleReference = {
|
||||
SpanStyle(
|
||||
color = when (priceImpact.type) {
|
||||
PriceImpact.Type.HIGH -> TangemTheme.colors.text.warning
|
||||
PriceImpact.Type.MEDIUM -> TangemTheme.colors.text.attention
|
||||
else -> TangemTheme.colors.text.tertiary
|
||||
},
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
} else {
|
||||
getFormattedFiatAmount(quoteModel.toTokenInfo.amountFiat)
|
||||
},
|
||||
token = toCurrencyStatus,
|
||||
tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl,
|
||||
coinId = toCurrencyStatus.currency.network.backendId,
|
||||
|
|
@ -413,13 +432,13 @@ internal class StateBuilder(
|
|||
fee = feeState,
|
||||
swapButton = SwapButton(
|
||||
walletInteractionIcon = walletInterationIcon(userWalletProvider()),
|
||||
isEnabled = getSwapButtonEnabled(notifications),
|
||||
isEnabled = getSwapButtonEnabled(notifications, priceImpact),
|
||||
isHoldToConfirm = isHoldToConfirmEnabled,
|
||||
onClick = actions.onSwapClick,
|
||||
),
|
||||
changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible),
|
||||
providerState = swapProvider.convertToContentClickableProviderState(
|
||||
isBestRate = bestRatedProviderId == swapProvider.providerId,
|
||||
isBestRate = bestRatedProviderId == swapProvider.providerId && !priceImpact.shouldShowWarning(),
|
||||
fromTokenInfo = quoteModel.fromTokenInfo,
|
||||
toTokenInfo = quoteModel.toTokenInfo,
|
||||
isNeedBestRateBadge = isNeedBestRateBadge,
|
||||
|
|
@ -427,11 +446,7 @@ internal class StateBuilder(
|
|||
onProviderClick = actions.onProviderClick,
|
||||
needApplyFCARestrictions = needApplyFCARestrictions,
|
||||
),
|
||||
priceImpact = if (quoteModel.priceImpact.value > PRICE_IMPACT_THRESHOLD) {
|
||||
quoteModel.priceImpact
|
||||
} else {
|
||||
PriceImpact.Empty()
|
||||
},
|
||||
priceImpact = priceImpact,
|
||||
tosState = createTosState(swapProvider),
|
||||
shouldShowMaxAmount = shouldShowMaxAmount(fromToken, toCurrencyStatus.currency),
|
||||
)
|
||||
|
|
@ -465,7 +480,7 @@ internal class StateBuilder(
|
|||
quoteModel.preparedSwapConfigState.includeFeeInAmount !is IncludeFeeInAmount.Included
|
||||
}
|
||||
|
||||
private fun getSwapButtonEnabled(notifications: ImmutableList<NotificationUM>): Boolean {
|
||||
private fun getSwapButtonEnabled(notifications: ImmutableList<NotificationUM>, priceImpact: PriceImpact): Boolean {
|
||||
return notifications.none { notification ->
|
||||
notification is SwapNotificationUM.Error || notification is NotificationUM.Error ||
|
||||
notification is SwapNotificationUM.Warning.ExpressError ||
|
||||
|
|
@ -474,7 +489,7 @@ internal class StateBuilder(
|
|||
notification is SwapNotificationUM.Warning.SwapNotSupported ||
|
||||
notification is SwapNotificationUM.Warning.NeedReserveToCreateAccount ||
|
||||
notification is SwapNotificationUM.Info.PermissionNeeded
|
||||
}
|
||||
} && !priceImpact.shouldDisableButton()
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -513,7 +528,7 @@ internal class StateBuilder(
|
|||
amountTextFieldValue = TextFieldValue(
|
||||
text = "0",
|
||||
),
|
||||
amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}",
|
||||
amountEquivalent = getFormattedFiatAmount(BigDecimal.ZERO),
|
||||
token = toToken,
|
||||
tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl,
|
||||
coinId = toToken.currency.network.backendId,
|
||||
|
|
@ -526,7 +541,7 @@ internal class StateBuilder(
|
|||
)
|
||||
} ?: SwapCardState.Empty(
|
||||
type = type,
|
||||
amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}",
|
||||
amountEquivalent = getFormattedFiatAmount(BigDecimal.ZERO),
|
||||
amountTextFieldValue = TextFieldValue(
|
||||
text = "0",
|
||||
),
|
||||
|
|
@ -549,7 +564,7 @@ internal class StateBuilder(
|
|||
),
|
||||
changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible),
|
||||
providerState = providerState,
|
||||
priceImpact = PriceImpact.Empty(),
|
||||
priceImpact = PriceImpact.Empty,
|
||||
tosState = createTosState(swapProvider),
|
||||
)
|
||||
}
|
||||
|
|
@ -647,7 +662,7 @@ internal class StateBuilder(
|
|||
),
|
||||
changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible),
|
||||
providerState = ProviderState.Empty(),
|
||||
priceImpact = PriceImpact.Empty(),
|
||||
priceImpact = PriceImpact.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -660,21 +675,6 @@ internal class StateBuilder(
|
|||
)
|
||||
}
|
||||
|
||||
fun addTokensToStateV2(
|
||||
uiState: SwapStateHolder,
|
||||
tokensDataState: CurrenciesGroup,
|
||||
isAccountsMode: Boolean,
|
||||
): SwapStateHolder {
|
||||
return TokensDataConverter(
|
||||
onSearchEntered = actions.onSearchEntered,
|
||||
onTokenSelected = actions.onTokenSelected,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
tokensDataState = tokensDataState,
|
||||
isBalanceHidden = isBalanceHiddenProvider(),
|
||||
isAccountsMode = isAccountsMode,
|
||||
).transform(uiState)
|
||||
}
|
||||
|
||||
fun createSilentLoadState(uiState: SwapStateHolder): SwapStateHolder {
|
||||
return uiState.copy(
|
||||
changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS,
|
||||
|
|
@ -759,28 +759,10 @@ internal class StateBuilder(
|
|||
val patchedReceiveCardData = uiState.receiveCardData.copy(
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
)
|
||||
val selectTokenState = uiState.selectTokenState?.copy(
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
availableTokens = uiState.selectTokenState.availableTokens.map { tokenState ->
|
||||
when (tokenState) {
|
||||
is TokenToSelectState.TokenToSelect -> {
|
||||
tokenState.copy(
|
||||
addedTokenBalanceData = tokenState.addedTokenBalanceData?.copy(
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
),
|
||||
)
|
||||
}
|
||||
is TokenToSelectState.Title -> {
|
||||
tokenState
|
||||
}
|
||||
}
|
||||
}.toImmutableList(),
|
||||
)
|
||||
|
||||
return uiState.copy(
|
||||
sendCardData = patchedSendCardData,
|
||||
receiveCardData = patchedReceiveCardData,
|
||||
selectTokenState = selectTokenState,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -888,8 +870,8 @@ internal class StateBuilder(
|
|||
toTitle = getToCardAccountTitle(toAccount = dataState.toAccount),
|
||||
fromTokenAmount = stringReference(swapTransactionState.fromAmount.orEmpty()),
|
||||
toTokenAmount = stringReference(swapTransactionState.toAmount.orEmpty()),
|
||||
fromTokenFiatAmount = stringReference(fromFiatAmount),
|
||||
toTokenFiatAmount = stringReference(toFiatAmount),
|
||||
fromTokenFiatAmount = fromFiatAmount,
|
||||
toTokenFiatAmount = toFiatAmount,
|
||||
fromTokenIconState = iconStateConverter.convert(fromCryptoCurrency),
|
||||
toTokenIconState = iconStateConverter.convert(toCryptoCurrency),
|
||||
onExploreButtonClick = onExploreClick,
|
||||
|
|
@ -928,8 +910,8 @@ internal class StateBuilder(
|
|||
toTitle = getToCardAccountTitle(toAccount = dataState.toAccount),
|
||||
fromTokenAmount = stringReference(swapTransactionState.fromAmount.orEmpty()),
|
||||
toTokenAmount = stringReference(swapTransactionState.toAmount.orEmpty()),
|
||||
fromTokenFiatAmount = stringReference(fromFiatAmount),
|
||||
toTokenFiatAmount = stringReference(toFiatAmount),
|
||||
fromTokenFiatAmount = fromFiatAmount,
|
||||
toTokenFiatAmount = toFiatAmount,
|
||||
fromTokenIconState = iconStateConverter.convert(fromCryptoCurrency),
|
||||
toTokenIconState = iconStateConverter.convert(toCryptoCurrency),
|
||||
onExploreButtonClick = onExploreClick,
|
||||
|
|
@ -1303,15 +1285,17 @@ internal class StateBuilder(
|
|||
return amount.format { crypto(symbol, currency.decimals) }
|
||||
}
|
||||
|
||||
private fun getFormattedFiatAmount(amount: BigDecimal?): String {
|
||||
private fun getFormattedFiatAmount(amount: BigDecimal?): TextReference {
|
||||
val appCurrency = appCurrencyProvider()
|
||||
|
||||
return amount.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
}
|
||||
return stringReference(
|
||||
amount.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun SwapAmount.getFormattedCryptoAmount(token: CryptoCurrency): String {
|
||||
|
|
@ -1375,7 +1359,6 @@ internal class StateBuilder(
|
|||
const val ADDRESS_MIN_LENGTH = 11
|
||||
const val ADDRESS_FIRST_PART_LENGTH = 7
|
||||
const val ADDRESS_SECOND_PART_LENGTH = 4
|
||||
private const val PRICE_IMPACT_THRESHOLD = 0.1
|
||||
private const val MAX_DECIMALS_TO_SHOW = 8
|
||||
private const val IF_ZERO_DECIMALS_TO_SHOW = 2
|
||||
private const val FEE_READ_MORE_URL_FIRST_PART = "https://tangem.com/"
|
||||
|
|
|
|||
|
|
@ -346,6 +346,7 @@ private fun SwapNotifications(notifications: List<NotificationUM>) {
|
|||
Notification(
|
||||
config = notification.config,
|
||||
iconTint = when (notification) {
|
||||
is SwapNotificationUM.Warning.TradeTooHigh -> TangemTheme.colors.icon.warning
|
||||
is SwapNotificationUM.Error.UnableToCoverFeeWarning,
|
||||
is NotificationUM.Error.TokenExceedsBalance,
|
||||
is NotificationUM.Error.ExceedsBalance,
|
||||
|
|
@ -412,7 +413,7 @@ private val sendCard = SwapCardState.SwapCardData(
|
|||
accountTitleUM = null,
|
||||
),
|
||||
amountTextFieldValue = TextFieldValue(),
|
||||
amountEquivalent = "1 000 000",
|
||||
amountEquivalent = stringReference("1 000 000"),
|
||||
tokenIconUrl = "",
|
||||
tokenCurrency = "DAI",
|
||||
isNotNativeToken = true,
|
||||
|
|
@ -427,7 +428,7 @@ private val sendCard = SwapCardState.SwapCardData(
|
|||
private val receiveCard = SwapCardState.SwapCardData(
|
||||
type = TransactionCardType.ReadOnly(),
|
||||
amountTextFieldValue = TextFieldValue(),
|
||||
amountEquivalent = "1 000 000",
|
||||
amountEquivalent = stringReference("1 000 000"),
|
||||
tokenIconUrl = "",
|
||||
tokenCurrency = "DAI",
|
||||
isNotNativeToken = true,
|
||||
|
|
@ -466,7 +467,7 @@ private val state = SwapStateHolder(
|
|||
permissionState = GiveTxPermissionState.InProgress,
|
||||
blockchainId = "POLYGON",
|
||||
providerState = ProviderState.Loading(),
|
||||
priceImpact = PriceImpact.Empty(),
|
||||
priceImpact = PriceImpact.Empty,
|
||||
shouldShowMaxAmount = true,
|
||||
isInsufficientFunds = false,
|
||||
onSuccess = {},
|
||||
|
|
|
|||
|
|
@ -1,22 +1,16 @@
|
|||
package com.tangem.feature.swap.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
|
|
@ -26,32 +20,27 @@ import androidx.compose.ui.semantics.semantics
|
|||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.SpacerH12
|
||||
import com.tangem.core.ui.components.SpacerH32
|
||||
import com.tangem.core.ui.components.SpacerW2
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.appbar.ExpandableSearchView
|
||||
import com.tangem.core.ui.components.list.InfiniteListHandler
|
||||
import com.tangem.core.ui.components.atoms.text.EllipsisText
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.components.tokenlist.PortfolioListItem
|
||||
import com.tangem.core.ui.components.tokenlist.PortfolioTokensListItem
|
||||
import com.tangem.core.ui.components.tokenlist.TokenListItem
|
||||
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.BuyTokenScreenTestTags
|
||||
import com.tangem.core.ui.test.MainScreenTestTags
|
||||
import com.tangem.core.ui.utils.lazyListItemPosition
|
||||
import com.tangem.feature.swap.models.SwapSelectTokenStateHolder
|
||||
import com.tangem.feature.swap.models.TokenListUMData
|
||||
import com.tangem.feature.swap.models.TokenToSelectState
|
||||
import com.tangem.feature.swap.models.isEmptyState
|
||||
import com.tangem.feature.swap.models.isNotFoundState
|
||||
import com.tangem.feature.swap.models.market.state.SwapMarketState
|
||||
|
|
@ -59,7 +48,6 @@ import com.tangem.feature.swap.presentation.R
|
|||
import com.tangem.feature.swap.ui.market.swapMarketsListItems
|
||||
import com.tangem.feature.swap.ui.preview.SwapSelectTokenPreviewProvider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
private const val LOAD_MORE_BUFFER = 25
|
||||
|
||||
|
|
@ -162,12 +150,6 @@ private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier =
|
|||
tokensListData = state.tokensListData,
|
||||
isBalanceHidden = state.isBalanceHidden,
|
||||
)
|
||||
|
||||
tokensToSelectItems(state.availableTokens, state.onTokenSelected)
|
||||
|
||||
item { SpacerH12() }
|
||||
|
||||
tokensToSelectItems(state.unavailableTokens, state.onTokenSelected)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -197,18 +179,8 @@ private fun ListOfTokensWithMarkets(
|
|||
isBalanceHidden = state.isBalanceHidden,
|
||||
)
|
||||
|
||||
tokensToSelectItems(state.availableTokens, state.onTokenSelected)
|
||||
if (state.unavailableTokens.isNotEmpty()) {
|
||||
item { SpacerH12() }
|
||||
tokensToSelectItems(state.unavailableTokens, state.onTokenSelected)
|
||||
}
|
||||
|
||||
val hasPortfolioContent = state.tokensListData !is TokenListUMData.EmptyList ||
|
||||
state.availableTokens.isNotEmpty()
|
||||
if (hasPortfolioContent) {
|
||||
item { SpacerH32() }
|
||||
} else {
|
||||
item { SpacerH12() }
|
||||
item("spacer_before_markets") {
|
||||
SpacerH(32.dp)
|
||||
}
|
||||
|
||||
swapMarketsListItems(marketsState)
|
||||
|
|
@ -277,10 +249,12 @@ private fun LazyListScope.assetsTitle(count: Int, showCount: Boolean) {
|
|||
private fun LazyListScope.tokensListItems(tokensListData: TokenListUMData, isBalanceHidden: Boolean) {
|
||||
when (tokensListData) {
|
||||
is TokenListUMData.AccountList -> {
|
||||
tokensListData.tokensList.forEach { item ->
|
||||
tokensListData.tokensList.forEachIndexed { index, item ->
|
||||
portfolioTokensList(
|
||||
portfolio = item,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
portfolioIndex = index,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -316,242 +290,168 @@ private fun LazyListScope.tokensList(items: ImmutableList<TokensListItemUM>, isB
|
|||
)
|
||||
}
|
||||
|
||||
internal fun LazyListScope.portfolioTokensList(portfolio: TokensListItemUM.Portfolio, isBalanceHidden: Boolean) {
|
||||
internal fun LazyListScope.portfolioTokensList(
|
||||
portfolio: TokensListItemUM.Portfolio,
|
||||
modifier: Modifier,
|
||||
portfolioIndex: Int,
|
||||
isBalanceHidden: Boolean,
|
||||
) {
|
||||
val tokens = portfolio.tokens
|
||||
val isExpanded = portfolio.isExpanded
|
||||
val lastIndex = tokens.lastIndex.inc()
|
||||
|
||||
portfolioItem(
|
||||
portfolio = portfolio,
|
||||
modifier = Modifier,
|
||||
modifier = modifier,
|
||||
portfolioIndex = portfolioIndex,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
)
|
||||
if (!isExpanded) return
|
||||
itemsIndexed(
|
||||
items = tokens,
|
||||
key = { _, item -> item.id },
|
||||
key = { _, item -> item.id.toString() + "-portfolio-${portfolio.id}" },
|
||||
contentType = { _, item -> item::class.java },
|
||||
itemContent = { tokenIndex, token ->
|
||||
val indexWithHeader = tokenIndex.inc()
|
||||
PortfolioTokensListItem(
|
||||
state = token,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
SlideInItemVisibility(
|
||||
currentIndex = tokenIndex,
|
||||
lastIndex = lastIndex,
|
||||
modifier = modifier
|
||||
.testModifier(indexWithHeader)
|
||||
.animateItem(fadeInSpec = null, placementSpec = null, fadeOutSpec = null)
|
||||
.roundedShapeItemDecoration(
|
||||
radius = TangemTheme.dimens.radius14,
|
||||
currentIndex = indexWithHeader,
|
||||
lastIndex = tokens.lastIndex.inc(),
|
||||
lastIndex = lastIndex,
|
||||
backgroundColor = TangemTheme.colors.background.primary,
|
||||
)
|
||||
.conditional(tokenIndex == tokens.lastIndex) {
|
||||
Modifier.padding(bottom = 8.dp)
|
||||
},
|
||||
)
|
||||
),
|
||||
visible = isExpanded,
|
||||
) {
|
||||
val innerModifier = if (indexWithHeader == lastIndex) Modifier.padding(bottom = 8.dp) else Modifier
|
||||
PortfolioTokensListItem(
|
||||
state = token,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
modifier = innerModifier,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun LazyListScope.portfolioItem(
|
||||
portfolio: TokensListItemUM.Portfolio,
|
||||
modifier: Modifier,
|
||||
portfolioIndex: Int,
|
||||
isBalanceHidden: Boolean,
|
||||
) {
|
||||
val tokens = portfolio.tokens
|
||||
val isExpanded = portfolio.isExpanded
|
||||
val lastIndex = when {
|
||||
isExpanded && tokens.isEmpty() -> 1
|
||||
isExpanded -> tokens.lastIndex.inc()
|
||||
else -> 0
|
||||
}
|
||||
|
||||
item(
|
||||
key = "account-${portfolio.id}",
|
||||
contentType = "account",
|
||||
contentType = "account-content",
|
||||
) {
|
||||
// Snap immediately on expand; on collapse, hold until all child items finish
|
||||
// their shrink animation, then snap to fully-rounded shape.
|
||||
val effectiveLastIndex by animateIntAsState(
|
||||
targetValue = lastIndex,
|
||||
animationSpec = if (lastIndex != 0) {
|
||||
snap()
|
||||
} else {
|
||||
snap(delayMillis = minOf(50 * tokens.lastIndex, 250) + 150)
|
||||
},
|
||||
label = "lastIndex",
|
||||
)
|
||||
|
||||
PortfolioListItem(
|
||||
state = portfolio,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
modifier = modifier
|
||||
.testModifier(portfolioIndex)
|
||||
.roundedShapeItemDecoration(
|
||||
currentIndex = 0,
|
||||
lastIndex = portfolio.tokens.lastIndex.inc(),
|
||||
radius = TangemTheme.dimens.radius14,
|
||||
lastIndex = effectiveLastIndex,
|
||||
backgroundColor = TangemTheme.colors.background.primary,
|
||||
)
|
||||
.then(modifier),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.tokensToSelectItems(
|
||||
items: ImmutableList<TokenToSelectState>,
|
||||
onTokenClick: (String) -> Unit,
|
||||
) {
|
||||
itemsIndexed(items = items) { index, item ->
|
||||
when (item) {
|
||||
is TokenToSelectState.Title -> {
|
||||
TitleHeader(
|
||||
item = item,
|
||||
modifier = Modifier.roundedShapeItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = items.lastIndex,
|
||||
),
|
||||
)
|
||||
}
|
||||
is TokenToSelectState.TokenToSelect -> {
|
||||
TokenItem(
|
||||
token = item,
|
||||
modifier = Modifier
|
||||
.roundedShapeItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = items.lastIndex,
|
||||
)
|
||||
.background(TangemTheme.colors.background.action),
|
||||
onTokenClick = {
|
||||
onTokenClick(item.id)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TitleHeader(item: TokenToSelectState.Title, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(TangemTheme.colors.background.action),
|
||||
) {
|
||||
Text(
|
||||
text = item.title.resolveReference().uppercase(),
|
||||
style = TangemTheme.typography.overline,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing16,
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun Modifier.testModifier(index: Int): Modifier = this
|
||||
.testTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
||||
.semantics { lazyListItemPosition = index }
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
private fun TokenItem(
|
||||
token: TokenToSelectState.TokenToSelect,
|
||||
onTokenClick: () -> Unit,
|
||||
internal fun SlideInItemVisibility(
|
||||
visible: Boolean,
|
||||
currentIndex: Int,
|
||||
lastIndex: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(TangemTheme.dimens.size72)
|
||||
.clickable(
|
||||
enabled = token.isAvailable,
|
||||
onClick = onTokenClick,
|
||||
)
|
||||
.padding(
|
||||
vertical = TangemTheme.dimens.spacing14,
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
val maxDelay = 250
|
||||
val delayEnter = minOf(50 * currentIndex, maxDelay)
|
||||
val delayExit = minOf(50 * (lastIndex - currentIndex - 1), maxDelay)
|
||||
|
||||
AnimatedVisibility(
|
||||
modifier = modifier,
|
||||
visible = visible,
|
||||
enter = expandVertically(
|
||||
tween(200, delayMillis = delayEnter, easing = LinearOutSlowInEasing),
|
||||
expandFrom = Alignment.Top,
|
||||
) + fadeIn(tween(200, delayMillis = delayEnter, easing = LinearOutSlowInEasing)),
|
||||
exit = shrinkVertically(
|
||||
tween(150, delayMillis = delayExit, easing = FastOutLinearInEasing),
|
||||
shrinkTowards = Alignment.Top,
|
||||
) + fadeOut(tween(150, delayMillis = delayExit, easing = FastOutLinearInEasing)),
|
||||
) {
|
||||
CurrencyIcon(
|
||||
state = token.tokenIcon,
|
||||
shouldDisplayNetwork = true,
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun SwapSelectTokenScreen_Preview(
|
||||
@PreviewParameter(SwapSelectTokenScreenPreviewProvider::class) params: SwapSelectTokenStateHolder,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
SwapSelectTokenScreen(
|
||||
state = params,
|
||||
onBack = {},
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(start = TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
EllipsisText(
|
||||
text = token.name,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = if (token.isAvailable) {
|
||||
TangemTheme.colors.text.primary1
|
||||
} else {
|
||||
TangemTheme.colors.text.tertiary
|
||||
},
|
||||
)
|
||||
SpacerW2()
|
||||
EllipsisText(
|
||||
text = token.symbol,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
|
||||
if (token.addedTokenBalanceData != null) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
Text(
|
||||
text = token.addedTokenBalanceData.amountEquivalent.orEmpty().orMaskWithStars(
|
||||
maskWithStars = token.addedTokenBalanceData.isBalanceHidden &&
|
||||
!token.addedTokenBalanceData.amountEquivalent.isNullOrEmpty(),
|
||||
),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Visible,
|
||||
color = if (token.isAvailable) {
|
||||
TangemTheme.colors.text.primary1
|
||||
} else {
|
||||
TangemTheme.colors.text.tertiary
|
||||
},
|
||||
)
|
||||
SpacerW2()
|
||||
Text(
|
||||
text = token.addedTokenBalanceData.amount.orEmpty().orMaskWithStars(
|
||||
maskWithStars = token.addedTokenBalanceData.isBalanceHidden &&
|
||||
!token.addedTokenBalanceData.amount.isNullOrEmpty(),
|
||||
),
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SwapSelectTokenScreenPreviewProvider : PreviewParameterProvider<SwapSelectTokenStateHolder> {
|
||||
override val values: Sequence<SwapSelectTokenStateHolder> = sequenceOf(
|
||||
// Content state with tokens and markets
|
||||
SwapSelectTokenPreviewProvider().provideSwapSelectTokenState(),
|
||||
// Empty state
|
||||
SwapSelectTokenStateHolder(
|
||||
availableTokens = persistentListOf(),
|
||||
unavailableTokens = persistentListOf(),
|
||||
tokensListData = TokenListUMData.EmptyList,
|
||||
marketsState = null,
|
||||
isAfterSearch = false,
|
||||
isBalanceHidden = false,
|
||||
onSearchEntered = {},
|
||||
onTokenSelected = {},
|
||||
),
|
||||
// Not found state
|
||||
SwapSelectTokenStateHolder(
|
||||
availableTokens = persistentListOf(),
|
||||
unavailableTokens = persistentListOf(),
|
||||
tokensListData = TokenListUMData.EmptyList,
|
||||
marketsState = null,
|
||||
isAfterSearch = true,
|
||||
isBalanceHidden = false,
|
||||
onSearchEntered = {},
|
||||
onTokenSelected = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun TokenScreenPreview(
|
||||
@PreviewParameter(SwapSelectTokenScreenPreviewProvider::class)
|
||||
state: SwapSelectTokenStateHolder,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
SwapSelectTokenScreen(
|
||||
state = state,
|
||||
onBack = {},
|
||||
override val values: Sequence<SwapSelectTokenStateHolder>
|
||||
get() = sequenceOf(
|
||||
// Content state with tokens and markets
|
||||
SwapSelectTokenPreviewProvider.defaultState,
|
||||
// Empty state
|
||||
SwapSelectTokenStateHolder(
|
||||
tokensListData = TokenListUMData.EmptyList,
|
||||
marketsState = SwapMarketState.DefaultLoading,
|
||||
isAfterSearch = false,
|
||||
isBalanceHidden = false,
|
||||
onSearchEntered = {},
|
||||
),
|
||||
// Not found state
|
||||
SwapSelectTokenStateHolder(
|
||||
tokensListData = TokenListUMData.EmptyList,
|
||||
marketsState = SwapMarketState.SearchLoading,
|
||||
isAfterSearch = true,
|
||||
isBalanceHidden = false,
|
||||
onSearchEntered = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -26,9 +26,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -41,8 +39,7 @@ import com.tangem.common.ui.account.AccountTitleUM
|
|||
import com.tangem.common.ui.account.CryptoPortfolioIconConverter
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.SwapTokenScreenTestTags
|
||||
|
|
@ -59,7 +56,7 @@ fun TransactionCard(
|
|||
balance: String,
|
||||
tokenIconUrl: String,
|
||||
tokenCurrency: String,
|
||||
amountEquivalent: String?,
|
||||
amountEquivalent: TextReference?,
|
||||
priceImpact: PriceImpact,
|
||||
textFieldValue: TextFieldValue?,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -129,7 +126,7 @@ fun TransactionCard(
|
|||
@Composable
|
||||
fun TransactionCardEmpty(
|
||||
type: TransactionCardType,
|
||||
amountEquivalent: String?,
|
||||
amountEquivalent: TextReference?,
|
||||
textFieldValue: TextFieldValue?,
|
||||
modifier: Modifier = Modifier,
|
||||
onChangeTokenClick: (() -> Unit)? = null,
|
||||
|
|
@ -157,7 +154,7 @@ fun TransactionCardEmpty(
|
|||
type = type,
|
||||
amountEquivalent = amountEquivalent,
|
||||
textFieldValue = textFieldValue,
|
||||
priceImpact = PriceImpact.Empty(),
|
||||
priceImpact = PriceImpact.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -250,7 +247,7 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie
|
|||
@Composable
|
||||
private fun Content(
|
||||
type: TransactionCardType,
|
||||
amountEquivalent: String?,
|
||||
amountEquivalent: TextReference?,
|
||||
priceImpact: PriceImpact,
|
||||
textFieldValue: TextFieldValue?,
|
||||
) {
|
||||
|
|
@ -320,27 +317,13 @@ private fun Content(
|
|||
modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size20),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (priceImpact is PriceImpact.Value) {
|
||||
AnimatedContent(targetState = amountEquivalent, label = "") { amount ->
|
||||
Text(
|
||||
text = makePriceImpactBalanceWarning(
|
||||
amountEquivalent,
|
||||
priceImpact.getIntPercentValue(),
|
||||
),
|
||||
text = amount.resolveAnnotatedReference(),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body2,
|
||||
modifier = Modifier.testTag(
|
||||
SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT_WARNING,
|
||||
),
|
||||
modifier = Modifier.testTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT),
|
||||
)
|
||||
} else {
|
||||
AnimatedContent(targetState = amountEquivalent, label = "") { amount ->
|
||||
Text(
|
||||
text = amount,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body2,
|
||||
modifier = Modifier.testTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (type.shouldShowWarning) {
|
||||
SpacerW4()
|
||||
|
|
@ -353,10 +336,10 @@ private fun Content(
|
|||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_information_24),
|
||||
contentDescription = null,
|
||||
tint = if (priceImpact is PriceImpact.Value) {
|
||||
TangemTheme.colors.text.attention
|
||||
} else {
|
||||
TangemTheme.colors.text.tertiary
|
||||
tint = when (priceImpact.type) {
|
||||
PriceImpact.Type.HIGH -> TangemTheme.colors.text.warning
|
||||
PriceImpact.Type.MEDIUM -> TangemTheme.colors.text.attention
|
||||
else -> TangemTheme.colors.text.tertiary
|
||||
},
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
|
|
@ -368,7 +351,7 @@ private fun Content(
|
|||
} else {
|
||||
AnimatedContent(targetState = amountEquivalent, label = "") { amount ->
|
||||
Text(
|
||||
text = amount,
|
||||
text = amount.resolveAnnotatedReference(),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body2,
|
||||
modifier = Modifier
|
||||
|
|
@ -530,19 +513,6 @@ fun ChangeTokenSelector() {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun makePriceImpactBalanceWarning(value: String, priceImpactPercents: Int): AnnotatedString {
|
||||
val fullValue = "$value (-$priceImpactPercents%)"
|
||||
return buildAnnotatedString {
|
||||
append(fullValue)
|
||||
addStyle(
|
||||
style = SpanStyle(color = TangemTheme.colors.text.attention),
|
||||
start = value.length,
|
||||
end = fullValue.length,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region preview
|
||||
|
||||
@Preview(widthDp = 328, heightDp = 116, showBackground = true)
|
||||
|
|
@ -602,14 +572,14 @@ private fun TransactionCardPreview() {
|
|||
inputError = TransactionCardType.InputError.Empty,
|
||||
accountTitleUM = null,
|
||||
),
|
||||
amountEquivalent = "1 000 000",
|
||||
amountEquivalent = stringReference("1 000 000"),
|
||||
tokenIconUrl = "",
|
||||
tokenCurrency = "DAI",
|
||||
networkIconRes = R.drawable.img_polygon_22,
|
||||
onChangeTokenClick = {},
|
||||
balance = "123",
|
||||
textFieldValue = TextFieldValue(),
|
||||
priceImpact = PriceImpact.Empty(),
|
||||
priceImpact = PriceImpact.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -625,14 +595,24 @@ private fun TransactionCardPreviewWithPriceImpact() {
|
|||
icon = CryptoPortfolioIconConverter.convert(CryptoPortfolioIcon.ofDefaultCustomAccount()),
|
||||
),
|
||||
),
|
||||
amountEquivalent = "1 000 000",
|
||||
amountEquivalent = combinedReference(
|
||||
stringReference("1 000 000 $"),
|
||||
styledStringReference(
|
||||
" (-15%)",
|
||||
{ SpanStyle(color = TangemTheme.colors.text.attention) },
|
||||
),
|
||||
),
|
||||
tokenIconUrl = "",
|
||||
tokenCurrency = "DAI",
|
||||
networkIconRes = R.drawable.img_polygon_22,
|
||||
onChangeTokenClick = {},
|
||||
balance = "123",
|
||||
textFieldValue = TextFieldValue("1000000.0000000000000000000000000"),
|
||||
priceImpact = PriceImpact.Value(0.15F),
|
||||
priceImpact = PriceImpact(
|
||||
value = 0.15F.toBigDecimal(),
|
||||
type = PriceImpact.Type.MEDIUM,
|
||||
amountSignificance = PriceImpact.AmountSignificance.HIGH,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -647,14 +627,14 @@ private fun TransactionCardPreviewWithoutPriceImpact() {
|
|||
icon = CryptoPortfolioIconConverter.convert(CryptoPortfolioIcon.ofDefaultCustomAccount()),
|
||||
),
|
||||
),
|
||||
amountEquivalent = "1 000 000",
|
||||
amountEquivalent = stringReference("1 000 000"),
|
||||
tokenIconUrl = "",
|
||||
tokenCurrency = "DAI",
|
||||
networkIconRes = R.drawable.img_polygon_22,
|
||||
onChangeTokenClick = {},
|
||||
balance = "123",
|
||||
textFieldValue = TextFieldValue(),
|
||||
priceImpact = PriceImpact.Empty(),
|
||||
priceImpact = PriceImpact.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,36 +2,107 @@ package com.tangem.feature.swap.ui.preview
|
|||
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.components.token.AccountItemPreviewData
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.components.tokenlist.state.PortfolioItemContentUM
|
||||
import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM
|
||||
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.feature.swap.models.SwapSelectTokenStateHolder
|
||||
import com.tangem.feature.swap.models.TokenBalanceData
|
||||
import com.tangem.feature.swap.models.TokenListUMData
|
||||
import com.tangem.feature.swap.models.TokenToSelectState
|
||||
import com.tangem.feature.swap.models.market.state.SwapMarketState
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class SwapSelectTokenPreviewProvider {
|
||||
internal object SwapSelectTokenPreviewProvider {
|
||||
|
||||
fun provideSwapSelectTokenState(): SwapSelectTokenStateHolder {
|
||||
return SwapSelectTokenStateHolder(
|
||||
availableTokens = listOf(previewTitle, previewToken, previewToken, previewToken).toImmutableList(),
|
||||
unavailableTokens = listOf(previewTitle, previewToken, previewToken, previewToken).toImmutableList(),
|
||||
tokensListData = TokenListUMData.EmptyList,
|
||||
isAfterSearch = false,
|
||||
isBalanceHidden = false,
|
||||
onSearchEntered = {},
|
||||
onTokenSelected = {},
|
||||
marketsState = createPreviewMarketsState(),
|
||||
)
|
||||
}
|
||||
private const val CHART_VALUE_1 = 0.4
|
||||
private const val CHART_VALUE_2 = 0.2
|
||||
private const val CHART_VALUE_3 = 0.1
|
||||
private const val CHART_VALUE_4 = 2.0
|
||||
private const val CHART_VALUE_5 = 5.0
|
||||
private const val CHART_VALUE_6 = 3.0
|
||||
private const val TOTAL_ITEMS = 322
|
||||
|
||||
private fun createPreviewMarketsState() = SwapMarketState.Content(
|
||||
private val PREVIEW_CHART_DATA = MarketChartRawData(
|
||||
y = persistentListOf(
|
||||
CHART_VALUE_1,
|
||||
CHART_VALUE_2,
|
||||
CHART_VALUE_1,
|
||||
CHART_VALUE_3,
|
||||
CHART_VALUE_1,
|
||||
CHART_VALUE_4,
|
||||
CHART_VALUE_5,
|
||||
CHART_VALUE_3,
|
||||
CHART_VALUE_4,
|
||||
CHART_VALUE_4,
|
||||
CHART_VALUE_6,
|
||||
),
|
||||
)
|
||||
|
||||
private val tokenItemState = TokenItemState.Content(
|
||||
id = "1",
|
||||
iconState = CurrencyIconState.Locked,
|
||||
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Bitcoin")),
|
||||
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "12 368,14 \$"),
|
||||
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "0,35853044 BTC"),
|
||||
subtitleState = TokenItemState.SubtitleState.CryptoPriceContent(
|
||||
price = "34 496,75 \$",
|
||||
priceChangePercent = "0,43 %",
|
||||
type = PriceChangeType.DOWN,
|
||||
),
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
)
|
||||
|
||||
private val textContentTokensState = persistentListOf(
|
||||
TokensListItemUM.GroupTitle(id = 111, text = stringReference("Network Bitcoin")),
|
||||
TokensListItemUM.Token(state = tokenItemState),
|
||||
TokensListItemUM.GroupTitle(id = 222, text = stringReference("Network Ethereum")),
|
||||
TokensListItemUM.Token(
|
||||
state = tokenItemState.copy(
|
||||
id = "2",
|
||||
titleState = TokenItemState.TitleState.Content(text = stringReference("Ethereum")),
|
||||
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "3 340,79 \$"),
|
||||
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "1,856660295 ETH"),
|
||||
subtitleState = TokenItemState.SubtitleState.CryptoPriceContent(
|
||||
price = "1 799,41 \$",
|
||||
priceChangePercent = "5,16 %",
|
||||
type = PriceChangeType.UP,
|
||||
),
|
||||
),
|
||||
),
|
||||
TokensListItemUM.Token(
|
||||
state = TokenItemState.Unreachable(
|
||||
id = "3",
|
||||
iconState = CurrencyIconState.Locked,
|
||||
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")),
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
),
|
||||
),
|
||||
TokensListItemUM.Token(
|
||||
state = tokenItemState.copy(
|
||||
id = "4",
|
||||
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Shiba Inu")),
|
||||
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "48,64 \$"),
|
||||
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "6 200 220,00 SHIB"),
|
||||
subtitleState = TokenItemState.SubtitleState.CryptoPriceContent(
|
||||
price = "0.01 \$",
|
||||
priceChangePercent = "1,34 %",
|
||||
type = PriceChangeType.DOWN,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private val marketState = SwapMarketState.Content(
|
||||
items = createPreviewMarketItems(),
|
||||
loadMore = { },
|
||||
onItemClick = { },
|
||||
|
|
@ -41,6 +112,37 @@ internal class SwapSelectTokenPreviewProvider {
|
|||
shouldAssetsCount = false,
|
||||
)
|
||||
|
||||
val defaultState = SwapSelectTokenStateHolder(
|
||||
tokensListData = TokenListUMData.AccountList(
|
||||
tokensList = persistentListOf(
|
||||
TokensListItemUM.Portfolio(
|
||||
content = PortfolioItemContentUM.Tokens(
|
||||
tokens = textContentTokensState.filterIsInstance<PortfolioTokensListItemUM>()
|
||||
.toPersistentList(),
|
||||
),
|
||||
isExpanded = false,
|
||||
isCollapsable = true,
|
||||
tokenItemUM = AccountItemPreviewData.accountItem
|
||||
.copy(iconState = AccountItemPreviewData.accountLetterIcon),
|
||||
),
|
||||
TokensListItemUM.Portfolio(
|
||||
content = PortfolioItemContentUM.Tokens(
|
||||
tokens = textContentTokensState.filterIsInstance<PortfolioTokensListItemUM>()
|
||||
.toPersistentList(),
|
||||
),
|
||||
isExpanded = true,
|
||||
isCollapsable = true,
|
||||
tokenItemUM = AccountItemPreviewData.accountItem,
|
||||
),
|
||||
),
|
||||
totalTokensCount = TOTAL_ITEMS,
|
||||
),
|
||||
isAfterSearch = false,
|
||||
isBalanceHidden = false,
|
||||
onSearchEntered = {},
|
||||
marketsState = marketState,
|
||||
)
|
||||
|
||||
private fun createPreviewMarketItems() = listOf(
|
||||
createMarketItem(
|
||||
id = "1",
|
||||
|
|
@ -111,51 +213,4 @@ internal class SwapSelectTokenPreviewProvider {
|
|||
stakingRate = stringReference("APY 12.34%"),
|
||||
updateTimestamp = 0,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val CHART_VALUE_1 = 0.4
|
||||
private const val CHART_VALUE_2 = 0.2
|
||||
private const val CHART_VALUE_3 = 0.1
|
||||
private const val CHART_VALUE_4 = 2.0
|
||||
private const val CHART_VALUE_5 = 5.0
|
||||
private const val CHART_VALUE_6 = 3.0
|
||||
private const val TOTAL_ITEMS = 322
|
||||
|
||||
private val PREVIEW_CHART_DATA = MarketChartRawData(
|
||||
y = persistentListOf(
|
||||
CHART_VALUE_1,
|
||||
CHART_VALUE_2,
|
||||
CHART_VALUE_1,
|
||||
CHART_VALUE_3,
|
||||
CHART_VALUE_1,
|
||||
CHART_VALUE_4,
|
||||
CHART_VALUE_5,
|
||||
CHART_VALUE_3,
|
||||
CHART_VALUE_4,
|
||||
CHART_VALUE_4,
|
||||
CHART_VALUE_6,
|
||||
),
|
||||
)
|
||||
|
||||
private val previewToken = TokenToSelectState.TokenToSelect(
|
||||
tokenIcon = CurrencyIconState.CoinIcon(
|
||||
url = "",
|
||||
fallbackResId = 0,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
),
|
||||
id = "",
|
||||
name = "Optimistic Ethereum (ETH)",
|
||||
symbol = "USDC",
|
||||
addedTokenBalanceData = TokenBalanceData(
|
||||
amount = "15 000 $",
|
||||
amountEquivalent = "15 000 USDT",
|
||||
isBalanceHidden = false,
|
||||
),
|
||||
)
|
||||
|
||||
private val previewTitle = TokenToSelectState.Title(
|
||||
title = stringReference("MY TOKENS"),
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue