Updated on 2026-08-14
This commit is contained in:
commit
507169a2c3
492 changed files with 5734 additions and 12510 deletions
|
|
@ -1,6 +0,0 @@
|
|||
package com.tangem.features.details
|
||||
|
||||
interface DetailsFeatureToggles {
|
||||
|
||||
val isRedesignEnabled: Boolean
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
package com.tangem.features.details
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
|
||||
internal class DefaultDetailsFeatureToggles(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : DetailsFeatureToggles {
|
||||
|
||||
override val isRedesignEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled("DETAILS_REDESIGN_ENABLED")
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
package com.tangem.features.details.di
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import com.tangem.features.details.DefaultDetailsFeatureToggles
|
||||
import com.tangem.features.details.DetailsFeatureToggles
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object FeatureModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): DetailsFeatureToggles {
|
||||
return DefaultDetailsFeatureToggles(featureTogglesManager)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,9 @@ import com.tangem.core.decompose.model.Model
|
|||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.domain.feedback.GetCardInfoUseCase
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.redux.LegacyAction
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase
|
||||
|
|
@ -42,6 +45,8 @@ internal class DetailsModel @Inject constructor(
|
|||
paramsContainer: ParamsContainer,
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val appStateHolder: ReduxStateHolder,
|
||||
private val getCardInfoUseCase: GetCardInfoUseCase,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -94,7 +99,9 @@ internal class DetailsModel @Inject constructor(
|
|||
val scanResponse = getSelectedWalletSyncUseCase().getOrNull()?.scanResponse
|
||||
?: error("Selected wallet is null")
|
||||
|
||||
appStateHolder.dispatch(LegacyAction.SendEmailSupport(scanResponse))
|
||||
val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return@launch
|
||||
|
||||
sendFeedbackEmailUseCase(type = FeedbackEmailType.DirectUserRequest(cardInfo = cardInfo))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
package com.tangem.features.managetokens
|
||||
|
||||
interface ManageTokensToggles {
|
||||
val isFeatureEnabled: Boolean
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.features.managetokens.component
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
interface OnboardingManageTokensComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(val userWalletId: UserWalletId)
|
||||
|
||||
interface Factory : ComponentFactory<Params, OnboardingManageTokensComponent>
|
||||
}
|
||||
|
|
@ -23,8 +23,9 @@ dependencies {
|
|||
implementation(projects.core.analytics)
|
||||
|
||||
/* Project - Domain */
|
||||
implementation(projects.domain.manageTokens)
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.manageTokens)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
|
@ -49,4 +50,5 @@ dependencies {
|
|||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.decompose.ext.compose)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.reKotlin) // need for legacy onboarding
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
package com.tangem.features.managetokens
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
|
||||
internal class DefaultManageTokensToggles(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : ManageTokensToggles {
|
||||
|
||||
override val isFeatureEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled("NEW_MANAGE_TOKENS")
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.features.managetokens.component.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.features.managetokens.component.OnboardingManageTokensComponent
|
||||
import com.tangem.features.managetokens.model.OnboardingManageTokensModel
|
||||
import com.tangem.features.managetokens.ui.OnboardingManageTokensContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultOnboardingManageTokensComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted params: OnboardingManageTokensComponent.Params,
|
||||
) : OnboardingManageTokensComponent, AppComponentContext by context {
|
||||
|
||||
private val model: OnboardingManageTokensModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
|
||||
OnboardingManageTokensContent(modifier = modifier, state = state)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : OnboardingManageTokensComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: OnboardingManageTokensComponent.Params,
|
||||
): DefaultOnboardingManageTokensComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -55,8 +55,8 @@ internal class PreviewCustomTokenSelectorComponent(
|
|||
network = Network(
|
||||
id = n.id,
|
||||
backendId = n.id.value,
|
||||
name = "",
|
||||
currencySymbol = "",
|
||||
name = "Network $index",
|
||||
currencySymbol = "N$index",
|
||||
derivationPath = Network.DerivationPath.Card(""),
|
||||
isTestnet = false,
|
||||
standardType = Network.StandardType.ERC20,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.flow.update
|
||||
|
||||
internal class PreviewManageTokensComponent(
|
||||
private val isLoading: Boolean = false,
|
||||
private val isLoading: Boolean,
|
||||
params: ManageTokensComponent.Params,
|
||||
) : ManageTokensComponent {
|
||||
|
||||
private val changedItemsIds: MutableSet<String> = mutableSetOf()
|
||||
|
|
@ -36,14 +37,21 @@ internal class PreviewManageTokensComponent(
|
|||
value = ManageTokensUM.ManageContent(
|
||||
popBack = {},
|
||||
items = items,
|
||||
topBar = ManageTokensTopBarUM.ManageContent(
|
||||
title = resourceReference(id = R.string.main_manage_tokens),
|
||||
onBackButtonClick = {},
|
||||
endButton = TopAppBarButtonUM(
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
onIconClicked = {},
|
||||
),
|
||||
),
|
||||
topBar = if (params.userWalletId != null) {
|
||||
ManageTokensTopBarUM.ManageContent(
|
||||
title = resourceReference(id = R.string.main_manage_tokens),
|
||||
onBackButtonClick = {},
|
||||
endButton = TopAppBarButtonUM(
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
onIconClicked = {},
|
||||
),
|
||||
)
|
||||
} else {
|
||||
ManageTokensTopBarUM.ReadContent(
|
||||
title = resourceReference(R.string.common_search_tokens),
|
||||
onBackButtonClick = {},
|
||||
)
|
||||
},
|
||||
search = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
|
||||
query = "",
|
||||
|
|
@ -173,6 +181,7 @@ internal class PreviewManageTokensComponent(
|
|||
)
|
||||
is CurrencyItemUM.Custom,
|
||||
is CurrencyItemUM.Loading,
|
||||
is CurrencyItemUM.SearchNothingFound,
|
||||
-> return
|
||||
}
|
||||
|
||||
|
|
@ -210,6 +219,7 @@ internal class PreviewManageTokensComponent(
|
|||
}
|
||||
is CurrencyItemUM.Custom,
|
||||
is CurrencyItemUM.Loading,
|
||||
is CurrencyItemUM.SearchNothingFound,
|
||||
-> return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
package com.tangem.features.managetokens.component.preview
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.features.managetokens.component.OnboardingManageTokensComponent
|
||||
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
|
||||
import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM
|
||||
import com.tangem.features.managetokens.entity.managetokens.OnboardingManageTokensUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.features.managetokens.ui.OnboardingManageTokensContent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class PreviewOnboardingManageTokensComponent(
|
||||
private val isLoading: Boolean = false,
|
||||
) : OnboardingManageTokensComponent {
|
||||
|
||||
private val state = OnboardingManageTokensUM(
|
||||
items = initItems(),
|
||||
isInitialBatchLoading = false,
|
||||
isNextBatchLoading = true,
|
||||
loadMore = { false },
|
||||
onBack = {},
|
||||
search = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
|
||||
query = "",
|
||||
onQueryChange = {},
|
||||
isActive = false,
|
||||
onActiveChange = { },
|
||||
),
|
||||
actionButtonConfig = OnboardingManageTokensUM.ActionButtonConfig.Later(
|
||||
onClick = {},
|
||||
showProgress = false,
|
||||
),
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
OnboardingManageTokensContent(state = state, modifier = modifier)
|
||||
}
|
||||
|
||||
private fun initItems() = List(size = 30) { index ->
|
||||
if (isLoading) {
|
||||
CurrencyItemUM.Loading(index)
|
||||
} else {
|
||||
if (index < 2) {
|
||||
getCustomItem(index)
|
||||
} else {
|
||||
getBasicItem(index)
|
||||
}
|
||||
}
|
||||
}.toPersistentList()
|
||||
|
||||
private fun getCustomItem(index: Int) = CurrencyItemUM.Custom(
|
||||
id = ManagedCryptoCurrency.ID(index.toString()),
|
||||
name = "Custom token $index",
|
||||
symbol = "CT$index",
|
||||
icon = CurrencyIconState.CustomTokenIcon(
|
||||
tint = Color.White,
|
||||
background = Color.Black,
|
||||
topBadgeIconResId = R.drawable.img_eth_22,
|
||||
isGrayscale = false,
|
||||
showCustomBadge = true,
|
||||
),
|
||||
onRemoveClick = {},
|
||||
)
|
||||
|
||||
private fun getBasicItem(index: Int) = CurrencyItemUM.Basic(
|
||||
id = ManagedCryptoCurrency.ID(index.toString()),
|
||||
name = "Currency $index",
|
||||
symbol = "C$index",
|
||||
icon = CurrencyIconState.CoinIcon(
|
||||
url = null,
|
||||
fallbackResId = R.drawable.img_btc_22,
|
||||
isGrayscale = false,
|
||||
showCustomBadge = false,
|
||||
),
|
||||
networks = if (index == 2) {
|
||||
CurrencyItemUM.Basic.NetworksUM.Expanded(getCurrencyNetworks())
|
||||
} else {
|
||||
CurrencyItemUM.Basic.NetworksUM.Collapsed
|
||||
},
|
||||
onExpandClick = {},
|
||||
)
|
||||
|
||||
private fun getCurrencyNetworks() = List(size = 3) { networkIndex ->
|
||||
CurrencyNetworkUM(
|
||||
network = Network(
|
||||
id = Network.ID(networkIndex.toString()),
|
||||
backendId = networkIndex.toString(),
|
||||
name = "Network $networkIndex",
|
||||
currencySymbol = "N$networkIndex",
|
||||
derivationPath = Network.DerivationPath.Card(""),
|
||||
isTestnet = false,
|
||||
standardType = Network.StandardType.ERC20,
|
||||
hasFiatFeeRate = false,
|
||||
canHandleTokens = false,
|
||||
),
|
||||
name = "NETWORK$networkIndex",
|
||||
type = "N$networkIndex",
|
||||
iconResId = R.drawable.ic_eth_16,
|
||||
isMainNetwork = networkIndex == 0,
|
||||
isSelected = false,
|
||||
onSelectedStateChange = {},
|
||||
onLongClick = {},
|
||||
)
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
|
@ -16,6 +16,12 @@ internal interface ComponentModule {
|
|||
@Singleton
|
||||
fun bindManageTokensComponentFactory(factory: DefaultManageTokensComponent.Factory): ManageTokensComponent.Factory
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindOnboardingManageTokensComponentFactory(
|
||||
factory: DefaultOnboardingManageTokensComponent.Factory,
|
||||
): OnboardingManageTokensComponent.Factory
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindAddCustomTokenComponentFactory(
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.features.managetokens.di
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import com.tangem.features.managetokens.DefaultManageTokensToggles
|
||||
import com.tangem.features.managetokens.ManageTokensToggles
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object FeatureModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): ManageTokensToggles =
|
||||
DefaultManageTokensToggles(featureTogglesManager = featureTogglesManager)
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import com.tangem.core.decompose.model.Model
|
|||
import com.tangem.features.managetokens.model.CustomTokenFormModel
|
||||
import com.tangem.features.managetokens.model.CustomTokenSelectorModel
|
||||
import com.tangem.features.managetokens.model.ManageTokensModel
|
||||
import com.tangem.features.managetokens.model.OnboardingManageTokensModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -20,6 +21,11 @@ internal interface ModelModule {
|
|||
@ClassKey(ManageTokensModel::class)
|
||||
fun provideManageTokensModel(model: ManageTokensModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(OnboardingManageTokensModel::class)
|
||||
fun provideOnboardingManageTokensModel(model: OnboardingManageTokensModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(CustomTokenFormModel::class)
|
||||
|
|
|
|||
|
|
@ -48,4 +48,11 @@ internal sealed class CurrencyItemUM {
|
|||
override val symbol: String = "loading"
|
||||
override val icon: CurrencyIconState = CurrencyIconState.Loading
|
||||
}
|
||||
|
||||
data object SearchNothingFound : CurrencyItemUM() {
|
||||
override val id: ManagedCryptoCurrency.ID = ManagedCryptoCurrency.ID(value = "not found text")
|
||||
override val name: String = "content not found"
|
||||
override val symbol: String = "content not found"
|
||||
override val icon: CurrencyIconState = CurrencyIconState.Loading
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ internal sealed class ManageTokensUM {
|
|||
abstract val isInitialBatchLoading: Boolean
|
||||
abstract val isNextBatchLoading: Boolean
|
||||
abstract val items: ImmutableList<CurrencyItemUM>
|
||||
abstract val topBar: ManageTokensTopBarUM
|
||||
abstract val topBar: ManageTokensTopBarUM?
|
||||
abstract val search: SearchBarUM
|
||||
abstract val loadMore: () -> Boolean
|
||||
abstract val scrollToTop: StateEvent<Unit>
|
||||
|
|
@ -24,7 +24,7 @@ internal sealed class ManageTokensUM {
|
|||
override val isInitialBatchLoading: Boolean,
|
||||
override val isNextBatchLoading: Boolean,
|
||||
override val items: ImmutableList<CurrencyItemUM>,
|
||||
override val topBar: ManageTokensTopBarUM,
|
||||
override val topBar: ManageTokensTopBarUM?,
|
||||
override val search: SearchBarUM,
|
||||
override val loadMore: () -> Boolean,
|
||||
override val scrollToTop: StateEvent<Unit> = consumedEvent(),
|
||||
|
|
@ -35,7 +35,7 @@ internal sealed class ManageTokensUM {
|
|||
override val isInitialBatchLoading: Boolean,
|
||||
override val isNextBatchLoading: Boolean,
|
||||
override val items: ImmutableList<CurrencyItemUM>,
|
||||
override val topBar: ManageTokensTopBarUM,
|
||||
override val topBar: ManageTokensTopBarUM?,
|
||||
override val search: SearchBarUM,
|
||||
override val loadMore: () -> Boolean,
|
||||
override val scrollToTop: StateEvent<Unit> = consumedEvent(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.features.managetokens.entity.managetokens
|
||||
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class OnboardingManageTokensUM(
|
||||
val onBack: () -> Unit,
|
||||
val isInitialBatchLoading: Boolean,
|
||||
val isNextBatchLoading: Boolean,
|
||||
val items: ImmutableList<CurrencyItemUM>,
|
||||
val loadMore: () -> Boolean,
|
||||
val search: SearchBarUM,
|
||||
val scrollToTop: StateEvent<Unit> = consumedEvent(),
|
||||
val actionButtonConfig: ActionButtonConfig,
|
||||
) {
|
||||
sealed class ActionButtonConfig {
|
||||
abstract val onClick: () -> Unit
|
||||
abstract val showProgress: Boolean
|
||||
|
||||
data class Continue(
|
||||
override val onClick: () -> Unit,
|
||||
override val showProgress: Boolean = false,
|
||||
val showTangemIcon: Boolean,
|
||||
) : ActionButtonConfig()
|
||||
|
||||
data class Later(
|
||||
override val onClick: () -> Unit,
|
||||
override val showProgress: Boolean = false,
|
||||
) : ActionButtonConfig()
|
||||
|
||||
fun copySealed(showProgress: Boolean): ActionButtonConfig {
|
||||
return when (this) {
|
||||
is Continue -> copy(showProgress = showProgress)
|
||||
is Later -> copy(showProgress = showProgress)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -75,7 +75,7 @@ internal class ManageTokensModel @Inject constructor(
|
|||
observeSearchQueryChanges()
|
||||
|
||||
modelScope.launch {
|
||||
manageTokensListManager.launchPagination(params)
|
||||
manageTokensListManager.launchPagination(source = params.source, userWalletId = params.userWalletId)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -159,12 +159,7 @@ internal class ManageTokensModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
.sample(periodMillis = 1_000)
|
||||
.onEach { query ->
|
||||
manageTokensListManager.search(
|
||||
userWalletId = params.userWalletId,
|
||||
query = query,
|
||||
)
|
||||
}
|
||||
.onEach { query -> manageTokensListManager.search(userWalletId = params.userWalletId, query = query) }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
|
|
@ -235,10 +230,12 @@ internal class ManageTokensModel @Inject constructor(
|
|||
},
|
||||
)
|
||||
}
|
||||
is PaginationStatus.EndOfPagination -> state.copySealed(
|
||||
isInitialBatchLoading = false,
|
||||
isNextBatchLoading = false,
|
||||
)
|
||||
is PaginationStatus.EndOfPagination -> {
|
||||
state.copySealed(
|
||||
isInitialBatchLoading = false,
|
||||
isNextBatchLoading = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -270,10 +267,7 @@ internal class ManageTokensModel @Inject constructor(
|
|||
if (state.isInitialBatchLoading || state.isNextBatchLoading) return false
|
||||
|
||||
modelScope.launch {
|
||||
manageTokensListManager.loadMore(
|
||||
userWalletId = params.userWalletId,
|
||||
query = state.search.query,
|
||||
)
|
||||
manageTokensListManager.loadMore(userWalletId = params.userWalletId, query = state.search.query)
|
||||
}
|
||||
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -0,0 +1,297 @@
|
|||
package com.tangem.features.managetokens.model
|
||||
|
||||
import arrow.core.flatten
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ComponentScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.event.triggeredEvent
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.card.HasMissedDerivationsUseCase
|
||||
import com.tangem.domain.managetokens.SaveManagedTokensUseCase
|
||||
import com.tangem.domain.redux.OnboardingManageTokensAction
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent
|
||||
import com.tangem.features.managetokens.component.ManageTokensSource
|
||||
import com.tangem.features.managetokens.component.OnboardingManageTokensComponent
|
||||
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
|
||||
import com.tangem.features.managetokens.entity.managetokens.OnboardingManageTokensUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.features.managetokens.utils.list.ChangedCurrencies
|
||||
import com.tangem.features.managetokens.utils.list.ManageTokensListManager
|
||||
import com.tangem.pagination.BatchFetchResult
|
||||
import com.tangem.pagination.PaginationStatus
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ComponentScoped
|
||||
internal class OnboardingManageTokensModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val manageTokensListManager: ManageTokensListManager,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val reduxStateHolder: ReduxStateHolder,
|
||||
private val saveManagedTokensUseCase: SaveManagedTokensUseCase,
|
||||
private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
private val params: OnboardingManageTokensComponent.Params = paramsContainer.require()
|
||||
val state: MutableStateFlow<OnboardingManageTokensUM> = MutableStateFlow(getInitialState())
|
||||
|
||||
init {
|
||||
manageTokensListManager.uiItems
|
||||
.onEach { items -> updateItems(items) }
|
||||
.launchIn(modelScope)
|
||||
|
||||
manageTokensListManager.paginationStatus
|
||||
.onEach { status -> updatePaginationStatus(status) }
|
||||
.launchIn(modelScope)
|
||||
|
||||
manageTokensListManager.currenciesToAdd
|
||||
.onEach(::handleNewAddedCurrencies)
|
||||
.launchIn(modelScope)
|
||||
|
||||
observeSearchQueryChanges()
|
||||
|
||||
modelScope.launch {
|
||||
manageTokensListManager.launchPagination(
|
||||
source = ManageTokensSource.ONBOARDING,
|
||||
userWalletId = params.userWalletId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInitialState(): OnboardingManageTokensUM {
|
||||
analyticsEventHandler.send(ManageTokensAnalyticEvent.ScreenOpened(source = ManageTokensSource.ONBOARDING))
|
||||
|
||||
return OnboardingManageTokensUM(
|
||||
isInitialBatchLoading = true,
|
||||
isNextBatchLoading = false,
|
||||
items = getLoadingItems(),
|
||||
loadMore = ::loadMoreItems,
|
||||
onBack = {},
|
||||
search = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
|
||||
query = "",
|
||||
onQueryChange = ::searchCurrencies,
|
||||
isActive = false,
|
||||
onActiveChange = ::toggleSearchBar,
|
||||
),
|
||||
actionButtonConfig = OnboardingManageTokensUM.ActionButtonConfig.Later(
|
||||
onClick = ::onLaterClick,
|
||||
showProgress = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
private fun observeSearchQueryChanges() {
|
||||
state
|
||||
.distinctUntilChanged { old, new ->
|
||||
// It's also used to skip search activation to avoid searching an empty query
|
||||
old.search.query == new.search.query &&
|
||||
(old.search.isActive == new.search.isActive || new.search.isActive)
|
||||
}
|
||||
.transform { state ->
|
||||
val query = state.search.query
|
||||
|
||||
if (state.search.isActive) {
|
||||
emit(query)
|
||||
}
|
||||
}
|
||||
.sample(periodMillis = 1_000)
|
||||
.onEach { query -> manageTokensListManager.search(userWalletId = params.userWalletId, query = query) }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun updateItems(items: ImmutableList<CurrencyItemUM>) {
|
||||
val updatedState = state.updateAndGet { state -> state.copy(items = items) }
|
||||
|
||||
if (updatedState.items.isEmpty() && updatedState.search.isActive) {
|
||||
val event = ManageTokensAnalyticEvent.TokensIsNotFound(
|
||||
query = updatedState.search.query,
|
||||
source = ManageTokensSource.ONBOARDING,
|
||||
)
|
||||
analyticsEventHandler.send(event)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePaginationStatus(status: PaginationStatus<*>) {
|
||||
state.update { state ->
|
||||
when (status) {
|
||||
is PaginationStatus.None,
|
||||
is PaginationStatus.InitialLoading,
|
||||
-> {
|
||||
if (state.search.isActive) {
|
||||
state.copy(items = getLoadingItems())
|
||||
} else {
|
||||
state.copy(items = getLoadingItems(), isInitialBatchLoading = true)
|
||||
}
|
||||
}
|
||||
is PaginationStatus.NextBatchLoading -> state.copy(isNextBatchLoading = true)
|
||||
is PaginationStatus.InitialLoadingError -> {
|
||||
val message = SnackbarMessage(
|
||||
message = status.throwable.localizedMessage
|
||||
?.let(::stringReference)
|
||||
?: resourceReference(R.string.common_error),
|
||||
)
|
||||
messageSender.send(message)
|
||||
|
||||
state.copy(
|
||||
isInitialBatchLoading = false,
|
||||
isNextBatchLoading = false,
|
||||
)
|
||||
}
|
||||
is PaginationStatus.Paginating -> {
|
||||
(status.lastResult as? BatchFetchResult.Error)?.let { fetchError ->
|
||||
Timber.e(fetchError.throwable)
|
||||
}
|
||||
|
||||
state.copy(
|
||||
isInitialBatchLoading = false,
|
||||
isNextBatchLoading = false,
|
||||
scrollToTop = if (state.isInitialBatchLoading && state.items.isNotEmpty()) {
|
||||
triggeredEvent(
|
||||
data = Unit,
|
||||
onConsume = ::consumeScrollToTopEvent,
|
||||
)
|
||||
} else {
|
||||
state.scrollToTop
|
||||
},
|
||||
)
|
||||
}
|
||||
is PaginationStatus.EndOfPagination -> state.copy(
|
||||
isInitialBatchLoading = false,
|
||||
isNextBatchLoading = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLoadingItems(): ImmutableList<CurrencyItemUM> {
|
||||
return List(size = 10) { index ->
|
||||
CurrencyItemUM.Loading(index)
|
||||
}.toPersistentList()
|
||||
}
|
||||
|
||||
private fun consumeScrollToTopEvent() {
|
||||
state.update { state -> state.copy(scrollToTop = consumedEvent()) }
|
||||
}
|
||||
|
||||
private suspend fun handleNewAddedCurrencies(currenciesToAdd: ChangedCurrencies) {
|
||||
if (currenciesToAdd.isEmpty()) {
|
||||
state.update { state ->
|
||||
state.copy(
|
||||
actionButtonConfig = OnboardingManageTokensUM.ActionButtonConfig.Later(onClick = ::onLaterClick),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val hasMissedDerivations = hasMissedDerivationsUseCase.invoke(
|
||||
userWalletId = params.userWalletId,
|
||||
networksWithDerivationPath = currenciesToAdd.values
|
||||
.flatten()
|
||||
.toSet()
|
||||
.associate { it.backendId to null },
|
||||
)
|
||||
state.update { state ->
|
||||
state.copy(
|
||||
actionButtonConfig = OnboardingManageTokensUM.ActionButtonConfig.Continue(
|
||||
onClick = ::saveChanges,
|
||||
showTangemIcon = hasMissedDerivations,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadMoreItems(): Boolean {
|
||||
val state = state.value
|
||||
if (state.isInitialBatchLoading || state.isNextBatchLoading) return false
|
||||
|
||||
modelScope.launch {
|
||||
manageTokensListManager.loadMore(userWalletId = params.userWalletId, query = state.search.query)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun saveChanges() = resource(
|
||||
acquire = {
|
||||
state.update { state ->
|
||||
state.copy(actionButtonConfig = state.actionButtonConfig.copySealed(showProgress = true))
|
||||
}
|
||||
},
|
||||
release = {
|
||||
state.update { state ->
|
||||
state.copy(actionButtonConfig = state.actionButtonConfig.copySealed(showProgress = false))
|
||||
}
|
||||
},
|
||||
) {
|
||||
val event = ManageTokensAnalyticEvent.TokenAdded(
|
||||
tokensCount = manageTokensListManager.currenciesToAdd.value.values.sumOf { it.size },
|
||||
source = ManageTokensSource.ONBOARDING,
|
||||
)
|
||||
analyticsEventHandler.send(event)
|
||||
|
||||
saveManagedTokensUseCase(
|
||||
userWalletId = requireNotNull(params.userWalletId),
|
||||
currenciesToAdd = manageTokensListManager.currenciesToAdd.value,
|
||||
currenciesToRemove = manageTokensListManager.currenciesToRemove.value,
|
||||
).getOrElse {
|
||||
Timber.e(it, "Failed to save changes")
|
||||
return@resource
|
||||
}
|
||||
|
||||
reduxStateHolder.dispatch(OnboardingManageTokensAction.CurrenciesSaved)
|
||||
}
|
||||
|
||||
private fun onLaterClick() = resource(
|
||||
acquire = {
|
||||
state.update { state ->
|
||||
state.copy(actionButtonConfig = state.actionButtonConfig.copySealed(showProgress = true))
|
||||
}
|
||||
},
|
||||
release = {
|
||||
state.update { state ->
|
||||
state.copy(actionButtonConfig = state.actionButtonConfig.copySealed(showProgress = false))
|
||||
}
|
||||
},
|
||||
) {
|
||||
reduxStateHolder.dispatch(OnboardingManageTokensAction.CurrenciesSaved)
|
||||
}
|
||||
|
||||
private fun searchCurrencies(query: String) {
|
||||
state.update { state ->
|
||||
state.copy(
|
||||
search = state.search.copy(
|
||||
query = query,
|
||||
isActive = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleSearchBar(isActive: Boolean) {
|
||||
state.update { state ->
|
||||
state.copy(
|
||||
search = state.search.copy(
|
||||
isActive = isActive,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -20,9 +20,11 @@ import androidx.compose.ui.res.stringResource
|
|||
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 androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
import com.tangem.core.ui.components.bottomFade
|
||||
import com.tangem.core.ui.components.fields.SimpleTextField
|
||||
import com.tangem.core.ui.components.isOpened
|
||||
import com.tangem.core.ui.components.keyboardAsState
|
||||
|
|
@ -60,18 +62,24 @@ internal fun CustomTokenFormContent(model: CustomTokenFormUM, modifier: Modifier
|
|||
.fillMaxSize()
|
||||
.background(color = TangemTheme.colors.background.secondary),
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
Column(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.verticalScroll(scrollState)
|
||||
.fillMaxSize()
|
||||
.padding(bottom = TangemTheme.dimens.spacing76),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
|
||||
.bottomFade(),
|
||||
) {
|
||||
AddCustomTokenDescription()
|
||||
FormContent(model)
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(scrollState)
|
||||
.fillMaxSize()
|
||||
.padding(bottom = 128.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
AddCustomTokenDescription()
|
||||
FormContent(model)
|
||||
}
|
||||
}
|
||||
|
||||
PrimaryButton(
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@ import androidx.compose.ui.res.stringResource
|
|||
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.block.information.InformationBlock
|
||||
import com.tangem.core.ui.components.bottomFade
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.rows.ChainRow
|
||||
import com.tangem.core.ui.components.rows.model.ChainRowUM
|
||||
|
|
@ -46,11 +48,11 @@ internal fun CustomTokenSelectorContent(model: CustomTokenSelectorUM, modifier:
|
|||
val lastIndex = model.items.lastIndex
|
||||
|
||||
LazyColumn(
|
||||
modifier = modifier.background(
|
||||
color = TangemTheme.colors.background.secondary,
|
||||
),
|
||||
modifier = modifier
|
||||
.background(color = TangemTheme.colors.background.secondary)
|
||||
.bottomFade(),
|
||||
contentPadding = PaddingValues(
|
||||
bottom = TangemTheme.dimens.spacing16 + bottomBarHeight,
|
||||
bottom = 96.dp + bottomBarHeight,
|
||||
),
|
||||
) {
|
||||
item {
|
||||
|
|
|
|||
|
|
@ -13,10 +13,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -62,7 +59,9 @@ import com.tangem.core.ui.res.LocalSnackbarHostState
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.WindowInsetsZero
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||
import com.tangem.features.managetokens.component.ManageTokensSource
|
||||
import com.tangem.features.managetokens.component.preview.PreviewManageTokensComponent
|
||||
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
|
||||
import com.tangem.features.managetokens.entity.item.CurrencyItemUM.Basic.NetworksUM
|
||||
|
|
@ -131,19 +130,21 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun ManageTokensTopBar(topBar: ManageTokensTopBarUM, search: SearchBarUM, modifier: Modifier = Modifier) {
|
||||
private fun ManageTokensTopBar(topBar: ManageTokensTopBarUM?, search: SearchBarUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier.background(TangemTheme.colors.background.primary),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
TangemTopAppBar(
|
||||
title = topBar.title.resolveReference(),
|
||||
startButton = TopAppBarButtonUM.Back(topBar.onBackButtonClick),
|
||||
endButton = when (topBar) {
|
||||
is ManageTokensTopBarUM.ManageContent -> topBar.endButton
|
||||
is ManageTokensTopBarUM.ReadContent -> null
|
||||
},
|
||||
)
|
||||
if (topBar != null) {
|
||||
TangemTopAppBar(
|
||||
title = topBar.title.resolveReference(),
|
||||
startButton = TopAppBarButtonUM.Back(topBar.onBackButtonClick),
|
||||
endButton = when (topBar) {
|
||||
is ManageTokensTopBarUM.ManageContent -> topBar.endButton
|
||||
is ManageTokensTopBarUM.ReadContent -> null
|
||||
},
|
||||
)
|
||||
}
|
||||
SearchBar(
|
||||
modifier = Modifier
|
||||
.padding(bottom = TangemTheme.dimens.spacing12)
|
||||
|
|
@ -199,7 +200,7 @@ private fun Content(state: ManageTokensUM, modifier: Modifier = Modifier) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun Currencies(
|
||||
internal fun Currencies(
|
||||
listState: LazyListState,
|
||||
items: ImmutableList<CurrencyItemUM>,
|
||||
showLoadingItem: Boolean,
|
||||
|
|
@ -241,6 +242,9 @@ private fun Currencies(
|
|||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
is CurrencyItemUM.SearchNothingFound -> {
|
||||
SearchNothingFoundText(modifier = Modifier.fillParentMaxSize())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -262,6 +266,20 @@ private fun Currencies(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchNothingFoundText(modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier,
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.markets_search_token_no_result_title),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProgressIndicator(modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
|
|
@ -452,8 +470,24 @@ private fun Preview_ManageTokens(
|
|||
private class PreviewManageTokensComponentProvider : PreviewParameterProvider<ManageTokensComponent> {
|
||||
override val values: Sequence<ManageTokensComponent>
|
||||
get() = sequenceOf(
|
||||
PreviewManageTokensComponent(),
|
||||
PreviewManageTokensComponent(isLoading = true),
|
||||
PreviewManageTokensComponent(
|
||||
isLoading = true,
|
||||
params = ManageTokensComponent.Params(
|
||||
source = ManageTokensSource.ONBOARDING,
|
||||
userWalletId = UserWalletId("wallet_id"),
|
||||
),
|
||||
),
|
||||
PreviewManageTokensComponent(
|
||||
isLoading = false,
|
||||
params = ManageTokensComponent.Params(source = ManageTokensSource.ONBOARDING, userWalletId = null),
|
||||
),
|
||||
PreviewManageTokensComponent(
|
||||
isLoading = false,
|
||||
params = ManageTokensComponent.Params(
|
||||
source = ManageTokensSource.ONBOARDING,
|
||||
userWalletId = UserWalletId("wallet_id"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
// endregion Preview
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
package com.tangem.features.managetokens.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.components.BottomFade
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.PrimaryButtonIconEnd
|
||||
import com.tangem.core.ui.components.SecondaryButton
|
||||
import com.tangem.core.ui.components.fields.SearchBar
|
||||
import com.tangem.core.ui.components.snackbar.TangemSnackbarHost
|
||||
import com.tangem.core.ui.event.EventEffect
|
||||
import com.tangem.core.ui.res.LocalSnackbarHostState
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.WindowInsetsZero
|
||||
import com.tangem.features.managetokens.component.OnboardingManageTokensComponent
|
||||
import com.tangem.features.managetokens.component.preview.PreviewOnboardingManageTokensComponent
|
||||
import com.tangem.features.managetokens.entity.managetokens.OnboardingManageTokensUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun OnboardingManageTokensContent(state: OnboardingManageTokensUM, modifier: Modifier = Modifier) {
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val nestedScrollConnection = remember {
|
||||
object : NestedScrollConnection {
|
||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||
keyboardController?.hide()
|
||||
|
||||
return super.onPreScroll(available, source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier.nestedScroll(nestedScrollConnection),
|
||||
containerColor = TangemTheme.colors.background.primary,
|
||||
contentWindowInsets = WindowInsetsZero,
|
||||
topBar = {
|
||||
SearchBar(
|
||||
modifier = Modifier
|
||||
.padding(vertical = TangemTheme.dimens.spacing12, horizontal = TangemTheme.dimens.spacing16),
|
||||
state = state.search,
|
||||
)
|
||||
},
|
||||
content = { innerPadding ->
|
||||
Content(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize(),
|
||||
state = state,
|
||||
)
|
||||
},
|
||||
snackbarHost = {
|
||||
TangemSnackbarHost(
|
||||
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
|
||||
hostState = LocalSnackbarHostState.current,
|
||||
)
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.Center,
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
config = state.actionButtonConfig,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(state: OnboardingManageTokensUM, modifier: Modifier = Modifier) {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
Box(modifier = modifier) {
|
||||
Currencies(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
listState = listState,
|
||||
items = state.items,
|
||||
showLoadingItem = state.isNextBatchLoading,
|
||||
onLoadMore = state.loadMore,
|
||||
isEditable = true,
|
||||
)
|
||||
|
||||
BottomFade(modifier = Modifier.align(Alignment.BottomCenter))
|
||||
}
|
||||
|
||||
EventEffect(event = state.scrollToTop) {
|
||||
listState.animateScrollToItem(index = 0)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FloatingActionButton(config: OnboardingManageTokensUM.ActionButtonConfig, modifier: Modifier = Modifier) {
|
||||
when (config) {
|
||||
is OnboardingManageTokensUM.ActionButtonConfig.Continue -> ContinueButton(config = config, modifier = modifier)
|
||||
is OnboardingManageTokensUM.ActionButtonConfig.Later -> SecondaryButton(
|
||||
modifier = modifier,
|
||||
text = stringResource(id = R.string.common_later),
|
||||
showProgress = config.showProgress,
|
||||
onClick = config.onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContinueButton(
|
||||
config: OnboardingManageTokensUM.ActionButtonConfig.Continue,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (config.showTangemIcon) {
|
||||
PrimaryButtonIconEnd(
|
||||
modifier = modifier,
|
||||
text = stringResource(id = R.string.common_continue),
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
showProgress = config.showProgress,
|
||||
onClick = config.onClick,
|
||||
)
|
||||
} else {
|
||||
PrimaryButton(
|
||||
modifier = modifier,
|
||||
text = stringResource(id = R.string.common_continue),
|
||||
showProgress = config.showProgress,
|
||||
onClick = config.onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 800)
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 800, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_ManageTokens(
|
||||
@PreviewParameter(PreviewOnboardingManageTokensComponentProvider::class) component: OnboardingManageTokensComponent,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
component.Content(Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
|
||||
private class PreviewOnboardingManageTokensComponentProvider :
|
||||
CollectionPreviewParameterProvider<PreviewOnboardingManageTokensComponent>(
|
||||
collection = listOf(
|
||||
PreviewOnboardingManageTokensComponent(isLoading = false),
|
||||
PreviewOnboardingManageTokensComponent(isLoading = true),
|
||||
),
|
||||
)
|
||||
// endregion
|
||||
|
|
@ -166,6 +166,7 @@ internal class CustomCurrencyValidator @Inject constructor(
|
|||
validatedForm: AddCustomTokenForm.Validated.All?,
|
||||
) {
|
||||
val currency = createCustomCurrencyUseCase(
|
||||
userWalletId = userWalletId,
|
||||
networkId = networkId,
|
||||
derivationPath = derivationPath,
|
||||
formValues = validatedForm,
|
||||
|
|
|
|||
|
|
@ -16,10 +16,10 @@ import com.tangem.domain.managetokens.model.*
|
|||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent
|
||||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||
import com.tangem.features.managetokens.component.ManageTokensSource
|
||||
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.pagination.Batch
|
||||
import com.tangem.pagination.BatchAction
|
||||
import com.tangem.pagination.BatchListState
|
||||
import com.tangem.pagination.PaginationStatus
|
||||
|
|
@ -80,9 +80,9 @@ internal class ManageTokensListManager @Inject constructor(
|
|||
.distinctUntilChanged()
|
||||
val uiItems: Flow<ImmutableList<CurrencyItemUM>> = uiManager.items
|
||||
|
||||
suspend fun launchPagination(params: ManageTokensComponent.Params) = coroutineScope {
|
||||
suspend fun launchPagination(source: ManageTokensSource, userWalletId: UserWalletId?) = coroutineScope {
|
||||
scope = this
|
||||
source = params.source
|
||||
this@ManageTokensListManager.source = source
|
||||
|
||||
val batchFlow = getManagedTokensUseCase(
|
||||
context = ManageTokensListBatchingContext(
|
||||
|
|
@ -92,13 +92,13 @@ internal class ManageTokensListManager @Inject constructor(
|
|||
)
|
||||
|
||||
batchFlow.state
|
||||
.onEach { state -> updateState(state, params.userWalletId) }
|
||||
.onEach { state -> updateState(state, userWalletId) }
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(scope = this)
|
||||
.saveIn(jobHolder)
|
||||
|
||||
// Initial load
|
||||
reload(params.userWalletId)
|
||||
reload(userWalletId)
|
||||
}
|
||||
|
||||
suspend fun reload(userWalletId: UserWalletId?) {
|
||||
|
|
@ -119,7 +119,7 @@ internal class ManageTokensListManager @Inject constructor(
|
|||
}
|
||||
|
||||
suspend fun search(userWalletId: UserWalletId?, query: String) {
|
||||
state.value = ManageTokensListState()
|
||||
state.value = ManageTokensListState(searchQuery = query)
|
||||
actionsFlow.emit(
|
||||
BatchAction.Reload(
|
||||
requestParams = ManageTokensListConfig(
|
||||
|
|
@ -140,6 +140,28 @@ internal class ManageTokensListManager @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
// Search nothing found
|
||||
if (
|
||||
state.value.searchQuery.isNullOrEmpty().not() &&
|
||||
batchListState.status is PaginationStatus.EndOfPagination &&
|
||||
batchListState.data.isEmpty()
|
||||
) {
|
||||
state.update { state ->
|
||||
state.copy(
|
||||
userWalletId = userWalletId,
|
||||
currencyBatches = emptyList(),
|
||||
uiBatches = listOf(
|
||||
Batch(
|
||||
key = Int.MAX_VALUE,
|
||||
data = listOf(CurrencyItemUM.SearchNothingFound),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
state.update { state ->
|
||||
val newBatches = batchListState.data
|
||||
val currentBatches = state.currencyBatches
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ internal data class ManageTokensListState(
|
|||
val uiBatches: List<Batch<Int, List<CurrencyItemUM>>> = mutableListOf(),
|
||||
val currencyBatches: List<Batch<Int, List<ManagedCryptoCurrency>>> = mutableListOf(),
|
||||
val canEditItems: Boolean = true,
|
||||
val searchQuery: String? = null,
|
||||
) {
|
||||
|
||||
fun batchIndexByCurrencyId(currencyId: ManagedCryptoCurrency.ID): Int {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ internal fun CurrencyItemUM.toggleExpanded(
|
|||
return when (this) {
|
||||
is CurrencyItemUM.Custom,
|
||||
is CurrencyItemUM.Loading,
|
||||
is CurrencyItemUM.SearchNothingFound,
|
||||
-> this
|
||||
is CurrencyItemUM.Basic -> {
|
||||
val isExpanded = networks !is NetworksUM.Expanded
|
||||
|
|
@ -41,6 +42,7 @@ internal fun CurrencyItemUM.update(currency: ManagedCryptoCurrency): CurrencyIte
|
|||
return when (this) {
|
||||
is CurrencyItemUM.Custom,
|
||||
is CurrencyItemUM.Loading,
|
||||
is CurrencyItemUM.SearchNothingFound,
|
||||
-> this
|
||||
is CurrencyItemUM.Basic -> {
|
||||
if (currency !is ManagedCryptoCurrency.Token) {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ dependencies {
|
|||
implementation(projects.domain.balanceHiding.models)
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.demo)
|
||||
implementation(projects.domain.feedback)
|
||||
implementation(projects.domain.manageTokens)
|
||||
implementation(projects.domain.markets)
|
||||
implementation(projects.domain.staking.models)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.markets.details.impl.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.ui.charts.state.MarketChartData
|
||||
import com.tangem.common.ui.charts.state.MarketChartDataProducer
|
||||
|
|
@ -19,18 +20,18 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.markets.*
|
||||
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
|
||||
import com.tangem.features.markets.details.impl.analytics.MarketDetailsAnalyticsEvent
|
||||
import com.tangem.features.markets.details.impl.model.converters.DescriptionConverter
|
||||
import com.tangem.features.markets.details.impl.model.converters.ExchangeItemStateConverter
|
||||
import com.tangem.features.markets.details.impl.model.converters.TokenMarketInfoConverter
|
||||
import com.tangem.features.markets.details.impl.model.formatter.*
|
||||
import com.tangem.features.markets.details.impl.model.formatter.formatAsPrice
|
||||
import com.tangem.features.markets.details.impl.model.formatter.getChangePercentBetween
|
||||
import com.tangem.features.markets.details.impl.model.formatter.getPercentByInterval
|
||||
import com.tangem.features.markets.details.impl.model.state.QuotesStateUpdater
|
||||
import com.tangem.features.markets.details.impl.model.state.TokenNetworksState
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
|
||||
import com.tangem.features.markets.details.impl.ui.state.ExchangesBottomSheetContent
|
||||
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
|
|
@ -57,6 +58,8 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase,
|
||||
private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase,
|
||||
private val getTokenFullQuotesUseCase: GetTokenFullQuotesUseCase,
|
||||
private val getTokenExchangesUseCase: GetTokenExchangesUseCase,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
|
|
@ -76,9 +79,8 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
|
||||
private val infoConverter = TokenMarketInfoConverter(
|
||||
appCurrency = Provider { currentAppCurrency.value },
|
||||
onInfoClick = {
|
||||
showInfoBottomSheet(it)
|
||||
},
|
||||
onInfoClick = { showBottomSheet(it) },
|
||||
onListedOnClick = ::onListedOnClick,
|
||||
onLinkClick = { link ->
|
||||
urlOpener.openUrl(link.url)
|
||||
// === Analytics ===
|
||||
|
|
@ -106,10 +108,20 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
|
||||
private val descriptionConverter = DescriptionConverter(
|
||||
onReadModeClicked = {
|
||||
showInfoBottomSheet(it)
|
||||
showBottomSheet(it)
|
||||
// === Analytics ===
|
||||
analyticsEventHandler.send(analyticsEventBuilder.readMoreClicked())
|
||||
},
|
||||
onGeneratedAINotificationClick = {
|
||||
modelScope.launch {
|
||||
sendFeedbackEmailUseCase(
|
||||
type = FeedbackEmailType.CurrencyDescriptionError(
|
||||
currencyId = params.token.id,
|
||||
currencyName = params.token.name,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
private val chartDataProducer = MarketChartDataProducer.build(dispatcher = dispatchers.default) {
|
||||
|
|
@ -179,7 +191,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
markerSet = false,
|
||||
body = MarketsTokenDetailsUM.Body.Loading,
|
||||
triggerPriceChange = consumedEvent(),
|
||||
infoBottomSheet = TangemBottomSheetConfig(
|
||||
bottomSheetConfig = TangemBottomSheetConfig(
|
||||
isShow = false,
|
||||
onDismissRequest = {},
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
|
|
@ -269,52 +281,56 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
val xAxisFormatter = MarketsDateTimeFormatters.getChartXFormatterByInterval(state.value.selectedInterval)
|
||||
|
||||
chart.onRight {
|
||||
chartDataProducer.runTransactionSuspend {
|
||||
chartData = MarketChartData.Data(
|
||||
y = it.priceY.toImmutableList(),
|
||||
x = it.timeStamps.map { it.toBigDecimal() }.toImmutableList(),
|
||||
).sorted()
|
||||
|
||||
updateLook {
|
||||
chart
|
||||
.onRight { updateTokenChart(it) }
|
||||
.onLeft {
|
||||
state.update {
|
||||
it.copy(
|
||||
xAxisFormatter = xAxisFormatter,
|
||||
type = state.value.priceChangeType.toChartType(),
|
||||
chartState = it.chartState.copy(
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.ERROR,
|
||||
),
|
||||
body = if (it.body is MarketsTokenDetailsUM.Body.Error) {
|
||||
MarketsTokenDetailsUM.Body.Nothing
|
||||
} else {
|
||||
it.body
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
state.update {
|
||||
it.copy(
|
||||
chartState = it.chartState.copy(
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.DATA,
|
||||
),
|
||||
body = if (it.body is MarketsTokenDetailsUM.Body.Nothing) {
|
||||
MarketsTokenDetailsUM.Body.Error(onLoadRetryClick = ::onLoadRetryClicked)
|
||||
} else {
|
||||
it.body
|
||||
},
|
||||
)
|
||||
}
|
||||
}.onLeft {
|
||||
state.update {
|
||||
it.copy(
|
||||
chartState = it.chartState.copy(
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.ERROR,
|
||||
),
|
||||
body = if (it.body is MarketsTokenDetailsUM.Body.Error) {
|
||||
MarketsTokenDetailsUM.Body.Nothing
|
||||
} else {
|
||||
it.body
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}.saveIn(loadChartJobHolder)
|
||||
}
|
||||
|
||||
private suspend fun updateTokenChart(tokenChart: TokenChart) {
|
||||
val xAxisFormatter = MarketsDateTimeFormatters.getChartXFormatterByInterval(state.value.selectedInterval)
|
||||
|
||||
chartDataProducer.runTransactionSuspend {
|
||||
chartData = MarketChartData.Data(
|
||||
y = tokenChart.priceY.toImmutableList(),
|
||||
x = tokenChart.timeStamps.map { it.toBigDecimal() }.toImmutableList(),
|
||||
).sorted()
|
||||
|
||||
updateLook {
|
||||
it.copy(
|
||||
xAxisFormatter = xAxisFormatter,
|
||||
type = state.value.priceChangeType.toChartType(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
state.update {
|
||||
it.copy(
|
||||
chartState = it.chartState.copy(
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.DATA,
|
||||
),
|
||||
body = if (it.body is MarketsTokenDetailsUM.Body.Nothing) {
|
||||
MarketsTokenDetailsUM.Body.Error(onLoadRetryClick = ::onLoadRetryClicked)
|
||||
} else {
|
||||
it.body
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadInfo() {
|
||||
state.update {
|
||||
it.copy(
|
||||
|
|
@ -482,24 +498,22 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun showInfoBottomSheet(content: InfoBottomSheetContent) {
|
||||
private fun showBottomSheet(content: TangemBottomSheetConfigContent) {
|
||||
state.update { stateToUpdate ->
|
||||
stateToUpdate.copy(
|
||||
infoBottomSheet = stateToUpdate.infoBottomSheet.copy(
|
||||
bottomSheetConfig = stateToUpdate.bottomSheetConfig.copy(
|
||||
isShow = true,
|
||||
onDismissRequest = ::hideInfoBottomSheet,
|
||||
onDismissRequest = ::hideBottomSheet,
|
||||
content = content,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideInfoBottomSheet() {
|
||||
private fun hideBottomSheet() {
|
||||
state.update { stateToUpdate ->
|
||||
stateToUpdate.copy(
|
||||
infoBottomSheet = stateToUpdate.infoBottomSheet.copy(
|
||||
isShow = false,
|
||||
),
|
||||
bottomSheetConfig = stateToUpdate.bottomSheetConfig.copy(isShow = false),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -519,6 +533,39 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun onListedOnClick(exchangesCount: Int) {
|
||||
showBottomSheet(content = ExchangesBottomSheetContent.Loading(exchangesCount))
|
||||
|
||||
modelScope.launch {
|
||||
val maybeExchanges = getTokenExchangesUseCase(tokenId = params.token.id)
|
||||
|
||||
updateExchangeBSContent(maybeExchanges = maybeExchanges, exchangesCount = exchangesCount)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateExchangeBSContent(
|
||||
maybeExchanges: Either<Throwable, List<TokenMarketExchange>>,
|
||||
exchangesCount: Int,
|
||||
) {
|
||||
val content = maybeExchanges
|
||||
.fold(
|
||||
ifLeft = {
|
||||
ExchangesBottomSheetContent.Error(onRetryClick = { onListedOnClick(exchangesCount) })
|
||||
},
|
||||
ifRight = {
|
||||
ExchangesBottomSheetContent.Content(
|
||||
exchangeItems = ExchangeItemStateConverter.convertList(it).toImmutableList(),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
state.update { stateToUpdate ->
|
||||
stateToUpdate.copy(
|
||||
bottomSheetConfig = stateToUpdate.bottomSheetConfig.copy(content = content),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.loadQuotesWithTimer(timeMillis: Long) {
|
||||
launch {
|
||||
while (true) {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ import com.tangem.utils.converter.Converter
|
|||
|
||||
@Stable
|
||||
internal class DescriptionConverter(
|
||||
val onReadModeClicked: (InfoBottomSheetContent) -> Unit,
|
||||
private val onReadModeClicked: (InfoBottomSheetContent) -> Unit,
|
||||
private val onGeneratedAINotificationClick: () -> Unit,
|
||||
) : Converter<TokenMarketInfo, MarketsTokenDetailsUM.Description?> {
|
||||
|
||||
override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.Description? {
|
||||
|
|
@ -32,7 +33,9 @@ internal class DescriptionConverter(
|
|||
),
|
||||
),
|
||||
body = stringReference(value.fullDescription ?: ""),
|
||||
showGeneratedAINotification = true,
|
||||
generatedAINotificationUM = InfoBottomSheetContent.GeneratedAINotificationUM(
|
||||
onClick = onGeneratedAINotificationClick,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
package com.tangem.features.markets.details.impl.model.converters
|
||||
|
||||
import com.tangem.core.ui.components.audits.AuditLabelUM
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.markets.TokenMarketExchange
|
||||
import com.tangem.domain.markets.TokenMarketExchange.TrustScore
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Converter from [TokenMarketExchange] to [TokenItemState]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object ExchangeItemStateConverter : Converter<TokenMarketExchange, TokenItemState> {
|
||||
|
||||
override fun convert(value: TokenMarketExchange): TokenItemState {
|
||||
return TokenItemState.Content(
|
||||
id = value.id,
|
||||
iconState = CurrencyIconState.CoinIcon(
|
||||
url = value.imageUrl,
|
||||
fallbackResId = R.drawable.ic_alert_24,
|
||||
isGrayscale = false,
|
||||
showCustomBadge = false,
|
||||
),
|
||||
titleState = TokenItemState.TitleState.Content(text = value.name),
|
||||
fiatAmountState = TokenItemState.FiatAmountState.Content(
|
||||
text = BigDecimalFormatter.formatFiatPriceUncapped(
|
||||
fiatAmount = value.volumeInUsd,
|
||||
fiatCurrencyCode = "USD",
|
||||
fiatCurrencySymbol = "$",
|
||||
),
|
||||
),
|
||||
subtitleState = TokenItemState.SubtitleState.TextContent(
|
||||
value = if (value.isCentralized) "CEX" else "DEX",
|
||||
),
|
||||
subtitle2State = TokenItemState.Subtitle2State.LabelContent(
|
||||
auditLabelUM = value.trustScore.toAuditLabelUM(),
|
||||
),
|
||||
onItemClick = null,
|
||||
onItemLongClick = null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TrustScore.toAuditLabelUM(): AuditLabelUM {
|
||||
return when (this) {
|
||||
TrustScore.Risky -> AuditLabelUM(
|
||||
text = resourceReference(id = R.string.markets_token_details_exchange_trust_score_risky),
|
||||
type = AuditLabelUM.Type.Prohibition,
|
||||
)
|
||||
TrustScore.Caution -> AuditLabelUM(
|
||||
text = resourceReference(id = R.string.markets_token_details_exchange_trust_score_caution),
|
||||
type = AuditLabelUM.Type.Warning,
|
||||
)
|
||||
TrustScore.Trusted -> AuditLabelUM(
|
||||
text = resourceReference(id = R.string.markets_token_details_exchange_trust_score_trusted),
|
||||
type = AuditLabelUM.Type.Permit,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import com.tangem.domain.markets.PriceChangeInterval
|
|||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
|
||||
import com.tangem.features.markets.details.impl.ui.state.LinksUM
|
||||
import com.tangem.features.markets.details.impl.ui.state.ListedOnUM
|
||||
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
|
@ -14,6 +15,7 @@ import com.tangem.utils.converter.Converter
|
|||
internal class TokenMarketInfoConverter(
|
||||
private val appCurrency: Provider<AppCurrency>,
|
||||
private val onInfoClick: (InfoBottomSheetContent) -> Unit,
|
||||
private val onListedOnClick: (Int) -> Unit,
|
||||
onLinkClick: (LinksUM.Link) -> Unit,
|
||||
onPricePerformanceIntervalChanged: (PriceChangeInterval) -> Unit,
|
||||
onInsightsIntervalChanged: (PriceChangeInterval) -> Unit,
|
||||
|
|
@ -41,6 +43,7 @@ internal class TokenMarketInfoConverter(
|
|||
onInfoClick = onInfoClick,
|
||||
)
|
||||
|
||||
val exchangesAmount = value.exchangesAmount
|
||||
return MarketsTokenDetailsUM.InformationBlocks(
|
||||
insights = value.insights?.let { insightsConverter.convert(it) },
|
||||
securityScore = null,
|
||||
|
|
@ -51,6 +54,11 @@ internal class TokenMarketInfoConverter(
|
|||
currentPrice = value.quotes.currentPrice,
|
||||
)
|
||||
},
|
||||
listedOn = if (exchangesAmount != null && exchangesAmount > 0) {
|
||||
ListedOnUM.Content(onClick = { onListedOnClick(exchangesAmount) }, amount = exchangesAmount)
|
||||
} else {
|
||||
ListedOnUM.Empty
|
||||
},
|
||||
links = value.links?.let { linksConverter.convert(it) },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,9 +37,12 @@ import com.tangem.core.ui.extensions.stringReference
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.features.markets.details.impl.ui.components.ExchangesBottomSheet
|
||||
import com.tangem.features.markets.details.impl.ui.components.InfoBottomSheet
|
||||
import com.tangem.features.markets.details.impl.ui.components.MarketTokenDetailsChart
|
||||
import com.tangem.features.markets.details.impl.ui.components.tokenMarketDetailsBody
|
||||
import com.tangem.features.markets.details.impl.ui.state.ExchangesBottomSheetContent
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
|
||||
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -67,7 +70,10 @@ internal fun MarketsTokenDetailsContent(
|
|||
addTopBarStatusBarInsets = addTopBarStatusBarPadding,
|
||||
)
|
||||
|
||||
InfoBottomSheet(config = state.infoBottomSheet)
|
||||
when (state.bottomSheetConfig.content) {
|
||||
is InfoBottomSheetContent -> InfoBottomSheet(config = state.bottomSheetConfig)
|
||||
is ExchangesBottomSheetContent -> ExchangesBottomSheet(config = state.bottomSheetConfig)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -299,7 +305,7 @@ private fun Preview() {
|
|||
selectedInterval = PriceChangeInterval.H24,
|
||||
onSelectedIntervalChange = { },
|
||||
body = MarketsTokenDetailsUM.Body.Loading,
|
||||
infoBottomSheet = TangemBottomSheetConfig(
|
||||
bottomSheetConfig = TangemBottomSheetConfig(
|
||||
isShow = false,
|
||||
onDismissRequest = {},
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,206 @@
|
|||
package com.tangem.features.markets.details.impl.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerH12
|
||||
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.audits.AuditLabelUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.token.TokenItem
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.markets.details.impl.ui.state.ExchangesBottomSheetContent
|
||||
import com.tangem.features.markets.impl.R
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* Exchanges bottom sheet
|
||||
*
|
||||
* @param config bottom sheet config
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
internal fun ExchangesBottomSheet(config: TangemBottomSheetConfig) {
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
|
||||
TangemBottomSheet<ExchangesBottomSheetContent>(
|
||||
config = config,
|
||||
addBottomInsets = false,
|
||||
title = { Title(textResId = it.titleResId, onBackClick = config.onDismissRequest) },
|
||||
content = { content ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(state = rememberScrollState()),
|
||||
) {
|
||||
Subtitle(
|
||||
subtitleRes = content.subtitleResId,
|
||||
volumeReference = content.volumeReference,
|
||||
modifier = Modifier.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
top = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing8,
|
||||
),
|
||||
)
|
||||
|
||||
when (content) {
|
||||
is ExchangesBottomSheetContent.Content,
|
||||
is ExchangesBottomSheetContent.Loading,
|
||||
-> {
|
||||
content.exchangeItems.fastForEach { item ->
|
||||
key(item.id) {
|
||||
TokenItem(state = item, isBalanceHidden = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
is ExchangesBottomSheetContent.Error -> {
|
||||
Error(
|
||||
content = content,
|
||||
modifier = Modifier
|
||||
.align(alignment = Alignment.CenterHorizontally)
|
||||
.padding(horizontal = 16.dp)
|
||||
.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SpacerH(bottomBarHeight)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Title(@StringRes textResId: Int, onBackClick: () -> Unit) {
|
||||
TangemTopAppBar(
|
||||
title = stringResource(id = textResId),
|
||||
startButton = TopAppBarButtonUM.Back(onBackClicked = onBackClick),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Subtitle(@StringRes subtitleRes: Int, volumeReference: TextReference, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
SubtitleText(textReference = resourceReference(id = subtitleRes))
|
||||
|
||||
SubtitleText(textReference = volumeReference)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SubtitleText(textReference: TextReference) {
|
||||
Text(
|
||||
text = textReference.resolveReference(),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Error(content: ExchangesBottomSheetContent.Error, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = content.message),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
style = TangemTheme.typography.caption1,
|
||||
)
|
||||
|
||||
SpacerH12()
|
||||
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = resourceReference(id = R.string.alert_button_try_again),
|
||||
onClick = content.onRetryClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(name = "Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_ExchangesBottomSheet(
|
||||
@PreviewParameter(ExchangesBottomSheetContentProvider::class) content: ExchangesBottomSheetContent,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
ExchangesBottomSheet(
|
||||
config = TangemBottomSheetConfig(
|
||||
onDismissRequest = {},
|
||||
content = content,
|
||||
isShow = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class ExchangesBottomSheetContentProvider : CollectionPreviewParameterProvider<ExchangesBottomSheetContent>(
|
||||
listOf(
|
||||
ExchangesBottomSheetContent.Loading(exchangesCount = 13),
|
||||
ExchangesBottomSheetContent.Error(onRetryClick = {}),
|
||||
ExchangesBottomSheetContent.Content(
|
||||
exchangeItems = List(size = 13) { index ->
|
||||
TokenItemState.Content(
|
||||
id = index.toString(),
|
||||
iconState = CurrencyIconState.CoinIcon(
|
||||
url = null,
|
||||
fallbackResId = R.drawable.ic_facebook_24,
|
||||
isGrayscale = false,
|
||||
showCustomBadge = false,
|
||||
),
|
||||
titleState = TokenItemState.TitleState.Content(text = "OKX"),
|
||||
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "$67.52M"),
|
||||
subtitleState = TokenItemState.SubtitleState.TextContent(value = "CEX"),
|
||||
subtitle2State = TokenItemState.Subtitle2State.LabelContent(
|
||||
auditLabelUM = AuditLabelUM(
|
||||
text = stringReference("Caution"),
|
||||
type = AuditLabelUM.Type.Warning,
|
||||
),
|
||||
),
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
)
|
||||
}
|
||||
.toImmutableList(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -27,14 +27,14 @@ internal fun InfoBottomSheet(config: TangemBottomSheetConfig) {
|
|||
config = config,
|
||||
addBottomInsets = false,
|
||||
title = { TangemBottomSheetTitle(title = it.title) },
|
||||
content = {
|
||||
content = { content ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
MarkdownText(
|
||||
markdown = it.body.resolveReference(),
|
||||
markdown = content.body.resolveReference(),
|
||||
disableLinkMovementMethod = true,
|
||||
linkifyMask = 0,
|
||||
syntaxHighlightColor = TangemTheme.colors.text.secondary,
|
||||
|
|
@ -43,8 +43,9 @@ internal fun InfoBottomSheet(config: TangemBottomSheetConfig) {
|
|||
),
|
||||
)
|
||||
|
||||
if (it.showGeneratedAINotification) {
|
||||
if (content.generatedAINotificationUM != null) {
|
||||
AdditionalInfoNotification(
|
||||
onClick = content.generatedAINotificationUM.onClick,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing12, bottom = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
|
|
@ -58,11 +59,13 @@ internal fun InfoBottomSheet(config: TangemBottomSheetConfig) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun AdditionalInfoNotification(modifier: Modifier = Modifier) {
|
||||
private fun AdditionalInfoNotification(onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Notification(
|
||||
config = NotificationConfig(
|
||||
subtitle = TextReference.Res(id = R.string.information_generated_with_ai),
|
||||
iconResId = R.drawable.ic_magic_28,
|
||||
onClick = onClick,
|
||||
showArrowIcon = false,
|
||||
),
|
||||
modifier = modifier,
|
||||
subtitleColor = TangemTheme.colors.text.primary1,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
package com.tangem.features.markets.details.impl.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.markets.details.impl.ui.state.ListedOnUM
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/**
|
||||
* "Listed on" block
|
||||
*
|
||||
* @param state block state
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
internal fun ListedOnBlock(state: ListedOnUM, modifier: Modifier = Modifier) {
|
||||
Box {
|
||||
InformationBlock(
|
||||
title = {
|
||||
Text(
|
||||
text = state.title.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
modifier = modifier.clickable(enabled = state is ListedOnUM.Content) {
|
||||
(state as? ListedOnUM.Content)?.onClick?.invoke()
|
||||
},
|
||||
) {
|
||||
Description(
|
||||
state = state,
|
||||
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
|
||||
)
|
||||
}
|
||||
|
||||
if (state is ListedOnUM.Content) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_chevron_right_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.padding(end = TangemTheme.dimens.spacing12),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun ListedOnBlockPlaceholder(modifier: Modifier = Modifier) {
|
||||
InformationBlock(
|
||||
title = {
|
||||
TextShimmer(
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
modifier = Modifier.fillMaxWidth(fraction = 0.5f),
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
) {
|
||||
TextShimmer(
|
||||
style = TangemTheme.typography.body2,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(fraction = 0.3f)
|
||||
.padding(bottom = TangemTheme.dimens.spacing12),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Description(state: ListedOnUM, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text = state.description.resolveReference(),
|
||||
modifier = modifier,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(widthDp = 328, heightDp = 68)
|
||||
@Preview(name = "Dark Theme", widthDp = 328, heightDp = 68, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_ListedOnBlock(@PreviewParameter(ListenOnUMProvider::class) state: ListedOnUM?) {
|
||||
TangemThemePreview {
|
||||
if (state == null) {
|
||||
ListedOnBlockPlaceholder()
|
||||
} else {
|
||||
ListedOnBlock(state = state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_ListedOnBlock_StateChanging() {
|
||||
var state by remember { mutableStateOf<ListedOnUM?>(value = null) }
|
||||
|
||||
Preview_ListedOnBlock(state = state)
|
||||
|
||||
LaunchedEffect(key1 = null) {
|
||||
delay(timeMillis = 3000)
|
||||
|
||||
state = ListedOnUM.Empty
|
||||
}
|
||||
}
|
||||
|
||||
private class ListenOnUMProvider : CollectionPreviewParameterProvider<ListedOnUM?>(
|
||||
collection = listOf(
|
||||
ListedOnUM.Empty,
|
||||
ListedOnUM.Content(onClick = {}, amount = 5),
|
||||
null,
|
||||
),
|
||||
)
|
||||
|
|
@ -115,6 +115,10 @@ internal fun LazyListScope.infoBlocksList(state: MarketsTokenDetailsUM.Informati
|
|||
}
|
||||
}
|
||||
|
||||
item(key = "listedOn") {
|
||||
ListedOnBlock(state = state.listedOn, modifier = Modifier.blockPaddings())
|
||||
}
|
||||
|
||||
if (state.links != null) {
|
||||
item("links") {
|
||||
LinksBlock(
|
||||
|
|
@ -143,6 +147,10 @@ private fun LazyListScope.loadingInfoBlocks() {
|
|||
PricePerformanceBlockPlaceholder(modifier = Modifier.blockPaddings())
|
||||
}
|
||||
|
||||
item(key = "listedOn-loading") {
|
||||
ListedOnBlockPlaceholder(modifier = Modifier.blockPaddings())
|
||||
}
|
||||
|
||||
item("links-loading") {
|
||||
LinksBlockPlaceholder(modifier = Modifier.blockPaddings())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
package com.tangem.features.markets.details.impl.ui.state
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.plus
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.utils.StringsSigns.DOT
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* Exchanges bottom sheet content
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal sealed interface ExchangesBottomSheetContent : TangemBottomSheetConfigContent {
|
||||
|
||||
/** Title of bottom sheet. Like, app bar. */
|
||||
@get:StringRes
|
||||
val titleResId: Int
|
||||
get() = R.string.markets_token_details_exchanges_title
|
||||
|
||||
/** Subtitle */
|
||||
@get:StringRes
|
||||
val subtitleResId: Int
|
||||
get() = R.string.markets_token_details_exchange
|
||||
|
||||
/** Volume info */
|
||||
@get:StringRes
|
||||
val volumeReference: TextReference
|
||||
get() = resourceReference(id = R.string.markets_token_details_volume) +
|
||||
stringReference(value = " $DOT ") +
|
||||
resourceReference(id = R.string.markets_selector_interval_24h_title)
|
||||
|
||||
/** Exchange items */
|
||||
val exchangeItems: ImmutableList<TokenItemState>
|
||||
|
||||
/**
|
||||
* Loading state
|
||||
*
|
||||
* @property exchangesCount count of exchanges
|
||||
*/
|
||||
data class Loading(val exchangesCount: Int) : ExchangesBottomSheetContent {
|
||||
|
||||
override val exchangeItems: ImmutableList<TokenItemState>
|
||||
get() = List(size = exchangesCount) { index -> TokenItemState.Loading(id = "loading#$index") }
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Content state
|
||||
*
|
||||
* @property exchangeItems exchanges
|
||||
*/
|
||||
data class Content(
|
||||
override val exchangeItems: ImmutableList<TokenItemState>,
|
||||
) : ExchangesBottomSheetContent
|
||||
|
||||
/** Error state */
|
||||
data class Error(
|
||||
val onRetryClick: () -> Unit,
|
||||
) : ExchangesBottomSheetContent {
|
||||
override val exchangeItems: ImmutableList<TokenItemState> = persistentListOf()
|
||||
|
||||
@StringRes
|
||||
val message: Int = R.string.markets_loading_error_title
|
||||
}
|
||||
}
|
||||
|
|
@ -6,5 +6,8 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
internal data class InfoBottomSheetContent(
|
||||
val title: TextReference,
|
||||
val body: TextReference,
|
||||
val showGeneratedAINotification: Boolean = false,
|
||||
) : TangemBottomSheetConfigContent
|
||||
val generatedAINotificationUM: GeneratedAINotificationUM? = null,
|
||||
) : TangemBottomSheetConfigContent {
|
||||
|
||||
data class GeneratedAINotificationUM(val onClick: () -> Unit)
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.features.markets.details.impl.ui.state
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.pluralReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.features.markets.impl.R
|
||||
|
||||
/**
|
||||
* "Listed on" block UI model
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal sealed interface ListedOnUM {
|
||||
|
||||
/** Title */
|
||||
val title: TextReference
|
||||
get() = resourceReference(id = R.string.markets_token_details_listed_on)
|
||||
|
||||
/** Description */
|
||||
val description: TextReference
|
||||
|
||||
/** Empty state. No exchanges found */
|
||||
data object Empty : ListedOnUM {
|
||||
override val description = resourceReference(id = R.string.markets_token_details_empty_exchanges)
|
||||
}
|
||||
|
||||
/**
|
||||
* Content with number of exchanges
|
||||
*
|
||||
* @property onClick lambda be invoked when button is clicked
|
||||
* @property amount amount of exchanges
|
||||
*/
|
||||
data class Content(
|
||||
val onClick: () -> Unit,
|
||||
private val amount: Int,
|
||||
) : ListedOnUM {
|
||||
override val description: TextReference = pluralReference(
|
||||
id = R.plurals.markets_token_details_amount_exchanges,
|
||||
count = amount,
|
||||
formatArgs = wrappedList(amount),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ internal data class MarketsTokenDetailsUM(
|
|||
val markerSet: Boolean,
|
||||
val chartState: ChartState,
|
||||
val onSelectedIntervalChange: (PriceChangeInterval) -> Unit,
|
||||
val infoBottomSheet: TangemBottomSheetConfig,
|
||||
val bottomSheetConfig: TangemBottomSheetConfig,
|
||||
val triggerPriceChange: StateEvent<PriceChangeType>,
|
||||
val body: Body,
|
||||
) {
|
||||
|
|
@ -41,6 +41,7 @@ internal data class MarketsTokenDetailsUM(
|
|||
val securityScore: SecurityScoreUM?,
|
||||
val metrics: MetricsUM?,
|
||||
val pricePerformance: PricePerformanceUM?,
|
||||
val listedOn: ListedOnUM,
|
||||
val links: LinksUM?,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ internal class MarketsPortfolioModel @Inject constructor(
|
|||
hasMissedDerivationsUseCase.invoke(
|
||||
userWalletId = selectedWalletId,
|
||||
networksWithDerivationPath = addToPortfolioData.addedNetworks[selectedWalletId].orEmpty()
|
||||
.associate { Network.ID(it.networkId) to null },
|
||||
.associate { it.networkId to null },
|
||||
)
|
||||
} else {
|
||||
false
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ internal class TokenActionsHandler @AssistedInject constructor(
|
|||
AppRoute.Swap(
|
||||
currency = cryptoCurrencyData.status.currency,
|
||||
userWalletId = cryptoCurrencyData.userWallet.walletId,
|
||||
isInitialReverseOrder = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,7 +102,8 @@ private class PortfolioTokenUMProvider : CollectionPreviewParameterProvider<Port
|
|||
tokenUM.copy(
|
||||
tokenItemState = tokenUM.tokenItemState.copy(
|
||||
fiatAmountState = contentFiatAmount.copy(text = DASH_SIGN),
|
||||
cryptoAmountState = tokenUM.tokenItemState.cryptoAmountState.copy(text = DASH_SIGN),
|
||||
subtitle2State = (tokenUM.tokenItemState.subtitle2State as TokenItemState.Subtitle2State.TextContent)
|
||||
.copy(text = DASH_SIGN),
|
||||
),
|
||||
),
|
||||
tokenUM.copy(isBalanceHidden = true),
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider<MyPortfol
|
|||
iconState = CurrencyIconState.Locked,
|
||||
titleState = TokenItemState.TitleState.Content(text = "My wallet"),
|
||||
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "486,65 \$"),
|
||||
cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "733,71097 MATIC"),
|
||||
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "733,71097 MATIC"),
|
||||
subtitleState = TokenItemState.SubtitleState.TextContent(value = "XRP Ledger token"),
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
|
|
@ -63,6 +63,6 @@ internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider<MyPortfol
|
|||
onQuickActionLongClick = {},
|
||||
),
|
||||
isBalanceHidden = false,
|
||||
walletId = UserWalletId("walletId"),
|
||||
walletId = UserWalletId(""),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
package com.tangem.features.pushnotifications.api.featuretoggles
|
||||
|
||||
/**
|
||||
* Push notifications feature toggles
|
||||
*/
|
||||
interface PushNotificationsFeatureToggles {
|
||||
/** Availability of push notifications */
|
||||
val isPushNotificationsEnabled: Boolean
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
package com.tangem.features.pushnotifications.impl.di
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles
|
||||
import com.tangem.features.pushnotifications.impl.featuretoggles.DefaultPushNotificationsFeatureToggles
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* DI module provides implementation of [PushNotificationsFeatureToggles]
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object PushNotificationsFeatureTogglesModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSendFeatureToggles(featureTogglesManager: FeatureTogglesManager): PushNotificationsFeatureToggles {
|
||||
return DefaultPushNotificationsFeatureToggles(featureTogglesManager = featureTogglesManager)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
package com.tangem.features.pushnotifications.impl.featuretoggles
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles
|
||||
|
||||
internal class DefaultPushNotificationsFeatureToggles(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : PushNotificationsFeatureToggles {
|
||||
override val isPushNotificationsEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled("PUSH_NOTIFICATIONS_ENABLED")
|
||||
}
|
||||
|
|
@ -9,7 +9,6 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
|
|||
import com.tangem.datasource.api.tangemTech.models.StartReferralBody
|
||||
import com.tangem.datasource.demo.DemoModeDatasource
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.referral.converters.ReferralConverter
|
||||
|
|
@ -68,7 +67,6 @@ internal class ReferralRepositoryImpl @Inject constructor(
|
|||
|
||||
override suspend fun getCryptoCurrency(userWalletId: UserWalletId, tokenData: TokenData): CryptoCurrency? {
|
||||
val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("Wallet $userWalletId not found")
|
||||
val derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider
|
||||
|
||||
val blockchain = Blockchain.fromNetworkId(tokenData.networkId)
|
||||
?: error("Blockchain ${tokenData.networkId} not found")
|
||||
|
|
@ -89,13 +87,13 @@ internal class ReferralRepositoryImpl @Inject constructor(
|
|||
sdkToken = sdkToken,
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = null,
|
||||
derivationStyleProvider = derivationStyleProvider,
|
||||
scanResponse = userWallet.scanResponse,
|
||||
)
|
||||
} else {
|
||||
cryptoCurrencyFactory.createCoin(
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = null,
|
||||
derivationStyleProvider = derivationStyleProvider,
|
||||
scanResponse = userWallet.scanResponse,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
package com.tangem.features.send.impl.presentation.errors
|
||||
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
|
||||
internal class FeeErrorStateMapper {
|
||||
|
||||
fun getFeeError(loadFeeError: GetFeeError?, tokenName: String): FeeSelectorState.Error {
|
||||
return when (loadFeeError) {
|
||||
GetFeeError.BlockchainErrors.TronActivationError -> FeeSelectorState.Error.TronAccountActivationError(
|
||||
tokenName,
|
||||
)
|
||||
is GetFeeError.DataError,
|
||||
GetFeeError.UnknownError,
|
||||
null,
|
||||
-> FeeSelectorState.Error.NetworkError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,58 +1,35 @@
|
|||
package com.tangem.features.send.impl.presentation.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.alerts.models.AlertUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.features.send.impl.R
|
||||
|
||||
@Immutable
|
||||
internal sealed class SendAlertState {
|
||||
|
||||
abstract val title: TextReference?
|
||||
abstract val message: TextReference
|
||||
open val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok)
|
||||
open val onConfirmClick: (() -> Unit)? = null
|
||||
internal sealed class SendAlertUM : AlertUM {
|
||||
|
||||
data class GenericError(
|
||||
override val title: TextReference? = resourceReference(id = R.string.send_alert_transaction_failed_title),
|
||||
override val onConfirmClick: (() -> Unit),
|
||||
) : SendAlertState() {
|
||||
) : SendAlertUM() {
|
||||
override val message: TextReference = resourceReference(R.string.common_unknown_error)
|
||||
override val confirmButtonText: TextReference =
|
||||
resourceReference(id = R.string.common_support)
|
||||
}
|
||||
|
||||
data class TransactionError(
|
||||
val code: String,
|
||||
val cause: String?,
|
||||
val causeTextReference: TextReference? = null,
|
||||
override val onConfirmClick: (() -> Unit),
|
||||
) : SendAlertState() {
|
||||
override val title: TextReference = resourceReference(id = R.string.send_alert_transaction_failed_title)
|
||||
override val message: TextReference = resourceReference(
|
||||
id = R.string.send_alert_transaction_failed_text,
|
||||
formatArgs = wrappedList(causeTextReference ?: cause.orEmpty(), code),
|
||||
)
|
||||
override val confirmButtonText: TextReference =
|
||||
resourceReference(id = R.string.common_support)
|
||||
}
|
||||
|
||||
data class DemoMode(
|
||||
data class FeeIncreased(
|
||||
override val onConfirmClick: () -> Unit,
|
||||
) : SendAlertState() {
|
||||
override val title: TextReference = resourceReference(id = R.string.warning_demo_mode_title)
|
||||
override val message: TextReference = resourceReference(id = R.string.warning_demo_mode_message)
|
||||
}
|
||||
|
||||
data object FeeIncreased : SendAlertState() {
|
||||
) : SendAlertUM() {
|
||||
override val title: TextReference? = null
|
||||
override val message: TextReference = resourceReference(id = R.string.send_notification_high_fee_title)
|
||||
override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok)
|
||||
}
|
||||
|
||||
data class FeeTooLow(
|
||||
override val onConfirmClick: () -> Unit,
|
||||
) : SendAlertState() {
|
||||
) : SendAlertUM() {
|
||||
override val title: TextReference? = null
|
||||
override val message: TextReference = resourceReference(id = R.string.send_alert_fee_too_low_text)
|
||||
override val confirmButtonText: TextReference = resourceReference(R.string.common_continue)
|
||||
|
|
@ -61,7 +38,7 @@ internal sealed class SendAlertState {
|
|||
data class FeeTooHigh(
|
||||
val times: String,
|
||||
override val onConfirmClick: () -> Unit,
|
||||
) : SendAlertState() {
|
||||
) : SendAlertUM() {
|
||||
override val title: TextReference? = null
|
||||
override val message: TextReference =
|
||||
resourceReference(id = R.string.send_alert_fee_too_high_text, wrappedList(times))
|
||||
|
|
@ -70,7 +47,7 @@ internal sealed class SendAlertState {
|
|||
|
||||
data class FeeUnreachableError(
|
||||
override val onConfirmClick: (() -> Unit),
|
||||
) : SendAlertState() {
|
||||
) : SendAlertUM() {
|
||||
override val title: TextReference = resourceReference(R.string.send_fee_unreachable_error_title)
|
||||
override val message: TextReference = resourceReference(R.string.send_fee_unreachable_error_text)
|
||||
override val confirmButtonText = resourceReference(R.string.warning_button_refresh)
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.send.impl.presentation.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.alerts.models.AlertUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
|
|
@ -8,5 +9,5 @@ internal sealed class SendEvent {
|
|||
|
||||
data class ShowSnackBar(val text: TextReference) : SendEvent()
|
||||
|
||||
data class ShowAlert(val alert: SendAlertState) : SendEvent()
|
||||
data class ShowAlert(val alert: AlertUM) : SendEvent()
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.send.impl.presentation.state
|
||||
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.ui.alerts.TransactionErrorAlertConverter
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.event.triggeredEvent
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
|
|
@ -24,9 +25,10 @@ internal class SendEventStateFactory(
|
|||
private val clickIntents: SendClickIntents,
|
||||
private val feeStateFactory: FeeStateFactory,
|
||||
) {
|
||||
private val sendTransactionErrorConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendTransactionAlertConverter(
|
||||
clickIntents = clickIntents,
|
||||
private val transactionErrorAlertConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
TransactionErrorAlertConverter(
|
||||
popBackStack = clickIntents::popBackStack,
|
||||
onFailedTxEmailClick = clickIntents::onFailedTxEmailClick,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -37,7 +39,7 @@ internal class SendEventStateFactory(
|
|||
fun getSendTransactionErrorState(error: SendTransactionError?, onConsume: () -> Unit): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val event = error?.let {
|
||||
sendTransactionErrorConverter.convert(error)?.let {
|
||||
transactionErrorAlertConverter.convert(error)?.let {
|
||||
triggeredEvent<SendEvent>(SendEvent.ShowAlert(it), onConsume)
|
||||
}
|
||||
}
|
||||
|
|
@ -68,7 +70,7 @@ internal class SendEventStateFactory(
|
|||
return if (newFeeValue > oldFeeValue) {
|
||||
updateFeeState.copy(
|
||||
event = triggeredEvent(
|
||||
data = SendEvent.ShowAlert(SendAlertState.FeeIncreased),
|
||||
data = SendEvent.ShowAlert(SendAlertUM.FeeIncreased(onConsume)),
|
||||
onConsume = onConsume,
|
||||
),
|
||||
)
|
||||
|
|
@ -83,7 +85,7 @@ internal class SendEventStateFactory(
|
|||
return state.copy(
|
||||
event = triggeredEvent(
|
||||
data = SendEvent.ShowAlert(
|
||||
SendAlertState.FeeTooLow(
|
||||
SendAlertUM.FeeTooLow(
|
||||
onConfirmClick = clickIntents::showSend,
|
||||
),
|
||||
),
|
||||
|
|
@ -96,7 +98,7 @@ internal class SendEventStateFactory(
|
|||
return currentStateProvider().copy(
|
||||
event = triggeredEvent(
|
||||
data = SendEvent.ShowAlert(
|
||||
SendAlertState.FeeTooHigh(
|
||||
SendAlertUM.FeeTooHigh(
|
||||
onConfirmClick = clickIntents::showSend,
|
||||
times = diff,
|
||||
),
|
||||
|
|
@ -111,7 +113,7 @@ internal class SendEventStateFactory(
|
|||
return state.copy(
|
||||
event = triggeredEvent(
|
||||
data = SendEvent.ShowAlert(
|
||||
SendAlertState.GenericError(
|
||||
SendAlertUM.GenericError(
|
||||
onConfirmClick = { clickIntents.onFailedTxEmailClick(error?.localizedMessage.orEmpty()) },
|
||||
),
|
||||
),
|
||||
|
|
@ -125,7 +127,7 @@ internal class SendEventStateFactory(
|
|||
return state.copy(
|
||||
event = triggeredEvent(
|
||||
data = SendEvent.ShowAlert(
|
||||
SendAlertState.FeeUnreachableError(onConfirmClick = clickIntents::feeReload),
|
||||
SendAlertUM.FeeUnreachableError(onConfirmClick = clickIntents::feeReload),
|
||||
),
|
||||
onConsume = onConsume,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,250 +0,0 @@
|
|||
package com.tangem.features.send.impl.presentation.state
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.features.send.impl.R
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal sealed class SendNotification(val config: NotificationConfig) {
|
||||
|
||||
sealed class Error(
|
||||
title: TextReference,
|
||||
subtitle: TextReference,
|
||||
iconResId: Int = R.drawable.ic_alert_24,
|
||||
buttonState: NotificationConfig.ButtonsState? = null,
|
||||
onCloseClick: (() -> Unit)? = null,
|
||||
) : SendNotification(
|
||||
config = NotificationConfig(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
iconResId = iconResId,
|
||||
buttonsState = buttonState,
|
||||
onCloseClick = onCloseClick,
|
||||
),
|
||||
) {
|
||||
|
||||
data object TotalExceedsBalance : Error(
|
||||
title = resourceReference(R.string.send_notification_exceed_balance_title),
|
||||
subtitle = resourceReference(R.string.send_notification_exceed_balance_text),
|
||||
)
|
||||
|
||||
data object InvalidAmount : Error(
|
||||
title = resourceReference(R.string.send_notification_invalid_amount_title),
|
||||
subtitle = resourceReference(R.string.send_notification_invalid_amount_text),
|
||||
)
|
||||
|
||||
data class MinimumAmountError(val amount: String) : Error(
|
||||
title = resourceReference(R.string.send_notification_invalid_amount_title),
|
||||
subtitle = resourceReference(
|
||||
R.string.send_notification_invalid_minimum_amount_text,
|
||||
wrappedList(amount, amount),
|
||||
),
|
||||
)
|
||||
|
||||
data class TransactionLimitError(
|
||||
val cryptoCurrency: String,
|
||||
val utxoLimit: String,
|
||||
val amountLimit: String,
|
||||
val onConfirmClick: () -> Unit,
|
||||
) : Error(
|
||||
title = resourceReference(R.string.send_notification_transaction_limit_title),
|
||||
subtitle = resourceReference(
|
||||
R.string.send_notification_transaction_limit_text,
|
||||
wrappedList(cryptoCurrency, utxoLimit, amountLimit),
|
||||
),
|
||||
buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
|
||||
text = resourceReference(R.string.send_notification_leave_button, wrappedList(amountLimit)),
|
||||
onClick = onConfirmClick,
|
||||
),
|
||||
)
|
||||
|
||||
data class ExceedsBalance(
|
||||
val networkIconId: Int,
|
||||
val currencyName: String,
|
||||
val feeName: String,
|
||||
val feeSymbol: String,
|
||||
val networkName: String,
|
||||
val mergeFeeNetworkName: Boolean = false,
|
||||
val onClick: (() -> Unit)? = null,
|
||||
) : Error(
|
||||
title = resourceReference(
|
||||
id = R.string.warning_send_blocked_funds_for_fee_title,
|
||||
wrappedList(feeName),
|
||||
),
|
||||
subtitle = resourceReference(
|
||||
id = R.string.warning_send_blocked_funds_for_fee_message,
|
||||
formatArgs = wrappedList(currencyName, networkName, currencyName, feeName, feeSymbol),
|
||||
),
|
||||
iconResId = networkIconId,
|
||||
buttonState = onClick?.let {
|
||||
NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||
text = resourceReference(
|
||||
R.string.common_buy_currency,
|
||||
wrappedList(
|
||||
if (mergeFeeNetworkName) {
|
||||
"$currencyName ($feeSymbol)"
|
||||
} else {
|
||||
feeName
|
||||
},
|
||||
),
|
||||
),
|
||||
onClick = onClick,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
data class ExistentialDeposit(val deposit: String, val onConfirmClick: () -> Unit) : Error(
|
||||
title = resourceReference(R.string.send_notification_existential_deposit_title),
|
||||
subtitle = resourceReference(R.string.send_notification_existential_deposit_text, wrappedList(deposit)),
|
||||
buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
|
||||
text = resourceReference(R.string.common_ok),
|
||||
onClick = onConfirmClick,
|
||||
),
|
||||
)
|
||||
|
||||
data class ReserveAmount(val amount: String) : Error(
|
||||
title = resourceReference(
|
||||
id = R.string.send_notification_invalid_reserve_amount_title,
|
||||
wrappedList(amount),
|
||||
),
|
||||
subtitle = resourceReference(id = R.string.send_notification_invalid_reserve_amount_text),
|
||||
)
|
||||
}
|
||||
|
||||
sealed class Warning(
|
||||
title: TextReference,
|
||||
subtitle: TextReference,
|
||||
buttonsState: NotificationConfig.ButtonsState? = null,
|
||||
onCloseClick: (() -> Unit)? = null,
|
||||
) : SendNotification(
|
||||
config = NotificationConfig(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
iconResId = R.drawable.img_attention_20,
|
||||
buttonsState = buttonsState,
|
||||
onCloseClick = onCloseClick,
|
||||
),
|
||||
) {
|
||||
data class HighFeeError(
|
||||
val currencyName: String,
|
||||
val amount: String,
|
||||
val onConfirmClick: () -> Unit,
|
||||
val onCloseClick: () -> Unit,
|
||||
) : Warning(
|
||||
title = resourceReference(R.string.send_notification_high_fee_title),
|
||||
subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(currencyName, amount)),
|
||||
buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
|
||||
text = resourceReference(R.string.send_notification_reduce_by, wrappedList(amount)),
|
||||
onClick = onConfirmClick,
|
||||
),
|
||||
onCloseClick = onCloseClick,
|
||||
)
|
||||
|
||||
data object FeeTooLow : Warning(
|
||||
title = resourceReference(id = R.string.send_notification_transaction_delay_title),
|
||||
subtitle = resourceReference(id = R.string.send_notification_transaction_delay_text),
|
||||
)
|
||||
|
||||
data class TooHigh(
|
||||
val value: String,
|
||||
) : Warning(
|
||||
title = resourceReference(id = R.string.send_notification_fee_too_high_title),
|
||||
subtitle = resourceReference(id = R.string.send_notification_fee_too_high_text, wrappedList(value)),
|
||||
)
|
||||
|
||||
data class NetworkFeeUnreachable(val onRefresh: () -> Unit) : Warning(
|
||||
title = resourceReference(R.string.send_fee_unreachable_error_title),
|
||||
subtitle = resourceReference(R.string.send_fee_unreachable_error_text),
|
||||
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||
text = resourceReference(R.string.warning_button_refresh),
|
||||
onClick = onRefresh,
|
||||
),
|
||||
)
|
||||
|
||||
data class TronAccountNotActivated(val tokenName: String) : Warning(
|
||||
title = resourceReference(R.string.send_fee_unreachable_error_title),
|
||||
subtitle = resourceReference(
|
||||
R.string.send_tron_account_activation_error,
|
||||
wrappedList(tokenName),
|
||||
),
|
||||
)
|
||||
|
||||
data class FeeCoverageNotification(val cryptoAmount: String, val fiatAmount: String) : Warning(
|
||||
title = resourceReference(R.string.send_network_fee_warning_title),
|
||||
subtitle = resourceReference(
|
||||
R.string.common_network_fee_warning_content,
|
||||
wrappedList(cryptoAmount, fiatAmount),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
sealed interface Cardano {
|
||||
|
||||
data class MinAdaValueCharged(val tokenName: String, val minAdaValue: String) : Warning(
|
||||
title = resourceReference(id = R.string.cardano_coin_will_be_send_with_token_title),
|
||||
subtitle = resourceReference(
|
||||
id = R.string.cardano_coin_will_be_send_with_token_description,
|
||||
formatArgs = wrappedList(minAdaValue, tokenName),
|
||||
),
|
||||
)
|
||||
|
||||
data object InsufficientBalanceToTransferCoin : Error(
|
||||
title = resourceReference(id = R.string.cardano_max_amount_has_token_title),
|
||||
subtitle = resourceReference(id = R.string.cardano_max_amount_has_token_description),
|
||||
)
|
||||
|
||||
data class InsufficientBalanceToTransferToken(val tokenName: String) : Error(
|
||||
title = resourceReference(id = R.string.cardano_insufficient_balance_to_send_token_title),
|
||||
subtitle = resourceReference(
|
||||
id = R.string.cardano_insufficient_balance_to_send_token_description,
|
||||
formatArgs = wrappedList(tokenName),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
sealed interface Koinos {
|
||||
data class InsufficientRecoverableMana(
|
||||
val mana: BigDecimal,
|
||||
val maxMana: BigDecimal,
|
||||
) : Error(
|
||||
title = resourceReference(R.string.koinos_insufficient_mana_to_send_koin_title),
|
||||
subtitle = resourceReference(
|
||||
R.string.koinos_insufficient_mana_to_send_koin_description,
|
||||
formatArgs = wrappedList(
|
||||
BigDecimalFormatter.formatCryptoAmountShorted(mana, "", Blockchain.Koinos.decimals()),
|
||||
BigDecimalFormatter.formatCryptoAmountShorted(maxMana, "", Blockchain.Koinos.decimals()),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
data object InsufficientBalance : Error(
|
||||
title = resourceReference(R.string.koinos_insufficient_balance_to_send_koin_title),
|
||||
subtitle = resourceReference(R.string.koinos_insufficient_balance_to_send_koin_description),
|
||||
)
|
||||
|
||||
data class ManaExceedsBalance(
|
||||
val availableKoinForTransfer: BigDecimal,
|
||||
val onReduceClick: () -> Unit,
|
||||
) : Error(
|
||||
title = resourceReference(R.string.koinos_mana_exceeds_koin_balance_title),
|
||||
subtitle = resourceReference(
|
||||
R.string.koinos_mana_exceeds_koin_balance_description,
|
||||
formatArgs = wrappedList(
|
||||
BigDecimalFormatter.formatCryptoAmount(
|
||||
availableKoinForTransfer,
|
||||
Blockchain.Koinos.currency,
|
||||
Blockchain.Koinos.decimals(),
|
||||
),
|
||||
),
|
||||
),
|
||||
buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
|
||||
text = resourceReference(R.string.send_notification_reduce_to, wrappedList(availableKoinForTransfer)),
|
||||
onClick = onReduceClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.features.send.impl.presentation.state
|
|||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.common.ui.amountScreen.converters.AmountStateConverter
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
|
|
@ -164,14 +165,14 @@ internal class SendStateFactory(
|
|||
)
|
||||
}
|
||||
|
||||
fun getSendNotificationState(notifications: ImmutableList<SendNotification>): SendUiState {
|
||||
fun getSendNotificationState(notifications: ImmutableList<NotificationUM>): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val sendState = state.sendState ?: return state
|
||||
val reducedBy = sendState.reduceAmountBy.takeIf {
|
||||
notifications.none {
|
||||
it is SendNotification.Error.ExistentialDeposit ||
|
||||
it is SendNotification.Error.TransactionLimitError ||
|
||||
it is SendNotification.Warning.HighFeeError
|
||||
it is NotificationUM.Error.ExistentialDeposit ||
|
||||
it is NotificationUM.Error.TransactionLimitError ||
|
||||
it is NotificationUM.Warning.HighFeeError
|
||||
}
|
||||
}
|
||||
return state.copy(
|
||||
|
|
@ -199,10 +200,10 @@ internal class SendStateFactory(
|
|||
private fun isPrimaryButtonEnabled(
|
||||
state: SendUiState,
|
||||
isSending: Boolean,
|
||||
notifications: ImmutableList<SendNotification>,
|
||||
notifications: ImmutableList<NotificationUM>,
|
||||
): Boolean {
|
||||
val feeState = state.getFeeState(stateRouterProvider().isEditState) ?: return false
|
||||
val hasErrorNotifications = notifications.any { it is SendNotification.Error }
|
||||
val hasErrorNotifications = notifications.any { it is NotificationUM.Error }
|
||||
return !hasErrorNotifications && !isSending && feeState.feeSelectorState is FeeSelectorState.Content
|
||||
}
|
||||
//endregion
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
package com.tangem.features.send.impl.presentation.state
|
||||
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class SendTransactionAlertConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
) : Converter<SendTransactionError, SendAlertState?> {
|
||||
override fun convert(value: SendTransactionError): SendAlertState? {
|
||||
return when (value) {
|
||||
SendTransactionError.DemoCardError -> SendAlertState.DemoMode(
|
||||
onConfirmClick = { clickIntents.popBackStack() },
|
||||
)
|
||||
is SendTransactionError.TangemSdkError -> SendAlertState.TransactionError(
|
||||
code = value.code.toString(),
|
||||
cause = null,
|
||||
causeTextReference = resourceReference(value.messageRes, wrappedList(value.args)),
|
||||
onConfirmClick = { clickIntents.onFailedTxEmailClick(value.code.toString()) },
|
||||
)
|
||||
is SendTransactionError.BlockchainSdkError -> SendAlertState.TransactionError(
|
||||
code = value.code.toString(),
|
||||
cause = value.message,
|
||||
onConfirmClick = { clickIntents.onFailedTxEmailClick("${value.code}: ${value.message.orEmpty()}") },
|
||||
)
|
||||
is SendTransactionError.DataError -> SendAlertState.TransactionError(
|
||||
code = "",
|
||||
cause = value.message,
|
||||
onConfirmClick = { clickIntents.onFailedTxEmailClick(value.message.orEmpty()) },
|
||||
)
|
||||
is SendTransactionError.NetworkError -> SendAlertState.TransactionError(
|
||||
code = value.code.orEmpty(),
|
||||
cause = value.message.orEmpty(),
|
||||
onConfirmClick = { clickIntents.onFailedTxEmailClick(value.message.orEmpty()) },
|
||||
)
|
||||
is SendTransactionError.UnknownError -> SendAlertState.TransactionError(
|
||||
code = "",
|
||||
cause = value.ex?.localizedMessage,
|
||||
onConfirmClick = { clickIntents.onFailedTxEmailClick(value.ex?.localizedMessage.orEmpty()) },
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable
|
|||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent
|
||||
|
|
@ -112,7 +113,7 @@ internal sealed class SendStates {
|
|||
val appCurrency: AppCurrency,
|
||||
val isFeeApproximate: Boolean,
|
||||
val isCustomSelected: Boolean,
|
||||
val notifications: ImmutableList<SendNotification>,
|
||||
val notifications: ImmutableList<NotificationUM>,
|
||||
val isTronToken: Boolean,
|
||||
) : SendStates()
|
||||
|
||||
|
|
@ -129,7 +130,7 @@ internal sealed class SendStates {
|
|||
val reduceAmountBy: BigDecimal?,
|
||||
val isFromConfirmation: Boolean,
|
||||
val showTapHelp: Boolean,
|
||||
val notifications: ImmutableList<SendNotification>,
|
||||
val notifications: ImmutableList<NotificationUM>,
|
||||
) : SendStates()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,31 +1,35 @@
|
|||
package com.tangem.features.send.impl.presentation.state.confirm
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.BlockchainSdkError
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.blockchainsdk.utils.minimalAmount
|
||||
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.amountScreen.utils.getFiatString
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addDustWarningNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addExceedBalanceNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addExceedsBalanceNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addExistentialWarningNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addFeeCoverageNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addFeeUnreachableNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addReserveAmountErrorNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addTransactionLimitErrorNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addValidateTransactionNotifications
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.ui.extensions.networkIconResId
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.GetBalanceNotEnoughForFeeWarningUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.GetCurrencyCheckUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
|
||||
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase
|
||||
import com.tangem.domain.utils.convertToSdkAmount
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents
|
||||
import com.tangem.features.send.impl.presentation.state.*
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiStateType
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.*
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
|
|
@ -41,32 +45,33 @@ import kotlinx.coroutines.flow.map
|
|||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
internal class SendNotificationFactory(
|
||||
internal class SendNotificationFactory constructor(
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val validateTransactionUseCase: ValidateTransactionUseCase,
|
||||
private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase,
|
||||
private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val userWalletProvider: Provider<UserWallet>,
|
||||
private val currencyChecksRepository: CurrencyChecksRepository,
|
||||
private val stateRouterProvider: Provider<StateRouter>,
|
||||
private val isSubtractAvailableProvider: Provider<Boolean>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase,
|
||||
private val validateTransactionUseCase: ValidateTransactionUseCase,
|
||||
private val userWalletId: UserWalletId,
|
||||
) {
|
||||
|
||||
fun create(): Flow<ImmutableList<SendNotification>> = stateRouterProvider().currentState
|
||||
fun create(): Flow<ImmutableList<NotificationUM>> = stateRouterProvider().currentState
|
||||
.filter { it.type == SendUiStateType.Send }
|
||||
.map {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
|
||||
val state = currentStateProvider()
|
||||
val isEditState = stateRouterProvider().isEditState
|
||||
val balance = cryptoCurrencyStatusProvider().value.amount.orZero()
|
||||
val balance = cryptoCurrencyStatus.value.amount.orZero()
|
||||
val sendState = state.sendState ?: return@map persistentListOf()
|
||||
val feeState = state.getFeeState(isEditState) ?: return@map persistentListOf()
|
||||
val amountState = state.getAmountState(isEditState) as? AmountState.Data ?: return@map persistentListOf()
|
||||
|
||||
val recipientAddress = state.recipientState?.addressTextField?.value.orEmpty()
|
||||
val amountValue = amountState.amountTextField.cryptoAmount.value.orZero()
|
||||
val feeValue = feeState.fee?.amount?.value.orZero()
|
||||
val reduceAmountBy = sendState.reduceAmountBy.orZero()
|
||||
|
|
@ -78,42 +83,38 @@ internal class SendNotificationFactory(
|
|||
reduceAmountBy = reduceAmountBy,
|
||||
)
|
||||
val sendingAmount = checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable = isFeeCoverage,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatusProvider(),
|
||||
isAmountSubtractAvailable = isSubtractAvailableProvider(),
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
)
|
||||
val feeError = (feeState.feeSelectorState as? FeeSelectorState.Error)?.error
|
||||
val currencyCheck = getCurrencyCheckUseCase(
|
||||
userWalletId = userWalletId,
|
||||
currencyStatus = cryptoCurrencyStatus,
|
||||
amount = amountValue,
|
||||
fee = feeValue,
|
||||
)
|
||||
buildList {
|
||||
// errors
|
||||
addFeeUnreachableNotification(feeState.feeSelectorState)
|
||||
addExceedBalanceNotification(feeValue, sendingAmount)
|
||||
addExceedsBalanceNotification(feeState.fee)
|
||||
addDustWarningNotificationForSpecificBlockchains(feeValue, sendingAmount)
|
||||
addTransactionLimitErrorNotification(feeValue, sendingAmount)
|
||||
addReserveAmountErrorNotification(recipientAddress, sendingAmount)
|
||||
|
||||
// warnings
|
||||
addExistentialWarningNotification(feeValue, amountValue)
|
||||
addFeeCoverageNotification(
|
||||
isFeeCoverage = isFeeCoverage,
|
||||
amountField = amountState.amountTextField,
|
||||
sendingValue = sendingAmount,
|
||||
)
|
||||
addHighFeeWarningNotification(amountValue, sendState.ignoreAmountReduce)
|
||||
addTooHighNotification(feeState.feeSelectorState)
|
||||
addTooLowNotification(feeState)
|
||||
|
||||
// blockchain specific
|
||||
addValidateTransactionNotifications(
|
||||
addErrorNotifications(
|
||||
feeError = feeError,
|
||||
sendingAmount = sendingAmount,
|
||||
fee = feeState.fee,
|
||||
state = state,
|
||||
feeValue = feeValue,
|
||||
currencyCheck = currencyCheck,
|
||||
)
|
||||
addWarningNotifications(
|
||||
amountState = amountState,
|
||||
feeState = feeState,
|
||||
sendState = sendState,
|
||||
sendingAmount = sendingAmount,
|
||||
isFeeCoverage = isFeeCoverage,
|
||||
currencyCheck = currencyCheck,
|
||||
)
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
fun dismissNotificationState(clazz: Class<out SendNotification>, isIgnored: Boolean = false): SendUiState {
|
||||
fun dismissNotificationState(clazz: Class<out NotificationUM>, isIgnored: Boolean = false): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val sendState = state.sendState ?: return state
|
||||
val notificationsToRemove = sendState.notifications.filterIsInstance(clazz)
|
||||
|
|
@ -128,161 +129,120 @@ internal class SendNotificationFactory(
|
|||
)
|
||||
}
|
||||
|
||||
private fun MutableList<SendNotification>.addFeeUnreachableNotification(feeSelectorState: FeeSelectorState) {
|
||||
when (feeSelectorState) {
|
||||
is FeeSelectorState.Error.TronAccountActivationError -> add(
|
||||
SendNotification.Warning.TronAccountNotActivated(
|
||||
feeSelectorState.tokenName,
|
||||
),
|
||||
)
|
||||
is FeeSelectorState.Error.NetworkError -> add(
|
||||
SendNotification.Warning.NetworkFeeUnreachable(clickIntents::feeReload),
|
||||
)
|
||||
else -> {
|
||||
/* do nothing */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<SendNotification>.addExceedBalanceNotification(
|
||||
feeAmount: BigDecimal,
|
||||
receivedAmount: BigDecimal,
|
||||
) {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
||||
|
||||
if (!isSubtractAvailableProvider()) return
|
||||
|
||||
val showNotification = receivedAmount + feeAmount > balance
|
||||
if (showNotification) {
|
||||
add(SendNotification.Error.TotalExceedsBalance)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun MutableList<SendNotification>.addReserveAmountErrorNotification(
|
||||
recipientAddress: String,
|
||||
private suspend fun MutableList<NotificationUM>.addErrorNotifications(
|
||||
feeError: GetFeeError?,
|
||||
sendingAmount: BigDecimal,
|
||||
feeValue: BigDecimal,
|
||||
currencyCheck: CryptoCurrencyCheck,
|
||||
) {
|
||||
val userWalletId = userWalletProvider().walletId
|
||||
val cryptoCurrency = cryptoCurrencyStatusProvider().currency
|
||||
val isAccountFunded = currencyChecksRepository.checkIfAccountFunded(
|
||||
userWalletId,
|
||||
cryptoCurrency.network,
|
||||
recipientAddress,
|
||||
)
|
||||
val minimumAmount = currencyChecksRepository.getReserveAmount(userWalletId, cryptoCurrency.network)
|
||||
if (!isAccountFunded && minimumAmount != null && minimumAmount > sendingAmount) {
|
||||
add(
|
||||
SendNotification.Error.ReserveAmount(
|
||||
BigDecimalFormatter.formatCryptoAmount(
|
||||
cryptoAmount = minimumAmount,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun MutableList<SendNotification>.addTransactionLimitErrorNotification(
|
||||
feeAmount: BigDecimal,
|
||||
receivedAmount: BigDecimal,
|
||||
) {
|
||||
val userWalletId = userWalletProvider().walletId
|
||||
val cryptoCurrency = cryptoCurrencyStatusProvider().currency
|
||||
val utxoLimit = currencyChecksRepository.checkUtxoAmountLimit(
|
||||
userWalletId = userWalletId,
|
||||
network = cryptoCurrency.network,
|
||||
amount = receivedAmount,
|
||||
fee = feeAmount,
|
||||
)
|
||||
|
||||
if (utxoLimit != null) {
|
||||
add(
|
||||
SendNotification.Error.TransactionLimitError(
|
||||
cryptoCurrency = cryptoCurrency.name,
|
||||
utxoLimit = utxoLimit.maxLimit.toPlainString(),
|
||||
amountLimit = BigDecimalFormatter.formatCryptoAmount(
|
||||
cryptoAmount = utxoLimit.maxAmount,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
),
|
||||
onConfirmClick = {
|
||||
clickIntents.onAmountReduceClick(
|
||||
reduceAmountTo = utxoLimit.maxAmount,
|
||||
clazz = SendNotification.Error.TransactionLimitError::class.java,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun MutableList<SendNotification>.addExistentialWarningNotification(
|
||||
feeAmount: BigDecimal,
|
||||
receivedAmount: BigDecimal,
|
||||
) {
|
||||
val userWalletId = userWalletProvider().walletId
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val cryptoCurrency = cryptoCurrencyStatus.currency
|
||||
val balance = cryptoCurrencyStatus.value.amount ?: return
|
||||
val spendingAmount = if (cryptoCurrency is CryptoCurrency.Token) {
|
||||
feeAmount
|
||||
} else {
|
||||
receivedAmount
|
||||
}
|
||||
val currencyDeposit = currencyChecksRepository.getExistentialDeposit(
|
||||
userWalletId,
|
||||
cryptoCurrency.network,
|
||||
val currency = cryptoCurrencyStatusProvider().currency
|
||||
val currencyWarning = getBalanceNotEnoughForFeeWarningUseCase(
|
||||
fee = feeValue,
|
||||
userWalletId = userWalletId,
|
||||
tokenStatus = cryptoCurrencyStatus,
|
||||
coinStatus = feeCryptoCurrencyStatusProvider() ?: cryptoCurrencyStatus,
|
||||
).getOrNull()
|
||||
|
||||
addFeeUnreachableNotification(
|
||||
feeError = feeError,
|
||||
tokenName = currency.name,
|
||||
onReload = clickIntents::feeReload,
|
||||
)
|
||||
val diff = balance.minus(spendingAmount)
|
||||
if (currencyDeposit != null && diff >= BigDecimal.ZERO && currencyDeposit > diff) {
|
||||
add(
|
||||
SendNotification.Error.ExistentialDeposit(
|
||||
deposit = BigDecimalFormatter.formatCryptoAmountUncapped(
|
||||
cryptoAmount = currencyDeposit,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
addExceedBalanceNotification(
|
||||
feeAmount = feeValue,
|
||||
sendingAmount = sendingAmount,
|
||||
isSubtractionAvailable = isSubtractAvailableProvider(),
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
)
|
||||
addExceedsBalanceNotification(
|
||||
cryptoCurrencyWarning = currencyWarning,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
shouldMergeFeeNetworkName = BlockchainUtils.isArbitrum(currency.network.backendId),
|
||||
onClick = clickIntents::onTokenDetailsClick,
|
||||
onAnalyticsEvent = {
|
||||
analyticsEventHandler.send(
|
||||
SendAnalyticEvents.NoticeNotEnoughFee(
|
||||
token = cryptoCurrencyStatus.currency.symbol,
|
||||
blockchain = cryptoCurrencyStatus.currency.network.name,
|
||||
),
|
||||
onConfirmClick = {
|
||||
clickIntents.onAmountReduceClick(
|
||||
reduceAmountByDiff = currencyDeposit.minus(diff),
|
||||
reduceAmountBy = currencyDeposit,
|
||||
clazz = SendNotification.Error.ExistentialDeposit::class.java,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
if (!BlockchainUtils.isCardano(currency.id.value)) {
|
||||
addDustWarningNotification(
|
||||
dustValue = currencyCheck.dustValue,
|
||||
feeValue = feeValue,
|
||||
sendingAmount = sendingAmount,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
feeCurrencyStatus = feeCryptoCurrencyStatusProvider(),
|
||||
)
|
||||
}
|
||||
addTransactionLimitErrorNotification(
|
||||
utxoLimit = currencyCheck.utxoAmountLimit,
|
||||
cryptoCurrency = currency,
|
||||
onReduceClick = clickIntents::onAmountReduceToClick,
|
||||
)
|
||||
addReserveAmountErrorNotification(
|
||||
reserveAmount = currencyCheck.reserveAmount,
|
||||
sendingAmount = sendingAmount,
|
||||
cryptoCurrency = currency,
|
||||
isAccountFunded = false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun MutableList<SendNotification>.addFeeCoverageNotification(
|
||||
private suspend fun MutableList<NotificationUM>.addWarningNotifications(
|
||||
amountState: AmountState.Data,
|
||||
feeState: SendStates.FeeState,
|
||||
sendState: SendStates.SendState,
|
||||
sendingAmount: BigDecimal,
|
||||
isFeeCoverage: Boolean,
|
||||
amountField: AmountFieldModel,
|
||||
sendingValue: BigDecimal,
|
||||
currencyCheck: CryptoCurrencyCheck,
|
||||
) {
|
||||
if (isFeeCoverage) {
|
||||
analyticsEventHandler.send(SendAnalyticEvents.NoticeFeeCoverage)
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val cryptoCurrency = cryptoCurrencyStatus.currency
|
||||
val fiatRate = cryptoCurrencyStatus.value.fiatRate
|
||||
val amountValue = amountField.cryptoAmount.value ?: return
|
||||
|
||||
val cryptoDiff = amountValue.minus(sendingValue)
|
||||
add(
|
||||
SendNotification.Warning.FeeCoverageNotification(
|
||||
cryptoAmount = BigDecimalFormatter.formatCryptoAmountUncapped(
|
||||
cryptoAmount = cryptoDiff,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
),
|
||||
fiatAmount = getFiatString(
|
||||
value = cryptoDiff,
|
||||
rate = fiatRate,
|
||||
appCurrency = appCurrencyProvider(),
|
||||
),
|
||||
),
|
||||
)
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val currency = cryptoCurrencyStatus.currency
|
||||
val amountValue = amountState.amountTextField.cryptoAmount.value
|
||||
val validationError = amountValue?.let {
|
||||
validateTransactionUseCase(
|
||||
userWalletId = userWalletId,
|
||||
amount = amountValue.convertToSdkAmount(cryptoCurrencyStatus.currency),
|
||||
fee = feeState.fee,
|
||||
memo = null,
|
||||
destination = "",
|
||||
network = cryptoCurrencyStatus.currency.network,
|
||||
).leftOrNull()
|
||||
}
|
||||
addExistentialWarningNotification(
|
||||
existentialDeposit = currencyCheck.existentialDeposit,
|
||||
feeAmount = feeState.fee?.amount?.value.orZero(),
|
||||
receivedAmount = sendingAmount,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
onReduceClick = clickIntents::onAmountReduceByClick,
|
||||
)
|
||||
addFeeCoverageNotification(
|
||||
isFeeCoverage = isFeeCoverage,
|
||||
amountField = amountState.amountTextField,
|
||||
sendingValue = sendingAmount,
|
||||
appCurrency = appCurrencyProvider(),
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
)
|
||||
addValidateTransactionNotifications(
|
||||
dustValue = currencyCheck.dustValue.orZero(),
|
||||
fee = feeState.fee,
|
||||
validationError = validationError,
|
||||
cryptoCurrency = currency,
|
||||
onReduceClick = clickIntents::onAmountReduceToClick,
|
||||
)
|
||||
|
||||
addHighFeeWarningNotification(
|
||||
amountState.amountTextField.cryptoAmount.value.orZero(),
|
||||
sendState.ignoreAmountReduce,
|
||||
)
|
||||
addTooHighNotification(feeState.feeSelectorState)
|
||||
addTooLowNotification(feeState)
|
||||
}
|
||||
|
||||
private fun MutableList<SendNotification>.addHighFeeWarningNotification(
|
||||
private fun MutableList<NotificationUM>.addHighFeeWarningNotification(
|
||||
sendAmount: BigDecimal,
|
||||
ignoreAmountReduce: Boolean,
|
||||
) {
|
||||
|
|
@ -293,61 +253,32 @@ internal class SendNotificationFactory(
|
|||
val isTotalBalance = sendAmount >= balance && balance > threshold
|
||||
if (!ignoreAmountReduce && isTotalBalance && isTezos) {
|
||||
add(
|
||||
SendNotification.Warning.HighFeeError(
|
||||
NotificationUM.Warning.HighFeeError(
|
||||
currencyName = cryptoCurrencyStatus.currency.name,
|
||||
amount = threshold.toPlainString(),
|
||||
onConfirmClick = {
|
||||
clickIntents.onAmountReduceClick(
|
||||
clickIntents.onAmountReduceByClick(
|
||||
reduceAmountBy = threshold,
|
||||
clazz = SendNotification.Warning.HighFeeError::class.java,
|
||||
reduceAmountByDiff = threshold,
|
||||
notification = NotificationUM.Warning.HighFeeError::class.java,
|
||||
)
|
||||
},
|
||||
onCloseClick = {
|
||||
clickIntents.onNotificationCancel(SendNotification.Warning.HighFeeError::class.java)
|
||||
clickIntents.onNotificationCancel(NotificationUM.Warning.HighFeeError::class.java)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun MutableList<SendNotification>.addDustWarningNotificationForSpecificBlockchains(
|
||||
feeValue: BigDecimal,
|
||||
sendingAmount: BigDecimal,
|
||||
) {
|
||||
val isCardano = BlockchainUtils.isCardano(cryptoCurrencyStatusProvider().currency.network.id.value)
|
||||
|
||||
if (!isCardano) {
|
||||
addDustWarningNotification(feeValue, sendingAmount)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkDustLimits(feeAmount: BigDecimal, receivedAmount: BigDecimal, dustValue: BigDecimal): Boolean {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val feeCurrencyStatus = feeCryptoCurrencyStatusProvider() ?: return false
|
||||
|
||||
val change = when (cryptoCurrencyStatus.currency) {
|
||||
is CryptoCurrency.Coin -> {
|
||||
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
||||
balance - (feeAmount + receivedAmount)
|
||||
}
|
||||
is CryptoCurrency.Token -> {
|
||||
val balance = feeCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
||||
balance - feeAmount
|
||||
}
|
||||
}
|
||||
|
||||
val isChangeLowerThanDust = change < dustValue && change > BigDecimal.ZERO
|
||||
return receivedAmount < dustValue || isChangeLowerThanDust
|
||||
}
|
||||
|
||||
private fun MutableList<SendNotification>.addTooLowNotification(feeState: SendStates.FeeState) {
|
||||
private fun MutableList<NotificationUM>.addTooLowNotification(feeState: SendStates.FeeState) {
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return
|
||||
val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return
|
||||
val minimumValue = multipleFees.minimum.amount.value ?: return
|
||||
val customAmount = feeSelectorState.customValues.firstOrNull() ?: return
|
||||
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
|
||||
if (feeSelectorState.selectedFee == FeeType.Custom && minimumValue > customValue) {
|
||||
add(SendNotification.Warning.FeeTooLow)
|
||||
add(NotificationUM.Warning.FeeTooLow)
|
||||
analyticsEventHandler.send(
|
||||
SendAnalyticEvents.NoticeTransactionDelays(
|
||||
cryptoCurrencyStatusProvider().currency.symbol,
|
||||
|
|
@ -356,206 +287,11 @@ internal class SendNotificationFactory(
|
|||
}
|
||||
}
|
||||
|
||||
private fun MutableList<SendNotification>.addTooHighNotification(feeSelectorState: FeeSelectorState) {
|
||||
private fun MutableList<NotificationUM>.addTooHighNotification(feeSelectorState: FeeSelectorState) {
|
||||
if (feeSelectorState !is FeeSelectorState.Content) return
|
||||
|
||||
checkIfFeeTooHigh(feeSelectorState) { diff ->
|
||||
add(SendNotification.Warning.TooHigh(diff))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun MutableList<SendNotification>.addExceedsBalanceNotification(fee: Fee?) {
|
||||
val feeValue = fee?.amount?.value ?: BigDecimal.ZERO
|
||||
val userWalletId = userWalletProvider().walletId
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val feeCurrencyStatus = feeCryptoCurrencyStatusProvider() ?: return
|
||||
|
||||
val warning = getBalanceNotEnoughForFeeWarningUseCase(
|
||||
fee = feeValue,
|
||||
userWalletId = userWalletId,
|
||||
tokenStatus = cryptoCurrencyStatus,
|
||||
coinStatus = feeCurrencyStatus,
|
||||
).getOrNull() ?: return
|
||||
|
||||
val mergeFeeNetworkName = cryptoCurrencyStatus.shouldMergeFeeNetworkName()
|
||||
when (warning) {
|
||||
is CryptoCurrencyWarning.BalanceNotEnoughForFee -> {
|
||||
add(
|
||||
SendNotification.Error.ExceedsBalance(
|
||||
networkIconId = warning.coinCurrency.networkIconResId,
|
||||
networkName = warning.coinCurrency.name,
|
||||
currencyName = cryptoCurrencyStatus.currency.name,
|
||||
feeName = warning.coinCurrency.name,
|
||||
feeSymbol = warning.coinCurrency.symbol,
|
||||
mergeFeeNetworkName = mergeFeeNetworkName,
|
||||
onClick = {
|
||||
clickIntents.onTokenDetailsClick(
|
||||
userWalletId = userWalletId,
|
||||
currency = warning.coinCurrency,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
analyticsEventHandler.send(
|
||||
SendAnalyticEvents.NoticeNotEnoughFee(
|
||||
token = cryptoCurrencyStatus.currency.symbol,
|
||||
blockchain = cryptoCurrencyStatus.currency.network.name,
|
||||
),
|
||||
)
|
||||
}
|
||||
is CryptoCurrencyWarning.CustomTokenNotEnoughForFee -> {
|
||||
val currency = warning.feeCurrency
|
||||
add(
|
||||
SendNotification.Error.ExceedsBalance(
|
||||
networkIconId = currency?.networkIconResId ?: R.drawable.ic_alert_24,
|
||||
currencyName = warning.currency.name,
|
||||
feeName = warning.feeCurrencyName,
|
||||
feeSymbol = warning.feeCurrencySymbol,
|
||||
networkName = warning.networkName,
|
||||
mergeFeeNetworkName = mergeFeeNetworkName,
|
||||
onClick = currency?.let {
|
||||
{
|
||||
clickIntents.onTokenDetailsClick(
|
||||
userWalletId,
|
||||
currency,
|
||||
)
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
analyticsEventHandler.send(
|
||||
SendAnalyticEvents.NoticeNotEnoughFee(
|
||||
token = warning.currency.symbol,
|
||||
blockchain = warning.networkName,
|
||||
),
|
||||
)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
// workaround for networks that users have misunderstanding
|
||||
private fun CryptoCurrencyStatus.shouldMergeFeeNetworkName(): Boolean {
|
||||
return Blockchain.fromNetworkId(this.currency.network.backendId) == Blockchain.Arbitrum
|
||||
}
|
||||
|
||||
private suspend fun MutableList<SendNotification>.addValidateTransactionNotifications(
|
||||
sendingAmount: BigDecimal,
|
||||
fee: Fee?,
|
||||
state: SendUiState,
|
||||
) {
|
||||
val sendingCurrency = cryptoCurrencyStatusProvider().currency
|
||||
|
||||
validateTransactionUseCase(
|
||||
amount = sendingAmount.convertToSdkAmount(sendingCurrency),
|
||||
fee = fee ?: return,
|
||||
memo = state.recipientState?.memoTextField?.value,
|
||||
destination = requireNotNull(state.recipientState?.addressTextField?.value),
|
||||
userWalletId = userWalletProvider().walletId,
|
||||
network = sendingCurrency.network,
|
||||
).fold(
|
||||
ifLeft = {
|
||||
when (it) {
|
||||
is BlockchainSdkError.Cardano -> addCardanoTransactionValidationError(
|
||||
error = it,
|
||||
sendingCurrency = sendingCurrency,
|
||||
)
|
||||
is BlockchainSdkError.Koinos -> addKoinosTransactionValidationError(error = it)
|
||||
else -> return
|
||||
}
|
||||
},
|
||||
ifRight = {
|
||||
(fee as? Fee.CardanoToken)?.let {
|
||||
add(
|
||||
SendNotification.Cardano.MinAdaValueCharged(
|
||||
tokenName = sendingCurrency.name,
|
||||
minAdaValue = it.minAdaValue.parseBigDecimal(sendingCurrency.decimals),
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun MutableList<SendNotification>.addCardanoTransactionValidationError(
|
||||
error: BlockchainSdkError.Cardano,
|
||||
sendingCurrency: CryptoCurrency,
|
||||
) {
|
||||
when (error) {
|
||||
BlockchainSdkError.Cardano.InsufficientMinAdaBalanceToSendToken -> {
|
||||
add(SendNotification.Cardano.InsufficientBalanceToTransferToken(sendingCurrency.name))
|
||||
}
|
||||
BlockchainSdkError.Cardano.InsufficientRemainingBalanceToWithdrawTokens -> {
|
||||
when (sendingCurrency) {
|
||||
is CryptoCurrency.Coin -> SendNotification.Cardano.InsufficientBalanceToTransferCoin
|
||||
is CryptoCurrency.Token -> {
|
||||
SendNotification.Cardano.InsufficientBalanceToTransferToken(sendingCurrency.name)
|
||||
}
|
||||
}.let(::add)
|
||||
}
|
||||
BlockchainSdkError.Cardano.InsufficientRemainingBalance,
|
||||
BlockchainSdkError.Cardano.InsufficientSendingAdaAmount,
|
||||
-> {
|
||||
val dustValue = currencyChecksRepository.getDustValue(
|
||||
userWalletId = userWalletProvider().walletId,
|
||||
network = sendingCurrency.network,
|
||||
) ?: return
|
||||
|
||||
add(
|
||||
SendNotification.Error.MinimumAmountError(
|
||||
amount = dustValue.parseBigDecimal(sendingCurrency.decimals),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<SendNotification>.addKoinosTransactionValidationError(error: BlockchainSdkError.Koinos) {
|
||||
when (error) {
|
||||
is BlockchainSdkError.Koinos.InsufficientBalance -> {
|
||||
add(SendNotification.Koinos.InsufficientBalance)
|
||||
}
|
||||
is BlockchainSdkError.Koinos.InsufficientMana -> {
|
||||
add(
|
||||
SendNotification.Koinos.InsufficientRecoverableMana(
|
||||
mana = error.manaBalance ?: BigDecimal.ZERO,
|
||||
maxMana = error.maxMana ?: BigDecimal.ZERO,
|
||||
),
|
||||
)
|
||||
}
|
||||
is BlockchainSdkError.Koinos.ManaFeeExceedsBalance -> {
|
||||
add(
|
||||
SendNotification.Koinos.ManaExceedsBalance(
|
||||
availableKoinForTransfer = error.availableKoinForTransfer,
|
||||
onReduceClick = {
|
||||
clickIntents.onAmountReduceClick(
|
||||
reduceAmountTo = error.availableKoinForTransfer,
|
||||
clazz = SendNotification.Koinos.InsufficientRecoverableMana::class.java,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun MutableList<SendNotification>.addDustWarningNotification(
|
||||
feeValue: BigDecimal,
|
||||
sendingAmount: BigDecimal,
|
||||
) {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val dustValue = currencyChecksRepository.getDustValue(
|
||||
userWalletProvider().walletId,
|
||||
cryptoCurrencyStatus.currency.network,
|
||||
) ?: return
|
||||
|
||||
if (checkDustLimits(feeValue, sendingAmount, dustValue)) {
|
||||
add(
|
||||
SendNotification.Error.MinimumAmountError(
|
||||
amount = dustValue.parseBigDecimal(cryptoCurrencyStatus.currency.decimals),
|
||||
),
|
||||
)
|
||||
add(NotificationUM.Warning.TooHigh(diff))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee
|
||||
|
||||
import com.tangem.features.send.impl.presentation.state.SendNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addFeeUnreachableNotification
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiStateType
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
|
|
@ -25,24 +25,10 @@ internal class FeeNotificationFactory(
|
|||
val feeState = state.getFeeState(stateRouterProvider().isEditState) ?: return@map persistentListOf()
|
||||
buildList {
|
||||
addFeeUnreachableNotification(
|
||||
feeState.feeSelectorState,
|
||||
feeError = (feeState.feeSelectorState as? FeeSelectorState.Error)?.error,
|
||||
tokenName = state.cryptoCurrencyName,
|
||||
onReload = clickIntents::feeReload,
|
||||
)
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
private fun MutableList<SendNotification>.addFeeUnreachableNotification(feeSelectorState: FeeSelectorState) {
|
||||
when (feeSelectorState) {
|
||||
is FeeSelectorState.Error.TronAccountActivationError -> add(
|
||||
SendNotification.Warning.TronAccountNotActivated(
|
||||
feeSelectorState.tokenName,
|
||||
),
|
||||
)
|
||||
is FeeSelectorState.Error.NetworkError -> add(
|
||||
SendNotification.Warning.NetworkFeeUnreachable(clickIntents::feeReload),
|
||||
)
|
||||
else -> {
|
||||
/* do nothing */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.send.impl.presentation.state.fee
|
|||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -17,10 +18,9 @@ internal sealed class FeeSelectorState {
|
|||
|
||||
data object Loading : FeeSelectorState()
|
||||
|
||||
sealed class Error : FeeSelectorState() {
|
||||
data object NetworkError : Error()
|
||||
data class TronAccountActivationError(val tokenName: String) : Error()
|
||||
}
|
||||
data class Error(
|
||||
val error: GetFeeError?,
|
||||
) : FeeSelectorState()
|
||||
}
|
||||
|
||||
enum class FeeType {
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@ import com.tangem.blockchain.common.Token
|
|||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.AmountType
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
|
||||
import com.tangem.features.send.impl.presentation.state.SendNotification
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
|
|
@ -102,13 +103,13 @@ internal class FeeStateFactory(
|
|||
)
|
||||
}
|
||||
|
||||
fun onFeeOnErrorState(feeError: FeeSelectorState.Error): SendUiState {
|
||||
fun onFeeOnErrorState(feeError: GetFeeError?): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val isEditState = stateRouterProvider().isEditState
|
||||
return state.copyWrapped(
|
||||
isEditState = isEditState,
|
||||
feeState = state.getFeeState(isEditState)?.copy(
|
||||
feeSelectorState = feeError,
|
||||
feeSelectorState = FeeSelectorState.Error(feeError),
|
||||
),
|
||||
sendState = state.sendState?.copy(
|
||||
isPrimaryButtonEnabled = false,
|
||||
|
|
@ -152,7 +153,7 @@ internal class FeeStateFactory(
|
|||
)
|
||||
}
|
||||
|
||||
fun getFeeNotificationState(notifications: ImmutableList<SendNotification>): SendUiState {
|
||||
fun getFeeNotificationState(notifications: ImmutableList<NotificationUM>): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val isEditState = stateRouterProvider().isEditState
|
||||
val feeState = state.getFeeState(isEditState) ?: return state
|
||||
|
|
@ -167,7 +168,7 @@ internal class FeeStateFactory(
|
|||
|
||||
private fun isPrimaryButtonEnabled(
|
||||
feeState: SendStates.FeeState,
|
||||
notifications: ImmutableList<SendNotification>,
|
||||
notifications: ImmutableList<NotificationUM>,
|
||||
): Boolean {
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return false
|
||||
val customValue = feeSelectorState.customValues.firstOrNull()
|
||||
|
|
@ -178,7 +179,7 @@ internal class FeeStateFactory(
|
|||
} else {
|
||||
false
|
||||
}
|
||||
val noErrors = notifications.none { it is SendNotification.Error }
|
||||
val noErrors = notifications.none { it is NotificationUM.Error }
|
||||
|
||||
return noErrors && (isNotEmptyCustom || isNotCustom)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,6 +103,6 @@ internal object FeeStatePreviewData {
|
|||
)
|
||||
|
||||
val errorFeeState = feeState.copy(
|
||||
feeSelectorState = FeeSelectorState.Error.NetworkError,
|
||||
feeSelectorState = FeeSelectorState.Error(null),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
package com.tangem.features.send.impl.presentation.state.previewdata
|
||||
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource
|
||||
import com.tangem.features.send.impl.presentation.state.SendNotification
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -24,11 +23,12 @@ internal object SendClickIntentsStub : SendClickIntents {
|
|||
|
||||
override fun onFailedTxEmailClick(errorMessage: String) {}
|
||||
|
||||
override fun onTokenDetailsClick(userWalletId: UserWalletId, currency: CryptoCurrency) {}
|
||||
override fun onTokenDetailsClick(currency: CryptoCurrency) {}
|
||||
|
||||
override fun onAmountValueChange(value: String) {}
|
||||
|
||||
override fun onCurrencyChangeClick(isFiat: Boolean) {}
|
||||
|
||||
override fun onAmountNext() {}
|
||||
|
||||
override fun onMaxValueClick() {}
|
||||
|
|
@ -61,13 +61,13 @@ internal object SendClickIntentsStub : SendClickIntents {
|
|||
|
||||
override fun onShareClick() {}
|
||||
|
||||
override fun onAmountReduceClick(
|
||||
reduceAmountBy: BigDecimal?,
|
||||
reduceAmountByDiff: BigDecimal?,
|
||||
reduceAmountTo: BigDecimal?,
|
||||
clazz: Class<out SendNotification>,
|
||||
) {
|
||||
}
|
||||
override fun onAmountReduceByClick(
|
||||
reduceAmountBy: BigDecimal,
|
||||
reduceAmountByDiff: BigDecimal,
|
||||
notification: Class<out NotificationUM>,
|
||||
) {}
|
||||
|
||||
override fun onNotificationCancel(clazz: Class<out SendNotification>) {}
|
||||
override fun onAmountReduceToClick(reduceAmountTo: BigDecimal, notification: Class<out NotificationUM>) {}
|
||||
|
||||
override fun onNotificationCancel(clazz: Class<out NotificationUM>) {}
|
||||
}
|
||||
|
|
@ -5,19 +5,19 @@ import androidx.compose.runtime.*
|
|||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.tangem.common.ui.alerts.models.AlertUM
|
||||
import com.tangem.core.ui.components.BasicDialog
|
||||
import com.tangem.core.ui.components.DialogButtonUM
|
||||
import com.tangem.core.ui.event.EventEffect
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.SendAlertState
|
||||
import com.tangem.features.send.impl.presentation.state.SendEvent
|
||||
|
||||
@Composable
|
||||
internal fun SendEventEffect(event: StateEvent<SendEvent>, snackbarHostState: SnackbarHostState) {
|
||||
val resources = LocalContext.current.resources
|
||||
var alertConfig by remember { mutableStateOf<SendAlertState?>(value = null) }
|
||||
var alertConfig by remember { mutableStateOf<AlertUM?>(value = null) }
|
||||
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
LaunchedEffect(key1 = alertConfig) {
|
||||
|
|
@ -44,7 +44,7 @@ internal fun SendEventEffect(event: StateEvent<SendEvent>, snackbarHostState: Sn
|
|||
}
|
||||
|
||||
@Composable
|
||||
internal fun SendAlert(state: SendAlertState, onDismiss: () -> Unit) {
|
||||
internal fun SendAlert(state: AlertUM, onDismiss: () -> Unit) {
|
||||
val confirmButton: DialogButtonUM
|
||||
val dismissButton: DialogButtonUM?
|
||||
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.presentation.state.SendNotification
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
internal fun LazyListScope.notifications(
|
||||
notifications: ImmutableList<SendNotification>,
|
||||
notifications: ImmutableList<NotificationUM>,
|
||||
modifier: Modifier = Modifier,
|
||||
hasPaddingAbove: Boolean = false,
|
||||
isClickDisabled: Boolean = false,
|
||||
|
|
@ -33,17 +33,18 @@ internal fun LazyListScope.notifications(
|
|||
.padding(top = topPadding)
|
||||
.animateItemPlacement(),
|
||||
containerColor = when (item) {
|
||||
is SendNotification.Error.ExceedsBalance,
|
||||
is SendNotification.Warning.NetworkFeeUnreachable,
|
||||
is SendNotification.Warning.HighFeeError,
|
||||
is NotificationUM.Error.TokenExceedsBalance,
|
||||
is NotificationUM.Warning.NetworkFeeUnreachable,
|
||||
is NotificationUM.Warning.HighFeeError,
|
||||
-> TangemTheme.colors.background.action
|
||||
else -> TangemTheme.colors.button.disabled
|
||||
},
|
||||
iconTint = when (item) {
|
||||
is SendNotification.Error.ExceedsBalance,
|
||||
is SendNotification.Warning,
|
||||
is NotificationUM.Error.TokenExceedsBalance,
|
||||
is NotificationUM.Warning,
|
||||
-> null
|
||||
is SendNotification.Error -> TangemTheme.colors.icon.warning
|
||||
is NotificationUM.Error -> TangemTheme.colors.icon.warning
|
||||
is NotificationUM.Info -> null
|
||||
},
|
||||
isEnabled = !isClickDisabled,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
package com.tangem.features.send.impl.presentation.viewmodel
|
||||
|
||||
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource
|
||||
import com.tangem.features.send.impl.presentation.state.SendNotification
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import java.math.BigDecimal
|
||||
|
||||
|
|
@ -25,7 +24,7 @@ internal interface SendClickIntents : AmountScreenClickIntents {
|
|||
|
||||
fun onFailedTxEmailClick(errorMessage: String)
|
||||
|
||||
fun onTokenDetailsClick(userWalletId: UserWalletId, currency: CryptoCurrency)
|
||||
fun onTokenDetailsClick(currency: CryptoCurrency)
|
||||
|
||||
// region Recipient
|
||||
fun onRecipientAddressValueChange(value: String, type: EnterAddressSource? = null)
|
||||
|
|
@ -58,13 +57,14 @@ internal interface SendClickIntents : AmountScreenClickIntents {
|
|||
|
||||
fun onShareClick()
|
||||
|
||||
fun onAmountReduceClick(
|
||||
reduceAmountBy: BigDecimal? = null,
|
||||
reduceAmountByDiff: BigDecimal? = reduceAmountBy,
|
||||
reduceAmountTo: BigDecimal? = null,
|
||||
clazz: Class<out SendNotification>,
|
||||
fun onAmountReduceByClick(
|
||||
reduceAmountBy: BigDecimal,
|
||||
reduceAmountByDiff: BigDecimal,
|
||||
notification: Class<out NotificationUM>,
|
||||
)
|
||||
|
||||
fun onNotificationCancel(clazz: Class<out SendNotification>)
|
||||
fun onAmountReduceToClick(reduceAmountTo: BigDecimal, notification: Class<out NotificationUM>)
|
||||
|
||||
fun onNotificationCancel(clazz: Class<out NotificationUM>)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -6,29 +6,33 @@ import androidx.lifecycle.*
|
|||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.left
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.bundle.unbundle
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.feedback.GetCardInfoUseCase
|
||||
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.qrscanning.models.SourceType
|
||||
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
|
||||
import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
|
||||
import com.tangem.domain.redux.LegacyAction
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase
|
||||
import com.tangem.domain.settings.NeverShowTapHelpUseCase
|
||||
import com.tangem.domain.tokens.*
|
||||
import com.tangem.domain.tokens.error.CurrencyStatusError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.transaction.error.ValidateAddressError
|
||||
import com.tangem.domain.transaction.usecase.*
|
||||
|
|
@ -47,7 +51,6 @@ import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents
|
|||
import com.tangem.features.send.impl.presentation.analytics.SendScreenSource
|
||||
import com.tangem.features.send.impl.presentation.analytics.utils.SendScreenAnalyticSender
|
||||
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
|
||||
import com.tangem.features.send.impl.presentation.errors.FeeErrorStateMapper
|
||||
import com.tangem.features.send.impl.presentation.state.*
|
||||
import com.tangem.features.send.impl.presentation.state.amount.AmountStateFactory
|
||||
import com.tangem.features.send.impl.presentation.state.confirm.SendNotificationFactory
|
||||
|
|
@ -56,6 +59,7 @@ import com.tangem.features.send.impl.presentation.state.recipient.RecipientSendF
|
|||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.*
|
||||
import com.tangem.utils.extensions.stripZeroPlainString
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
|
@ -83,7 +87,6 @@ internal class SendViewModel @Inject constructor(
|
|||
private val sendTransactionUseCase: SendTransactionUseCase,
|
||||
private val createTransactionUseCase: CreateTransactionUseCase,
|
||||
private val validateWalletAddressUseCase: ValidateWalletAddressUseCase,
|
||||
private val reduxStateHolder: ReduxStateHolder,
|
||||
private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase,
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
|
|
@ -97,9 +100,12 @@ internal class SendViewModel @Inject constructor(
|
|||
private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase,
|
||||
private val isUtxoConsolidationAvailableUseCase: IsUtxoConsolidationAvailableUseCase,
|
||||
private val validateWalletMemoUseCase: ValidateWalletMemoUseCase,
|
||||
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase,
|
||||
private val getCardInfoUseCase: GetCardInfoUseCase,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
@DelayedWork private val coroutineScope: CoroutineScope,
|
||||
validateTransactionUseCase: ValidateTransactionUseCase,
|
||||
currencyChecksRepository: CurrencyChecksRepository,
|
||||
getCurrencyCheckUseCase: GetCurrencyCheckUseCase,
|
||||
isFeeApproximateUseCase: IsFeeApproximateUseCase,
|
||||
getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
|
|
@ -124,8 +130,6 @@ internal class SendViewModel @Inject constructor(
|
|||
var stateRouter: StateRouter by Delegates.notNull()
|
||||
private set
|
||||
|
||||
private val feeErrorHandler = FeeErrorStateMapper()
|
||||
|
||||
private val stateFactory = SendStateFactory(
|
||||
clickIntents = this,
|
||||
stateRouterProvider = Provider { stateRouter },
|
||||
|
|
@ -174,18 +178,18 @@ internal class SendViewModel @Inject constructor(
|
|||
)
|
||||
|
||||
private val sendNotificationFactory = SendNotificationFactory(
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
validateTransactionUseCase = validateTransactionUseCase,
|
||||
getCurrencyCheckUseCase = getCurrencyCheckUseCase,
|
||||
getBalanceNotEnoughForFeeWarningUseCase = getBalanceNotEnoughForFeeWarningUseCase,
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus },
|
||||
currentStateProvider = Provider { uiState.value },
|
||||
userWalletProvider = Provider { userWallet },
|
||||
stateRouterProvider = Provider { stateRouter },
|
||||
isSubtractAvailableProvider = Provider { isAmountSubtractAvailable },
|
||||
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
|
||||
currencyChecksRepository = currencyChecksRepository,
|
||||
clickIntents = this,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
getBalanceNotEnoughForFeeWarningUseCase = getBalanceNotEnoughForFeeWarningUseCase,
|
||||
validateTransactionUseCase = validateTransactionUseCase,
|
||||
userWalletId = userWalletId,
|
||||
)
|
||||
|
||||
private val sendScreenAnalyticSender by lazy(LazyThreadSafetyMode.NONE) {
|
||||
|
|
@ -524,21 +528,34 @@ internal class SendViewModel @Inject constructor(
|
|||
} else {
|
||||
null
|
||||
}
|
||||
reduxStateHolder.dispatch(
|
||||
LegacyAction.SendEmailTransactionFailed(
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
userWalletId = userWalletId,
|
||||
amount = receivingAmount,
|
||||
fee = feeValue,
|
||||
destinationAddress = recipient,
|
||||
|
||||
val amount = receivingAmount?.convertToSdkAmount(cryptoCurrency)
|
||||
|
||||
saveBlockchainErrorUseCase(
|
||||
error = BlockchainErrorInfo(
|
||||
errorMessage = errorMessage,
|
||||
scanResponse = userWallet.scanResponse,
|
||||
blockchainId = cryptoCurrency.network.id.value,
|
||||
derivationPath = cryptoCurrency.network.derivationPath.value,
|
||||
destinationAddress = recipient.orEmpty(),
|
||||
tokenSymbol = if (amount?.type is AmountType.Token) {
|
||||
amount.currencySymbol
|
||||
} else {
|
||||
""
|
||||
},
|
||||
amount = amount?.value?.stripZeroPlainString() ?: "unknown",
|
||||
fee = feeValue?.convertToSdkAmount(cryptoCurrency)
|
||||
?.value?.stripZeroPlainString() ?: "unknown",
|
||||
),
|
||||
)
|
||||
|
||||
val cardInfo = getCardInfoUseCase(userWallet.scanResponse).getOrNull() ?: return
|
||||
|
||||
viewModelScope.launch {
|
||||
sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(cardInfo = cardInfo))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTokenDetailsClick(userWalletId: UserWalletId, currency: CryptoCurrency) =
|
||||
innerRouter.openTokenDetails(userWalletId, currency)
|
||||
override fun onTokenDetailsClick(currency: CryptoCurrency) = innerRouter.openTokenDetails(userWalletId, currency)
|
||||
|
||||
private fun onFeeNext(): Boolean {
|
||||
val feeState = uiState.value.getFeeState(stateRouter.isEditState)
|
||||
|
|
@ -730,7 +747,7 @@ internal class SendViewModel @Inject constructor(
|
|||
private fun onFeeLoadFailed(isShowStatus: Boolean, loadFeeError: GetFeeError?) {
|
||||
if (isShowStatus) {
|
||||
uiState.value = feeStateFactory.onFeeOnErrorState(
|
||||
feeErrorHandler.getFeeError(loadFeeError, cryptoCurrency.name),
|
||||
loadFeeError,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -814,26 +831,27 @@ internal class SendViewModel @Inject constructor(
|
|||
analyticsEventHandler.send(SendAnalyticEvents.ShareButtonClicked)
|
||||
}
|
||||
|
||||
override fun onAmountReduceClick(
|
||||
reduceAmountBy: BigDecimal?,
|
||||
reduceAmountByDiff: BigDecimal?,
|
||||
reduceAmountTo: BigDecimal?,
|
||||
clazz: Class<out SendNotification>,
|
||||
) {
|
||||
uiState.value = when {
|
||||
reduceAmountBy != null && reduceAmountByDiff != null -> amountStateFactory.getOnAmountReduceByState(
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
reduceAmountByDiff = reduceAmountByDiff,
|
||||
)
|
||||
reduceAmountTo != null -> amountStateFactory.getOnAmountReduceToState(reduceAmountTo)
|
||||
else -> return
|
||||
}
|
||||
|
||||
uiState.value = sendNotificationFactory.dismissNotificationState(clazz)
|
||||
override fun onAmountReduceToClick(reduceAmountTo: BigDecimal, notification: Class<out NotificationUM>) {
|
||||
uiState.value = amountStateFactory.getOnAmountReduceToState(reduceAmountTo)
|
||||
uiState.value = sendNotificationFactory.dismissNotificationState(notification)
|
||||
updateNotifications()
|
||||
}
|
||||
|
||||
override fun onNotificationCancel(clazz: Class<out SendNotification>) {
|
||||
override fun onAmountReduceByClick(
|
||||
reduceAmountBy: BigDecimal,
|
||||
reduceAmountByDiff: BigDecimal,
|
||||
notification: Class<out NotificationUM>,
|
||||
) {
|
||||
uiState.value = amountStateFactory.getOnAmountReduceByState(
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
reduceAmountByDiff = reduceAmountByDiff,
|
||||
)
|
||||
|
||||
uiState.value = sendNotificationFactory.dismissNotificationState(notification)
|
||||
updateNotifications()
|
||||
}
|
||||
|
||||
override fun onNotificationCancel(clazz: Class<out NotificationUM>) {
|
||||
uiState.value = sendNotificationFactory.dismissNotificationState(clazz = clazz, isIgnored = true)
|
||||
}
|
||||
|
||||
|
|
@ -972,7 +990,7 @@ internal class SendViewModel @Inject constructor(
|
|||
private fun onCheckFeeUpdate() {
|
||||
val sendState = uiState.value.sendState ?: return
|
||||
val isSuccess = sendState.isSuccess
|
||||
val noErrorNotifications = sendState.notifications.none { it is SendNotification.Error }
|
||||
val noErrorNotifications = sendState.notifications.none { it is NotificationUM.Error }
|
||||
|
||||
if (!isSuccess && noErrorNotifications) {
|
||||
viewModelScope.launch {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.events
|
||||
|
||||
import com.tangem.common.ui.alerts.SendTransactionAlertConverter
|
||||
import com.tangem.common.ui.alerts.TransactionErrorAlertConverter
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStateController
|
||||
|
|
@ -22,7 +22,7 @@ internal class StakingEventFactory(
|
|||
|
||||
fun createSendTransactionErrorAlert(error: SendTransactionError?) {
|
||||
val alert = error?.let {
|
||||
SendTransactionAlertConverter(
|
||||
TransactionErrorAlertConverter(
|
||||
popBackStack = popBackStack,
|
||||
onFailedTxEmailClick = onFailedTxEmailClick,
|
||||
).convert(error)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionSt
|
|||
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.domain.transaction.usecase.SendMultipleTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.staking.impl.analytics.StakingAnalyticsEvents
|
||||
|
|
@ -40,7 +40,7 @@ internal class StakingTransactionSender @AssistedInject constructor(
|
|||
private val stakingBalanceUpdater: StakingBalanceUpdater.Factory,
|
||||
private val getStakingTransactionUseCase: GetStakingTransactionUseCase,
|
||||
private val getConstructedStakingTransactionUseCase: GetConstructedStakingTransactionUseCase,
|
||||
private val sendMultipleTransactionUseCase: SendMultipleTransactionUseCase,
|
||||
private val sendTransactionUseCase: SendTransactionUseCase,
|
||||
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
private val submitHashUseCase: SubmitHashUseCase,
|
||||
private val saveUnsubmittedHashUseCase: SaveUnsubmittedHashUseCase,
|
||||
|
|
@ -210,7 +210,7 @@ internal class StakingTransactionSender @AssistedInject constructor(
|
|||
onSendSuccess: (txUrl: String) -> Unit,
|
||||
onSendError: (SendTransactionError?) -> Unit,
|
||||
) {
|
||||
sendMultipleTransactionUseCase(
|
||||
sendTransactionUseCase(
|
||||
txsData = fullTransactionsData.map { it.tangemTransaction },
|
||||
userWallet = userWallet,
|
||||
network = cryptoCurrencyStatus.currency.network,
|
||||
|
|
|
|||
|
|
@ -136,13 +136,11 @@ internal class AddStakingNotificationsTransformer(
|
|||
val cryptoCurrency = cryptoCurrencyStatus.currency
|
||||
val network = cryptoCurrency.network
|
||||
|
||||
if (feeError != null) {
|
||||
addFeeUnreachableNotification(
|
||||
feeError = feeError,
|
||||
tokenName = cryptoCurrencyStatusProvider().currency.name,
|
||||
onReload = onReload,
|
||||
)
|
||||
}
|
||||
addFeeUnreachableNotification(
|
||||
feeError = feeError,
|
||||
tokenName = cryptoCurrencyStatusProvider().currency.name,
|
||||
onReload = onReload,
|
||||
)
|
||||
addStakeExceedBalanceNotification(
|
||||
feeAmount = feeValue,
|
||||
sendingAmount = sendingAmount,
|
||||
|
|
|
|||
|
|
@ -8,15 +8,17 @@ import com.tangem.core.ui.utils.BigDecimalFormatter
|
|||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.state.*
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import com.tangem.features.staking.impl.presentation.state.utils.getRewardSchedule
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal class SetConfirmationStateLoadingTransformer(
|
||||
private val yield: Yield,
|
||||
private val appCurrency: AppCurrency,
|
||||
private val cryptoCurrency: CryptoCurrency,
|
||||
) : Transformer<StakingUiState> {
|
||||
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
|
|
@ -38,7 +40,7 @@ internal class SetConfirmationStateLoadingTransformer(
|
|||
availableValidators = filteredValidators,
|
||||
),
|
||||
notifications = persistentListOf(),
|
||||
footerText = getFooter(prevState, chosenValidator),
|
||||
footerText = getFooter(prevState),
|
||||
transactionDoneState = TransactionDoneState.Empty,
|
||||
pendingAction = possibleConfirmationState?.pendingAction,
|
||||
pendingActions = possibleConfirmationState?.pendingActions,
|
||||
|
|
@ -49,30 +51,25 @@ internal class SetConfirmationStateLoadingTransformer(
|
|||
)
|
||||
}
|
||||
|
||||
private fun getFooter(state: StakingUiState, validator: Yield.Validator): TextReference {
|
||||
private fun getFooter(state: StakingUiState): TextReference {
|
||||
val amountState = state.amountState as? AmountState.Data
|
||||
|
||||
val isEnterAction = state.actionType == StakingActionCommonType.ENTER
|
||||
|
||||
val apr = validator.apr.orZero()
|
||||
val amountDecimal = amountState?.amountTextField?.fiatAmount?.value
|
||||
val potentialReward = amountDecimal?.multiply(apr)
|
||||
|
||||
val amountValue = BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = amountDecimal,
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
val potentialRewardValue = BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = potentialReward,
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
withApproximateSign = true,
|
||||
val rewardSchedule = getRewardSchedule(
|
||||
yield.metadata.rewardSchedule,
|
||||
cryptoCurrency.network.id.value,
|
||||
)
|
||||
return if (isEnterAction && amountDecimal != null && potentialReward != null) {
|
||||
return if (isEnterAction && amountDecimal != null && rewardSchedule != null) {
|
||||
resourceReference(
|
||||
id = R.string.staking_summary_description_text,
|
||||
formatArgs = wrappedList(amountValue, potentialRewardValue),
|
||||
formatArgs = wrappedList(amountValue, rewardSchedule),
|
||||
)
|
||||
} else {
|
||||
TextReference.EMPTY
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import com.tangem.core.ui.extensions.*
|
|||
import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.core.serialization.SerializedBigDecimal
|
||||
import com.tangem.domain.staking.model.stakekit.BalanceItem
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
|
|
@ -19,6 +18,7 @@ import com.tangem.features.staking.impl.presentation.state.*
|
|||
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
|
||||
import com.tangem.features.staking.impl.presentation.state.converters.RewardsValidatorStateConverter
|
||||
import com.tangem.features.staking.impl.presentation.state.converters.YieldBalancesConverter
|
||||
import com.tangem.features.staking.impl.presentation.state.utils.getRewardSchedule
|
||||
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isPolkadot
|
||||
import com.tangem.utils.Provider
|
||||
|
|
@ -102,20 +102,18 @@ internal class SetInitialDataStateTransformer(
|
|||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
|
||||
return listOfNotNull(
|
||||
createAnnualPercentageRateItem(yield.validators),
|
||||
createAnnualPercentageRateItem(),
|
||||
createAvailableItem(cryptoCurrencyStatus),
|
||||
createUnbondingPeriodItem(yield.metadata.cooldownPeriod?.days),
|
||||
createMinimumRequirementItem(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
minimumCryptoAmount = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum,
|
||||
),
|
||||
createRewardClaimingItem(yield.metadata.rewardClaiming),
|
||||
createWarmupPeriodItem(yield.metadata.warmupPeriod.days),
|
||||
createRewardScheduleItem(yield.metadata.rewardSchedule),
|
||||
createUnbondingPeriodItem(),
|
||||
createMinimumRequirementItem(cryptoCurrencyStatus),
|
||||
createRewardClaimingItem(),
|
||||
createWarmupPeriodItem(),
|
||||
createRewardScheduleItem(),
|
||||
).toPersistentList()
|
||||
}
|
||||
|
||||
private fun createAnnualPercentageRateItem(validators: List<Yield.Validator>): RoundedListWithDividersItemData {
|
||||
private fun createAnnualPercentageRateItem(): RoundedListWithDividersItemData {
|
||||
val validators = yield.validators
|
||||
return RoundedListWithDividersItemData(
|
||||
id = R.string.staking_details_annual_percentage_rate,
|
||||
startText = TextReference.Res(R.string.staking_details_annual_percentage_rate),
|
||||
|
|
@ -140,8 +138,8 @@ internal class SetInitialDataStateTransformer(
|
|||
)
|
||||
}
|
||||
|
||||
private fun createUnbondingPeriodItem(cooldownPeriodDays: Int?): RoundedListWithDividersItemData? {
|
||||
cooldownPeriodDays ?: return null
|
||||
private fun createUnbondingPeriodItem(): RoundedListWithDividersItemData? {
|
||||
val cooldownPeriodDays = yield.metadata.cooldownPeriod?.days ?: return null
|
||||
return RoundedListWithDividersItemData(
|
||||
id = R.string.staking_details_unbonding_period,
|
||||
startText = TextReference.Res(R.string.staking_details_unbonding_period),
|
||||
|
|
@ -156,9 +154,8 @@ internal class SetInitialDataStateTransformer(
|
|||
|
||||
private fun createMinimumRequirementItem(
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
minimumCryptoAmount: SerializedBigDecimal?,
|
||||
): RoundedListWithDividersItemData? {
|
||||
if (minimumCryptoAmount == null) return null
|
||||
val minimumCryptoAmount = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum ?: return null
|
||||
if (!isPolkadot(cryptoCurrencyStatus.currency.network.id.value)) return null
|
||||
|
||||
val formattedAmount = BigDecimalFormatter.formatCryptoAmount(
|
||||
|
|
@ -174,9 +171,8 @@ internal class SetInitialDataStateTransformer(
|
|||
)
|
||||
}
|
||||
|
||||
private fun createRewardClaimingItem(
|
||||
rewardClaiming: Yield.Metadata.RewardClaiming,
|
||||
): RoundedListWithDividersItemData? {
|
||||
private fun createRewardClaimingItem(): RoundedListWithDividersItemData? {
|
||||
val rewardClaiming = yield.metadata.rewardClaiming
|
||||
val endTextId = rewardClaimingResources[rewardClaiming] ?: return null
|
||||
|
||||
return RoundedListWithDividersItemData(
|
||||
|
|
@ -187,7 +183,8 @@ internal class SetInitialDataStateTransformer(
|
|||
)
|
||||
}
|
||||
|
||||
private fun createWarmupPeriodItem(warmupPeriodDays: Int): RoundedListWithDividersItemData? {
|
||||
private fun createWarmupPeriodItem(): RoundedListWithDividersItemData? {
|
||||
val warmupPeriodDays = yield.metadata.warmupPeriod.days
|
||||
if (warmupPeriodDays == 0) return null
|
||||
|
||||
return RoundedListWithDividersItemData(
|
||||
|
|
@ -202,10 +199,8 @@ internal class SetInitialDataStateTransformer(
|
|||
)
|
||||
}
|
||||
|
||||
private fun createRewardScheduleItem(
|
||||
rewardSchedule: Yield.Metadata.RewardSchedule,
|
||||
): RoundedListWithDividersItemData? {
|
||||
val endTextReference = getRewardScheduleText(rewardSchedule) ?: return null
|
||||
private fun createRewardScheduleItem(): RoundedListWithDividersItemData? {
|
||||
val endTextReference = getRewardScheduleText() ?: return null
|
||||
|
||||
return RoundedListWithDividersItemData(
|
||||
id = R.string.staking_details_reward_schedule,
|
||||
|
|
@ -257,17 +252,16 @@ internal class SetInitialDataStateTransformer(
|
|||
return resourceReference(R.string.common_range, wrappedList(formattedMinApr, formattedMaxApr))
|
||||
}
|
||||
|
||||
private fun getRewardScheduleText(rewardSchedule: Yield.Metadata.RewardSchedule): TextReference? {
|
||||
return when (rewardSchedule) {
|
||||
Yield.Metadata.RewardSchedule.BLOCK -> resourceReference(R.string.staking_reward_schedule_block)
|
||||
Yield.Metadata.RewardSchedule.WEEK -> resourceReference(R.string.staking_reward_schedule_week)
|
||||
Yield.Metadata.RewardSchedule.HOUR -> resourceReference(R.string.staking_reward_schedule_hour)
|
||||
Yield.Metadata.RewardSchedule.DAY -> resourceReference(R.string.staking_reward_schedule_each_day)
|
||||
Yield.Metadata.RewardSchedule.MONTH -> resourceReference(R.string.staking_reward_schedule_month)
|
||||
Yield.Metadata.RewardSchedule.ERA -> resourceReference(R.string.staking_reward_schedule_era)
|
||||
Yield.Metadata.RewardSchedule.EPOCH -> resourceReference(R.string.staking_reward_schedule_epoch)
|
||||
Yield.Metadata.RewardSchedule.UNKNOWN -> null
|
||||
}
|
||||
private fun getRewardScheduleText(): TextReference? {
|
||||
val rewardSchedule = getRewardSchedule(
|
||||
yield.metadata.rewardSchedule,
|
||||
cryptoCurrencyStatusProvider().currency.network.id.value,
|
||||
) ?: return null
|
||||
|
||||
return resourceReference(
|
||||
R.string.staking_reward_schedule_each,
|
||||
wrappedList(rewardSchedule),
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.utils
|
||||
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.state.utils.StakingRewardSchedule.COSMOS_SCHEDULE
|
||||
import com.tangem.features.staking.impl.presentation.state.utils.StakingRewardSchedule.SOLANA_SCHEDULE
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isCosmos
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isSolana
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isTron
|
||||
import com.tangem.utils.StringsSigns.MINUS
|
||||
import com.tangem.utils.StringsSigns.NON_BREAKING_SPACE
|
||||
|
||||
private data object StakingRewardSchedule {
|
||||
val COSMOS_SCHEDULE = 5 to 12
|
||||
val SOLANA_SCHEDULE = 2 to 3
|
||||
}
|
||||
|
||||
internal fun getRewardSchedule(schedule: Yield.Metadata.RewardSchedule, networkId: String): TextReference? =
|
||||
when (schedule) {
|
||||
Yield.Metadata.RewardSchedule.BLOCK,
|
||||
Yield.Metadata.RewardSchedule.EPOCH,
|
||||
Yield.Metadata.RewardSchedule.ERA,
|
||||
-> getCustomRewardSchedule(networkId)
|
||||
Yield.Metadata.RewardSchedule.WEEK -> resourceReference(R.string.common_week)
|
||||
Yield.Metadata.RewardSchedule.HOUR -> resourceReference(R.string.common_hour)
|
||||
Yield.Metadata.RewardSchedule.DAY -> pluralReference(R.plurals.common_days_no_param, count = 1)
|
||||
Yield.Metadata.RewardSchedule.MONTH -> resourceReference(R.string.common_month)
|
||||
Yield.Metadata.RewardSchedule.UNKNOWN -> null
|
||||
}
|
||||
|
||||
private fun getCustomRewardSchedule(networkId: String): TextReference? {
|
||||
return when {
|
||||
isSolana(networkId) -> {
|
||||
combinedReference(
|
||||
stringReference("${SOLANA_SCHEDULE.first}$MINUS${SOLANA_SCHEDULE.second}$NON_BREAKING_SPACE"),
|
||||
pluralReference(
|
||||
id = R.plurals.common_days_no_param,
|
||||
count = SOLANA_SCHEDULE.second,
|
||||
),
|
||||
)
|
||||
}
|
||||
isCosmos(networkId) -> {
|
||||
combinedReference(
|
||||
stringReference("${COSMOS_SCHEDULE.first}$MINUS${COSMOS_SCHEDULE.second}$NON_BREAKING_SPACE"),
|
||||
resourceReference(R.string.common_second_no_param),
|
||||
)
|
||||
}
|
||||
isTron(networkId) -> pluralReference(R.plurals.common_days_no_param, count = 1)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
@ -50,29 +50,17 @@ internal fun StakingEventEffect(event: StateEvent<StakingEvent>, snackbarHostSta
|
|||
|
||||
@Composable
|
||||
internal fun StakingAlert(state: AlertUM, onDismiss: () -> Unit) {
|
||||
val confirmButton: DialogButtonUM
|
||||
val dismissButton: DialogButtonUM?
|
||||
|
||||
val onActionClick = state.onConfirmClick
|
||||
if (onActionClick != null) {
|
||||
confirmButton = DialogButtonUM(
|
||||
title = state.confirmButtonText.resolveReference(),
|
||||
onClick = {
|
||||
onActionClick()
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
dismissButton = DialogButtonUM(
|
||||
title = stringResource(id = R.string.common_cancel),
|
||||
onClick = onDismiss,
|
||||
)
|
||||
} else {
|
||||
confirmButton = DialogButtonUM(
|
||||
title = state.confirmButtonText.resolveReference(),
|
||||
onClick = onDismiss,
|
||||
)
|
||||
dismissButton = null
|
||||
}
|
||||
val confirmButton = DialogButtonUM(
|
||||
title = state.confirmButtonText.resolveReference(),
|
||||
onClick = {
|
||||
state.onConfirmClick()
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
val dismissButton = DialogButtonUM(
|
||||
title = stringResource(id = R.string.common_cancel),
|
||||
onClick = onDismiss,
|
||||
)
|
||||
|
||||
BasicDialog(
|
||||
message = state.message.resolveReference(),
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ import com.tangem.core.ui.haptic.VibratorHapticManager
|
|||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.feedback.FeedbackManager
|
||||
import com.tangem.domain.feedback.GetCardInfoUseCase
|
||||
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.staking.InvalidatePendingTransactionsUseCase
|
||||
|
|
@ -92,7 +92,6 @@ internal class StakingViewModel @Inject constructor(
|
|||
private val getAllowanceUseCase: GetAllowanceUseCase,
|
||||
private val isApproveNeededUseCase: IsApproveNeededUseCase,
|
||||
private val vibratorHapticManager: VibratorHapticManager,
|
||||
private val feedbackManager: FeedbackManager,
|
||||
private val getCardInfoUseCase: GetCardInfoUseCase,
|
||||
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase,
|
||||
private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase,
|
||||
|
|
@ -105,6 +104,7 @@ internal class StakingViewModel @Inject constructor(
|
|||
private val stakingFeeTransactionLoader: StakingFeeTransactionLoader.Factory,
|
||||
private val stakingBalanceUpdater: StakingBalanceUpdater.Factory,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
@DelayedWork private val coroutineScope: CoroutineScope,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents {
|
||||
|
|
@ -222,7 +222,13 @@ internal class StakingViewModel @Inject constructor(
|
|||
when {
|
||||
isInitState() -> {
|
||||
stateController.update(SetConfirmationStateResetAssentTransformer)
|
||||
stateController.update(SetConfirmationStateLoadingTransformer(yield, appCurrency))
|
||||
stateController.update(
|
||||
SetConfirmationStateLoadingTransformer(
|
||||
yield = yield,
|
||||
appCurrency = appCurrency,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
),
|
||||
)
|
||||
if (balanceState != null) {
|
||||
stateController.update(SetPossiblePendingTransactionTransformer(balanceState, cryptoCurrencyStatus))
|
||||
}
|
||||
|
|
@ -263,7 +269,13 @@ internal class StakingViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun getFee(pendingAction: PendingAction?, pendingActions: ImmutableList<PendingAction>?) {
|
||||
stateController.update(SetConfirmationStateLoadingTransformer(yield, appCurrency))
|
||||
stateController.update(
|
||||
SetConfirmationStateLoadingTransformer(
|
||||
yield = yield,
|
||||
appCurrency = appCurrency,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
),
|
||||
)
|
||||
viewModelScope.launch {
|
||||
feeLoader.getFee(
|
||||
pendingAction = pendingAction,
|
||||
|
|
@ -766,7 +778,7 @@ internal class StakingViewModel @Inject constructor(
|
|||
unsignedTransactions = transactionsInProgress.map { it.unsignedTransaction },
|
||||
)
|
||||
|
||||
feedbackManager.sendEmail(email)
|
||||
sendFeedbackEmailUseCase(email)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,14 +20,13 @@ import com.tangem.datasource.api.express.models.response.SwapPair
|
|||
import com.tangem.datasource.api.express.models.response.SwapPairsWithProviders
|
||||
import com.tangem.datasource.api.express.models.response.TxDetails
|
||||
import com.tangem.datasource.crypto.DataSignatureVerifier
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.swap.converters.*
|
||||
import com.tangem.feature.swap.domain.api.SwapRepository
|
||||
import com.tangem.feature.swap.domain.models.DataError
|
||||
import com.tangem.feature.swap.domain.models.ExpressDataError
|
||||
import com.tangem.feature.swap.domain.models.ExpressException
|
||||
import com.tangem.feature.swap.domain.models.createFromAmountWithOffset
|
||||
import com.tangem.feature.swap.domain.models.domain.*
|
||||
|
|
@ -196,7 +195,7 @@ internal class DefaultSwapRepository @Inject constructor(
|
|||
toDecimals: Int,
|
||||
providerId: String,
|
||||
rateType: RateType,
|
||||
): Either<DataError, QuoteModel> {
|
||||
): Either<ExpressDataError, QuoteModel> {
|
||||
return withContext(coroutineDispatcher.io) {
|
||||
try {
|
||||
val response = tangemExpressApi.getExchangeQuote(
|
||||
|
|
@ -234,7 +233,7 @@ internal class DefaultSwapRepository @Inject constructor(
|
|||
toAddress: String,
|
||||
refundAddress: String?, // for cex only
|
||||
refundExtraId: String?, // for cex only
|
||||
): Either<DataError, SwapDataModel> {
|
||||
): Either<ExpressDataError, SwapDataModel> {
|
||||
return withContext(coroutineDispatcher.io) {
|
||||
try {
|
||||
val requestId = UUID.randomUUID().toString()
|
||||
|
|
@ -256,12 +255,12 @@ internal class DefaultSwapRepository @Inject constructor(
|
|||
).getOrThrow()
|
||||
if (dataSignatureVerifier.verifySignature(response.signature, response.txDetailsJson)) {
|
||||
val txDetails = parseTxDetails(response.txDetailsJson)
|
||||
?: return@withContext DataError.UnknownError.left()
|
||||
?: return@withContext ExpressDataError.UnknownError.left()
|
||||
if (txDetails.requestId != requestId) {
|
||||
return@withContext DataError.InvalidRequestIdError().left()
|
||||
return@withContext ExpressDataError.InvalidRequestIdError().left()
|
||||
}
|
||||
if (!toAddress.equals(txDetails.payoutAddress, ignoreCase = true)) {
|
||||
return@withContext DataError.InvalidPayoutAddressError().left()
|
||||
return@withContext ExpressDataError.InvalidPayoutAddressError().left()
|
||||
}
|
||||
expressDataConverter.convert(
|
||||
ExchangeDataResponseWithTxDetails(
|
||||
|
|
@ -270,7 +269,7 @@ internal class DefaultSwapRepository @Inject constructor(
|
|||
),
|
||||
).right()
|
||||
} else {
|
||||
DataError.InvalidSignatureError().left()
|
||||
ExpressDataError.InvalidSignatureError().left()
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
getDataError(ex).left()
|
||||
|
|
@ -285,7 +284,7 @@ internal class DefaultSwapRepository @Inject constructor(
|
|||
payInAddress: String,
|
||||
txHash: String,
|
||||
payInExtraId: String?,
|
||||
): Either<DataError, Unit> = withContext(coroutineDispatcher.io) {
|
||||
): Either<ExpressDataError, Unit> = withContext(coroutineDispatcher.io) {
|
||||
try {
|
||||
tangemExpressApi.exchangeSent(
|
||||
ExchangeSentRequestBody(
|
||||
|
|
@ -390,21 +389,20 @@ internal class DefaultSwapRepository @Inject constructor(
|
|||
cryptoCurrencyFactory.createCoin(
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = null,
|
||||
derivationStyleProvider = requireNotNull(
|
||||
scanResponse = requireNotNull(
|
||||
userWalletsListManager
|
||||
.selectedUserWalletSync
|
||||
?.scanResponse
|
||||
?.derivationStyleProvider,
|
||||
?.scanResponse,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getDataError(ex: Exception): DataError {
|
||||
private fun getDataError(ex: Exception): ExpressDataError {
|
||||
return if (ex is ApiResponseError.HttpException) {
|
||||
errorsDataConverter.convert(ex.errorBody ?: "")
|
||||
} else {
|
||||
DataError.UnknownError
|
||||
ExpressDataError.UnknownError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,76 +3,77 @@ package com.tangem.feature.swap.converters
|
|||
import com.squareup.moshi.JsonAdapter
|
||||
import com.tangem.datasource.api.express.models.response.ExpressError
|
||||
import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
|
||||
import com.tangem.feature.swap.domain.models.DataError
|
||||
import com.tangem.feature.swap.domain.models.ExpressDataError
|
||||
import com.tangem.feature.swap.domain.models.createFromAmountWithOffset
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class ErrorsDataConverter(
|
||||
private val jsonAdapter: JsonAdapter<ExpressErrorResponse>,
|
||||
) : Converter<String, DataError> {
|
||||
) : Converter<String, ExpressDataError> {
|
||||
|
||||
@Suppress("MagicNumber", "CyclomaticComplexMethod")
|
||||
override fun convert(value: String): DataError {
|
||||
override fun convert(value: String): ExpressDataError {
|
||||
try {
|
||||
val error = jsonAdapter.fromJson(value)?.error ?: return DataError.UnknownError
|
||||
val error = jsonAdapter.fromJson(value)?.error ?: return ExpressDataError.UnknownError
|
||||
|
||||
return when (error.code) {
|
||||
2010 -> DataError.BadRequest(code = error.code)
|
||||
2200 -> DataError.SwapsAreUnavailableNowError(code = error.code)
|
||||
2210 -> DataError.ExchangeProviderNotFoundError(code = error.code)
|
||||
2220 -> DataError.ExchangeProviderNotActiveError(code = error.code)
|
||||
2230 -> DataError.ExchangeProviderNotAvailableError(code = error.code)
|
||||
2231 -> DataError.ExchangeProviderProviderInternalError(code = error.code)
|
||||
2240 -> DataError.ExchangeNotPossibleError(code = error.code)
|
||||
2010 -> ExpressDataError.BadRequest(code = error.code)
|
||||
2200 -> ExpressDataError.SwapsAreUnavailableNowError(code = error.code)
|
||||
2210 -> ExpressDataError.ExchangeProviderNotFoundError(code = error.code)
|
||||
2220 -> ExpressDataError.ExchangeProviderNotActiveError(code = error.code)
|
||||
2230 -> ExpressDataError.ExchangeProviderNotAvailableError(code = error.code)
|
||||
2231 -> ExpressDataError.ExchangeProviderProviderInternalError(code = error.code)
|
||||
2240 -> ExpressDataError.ExchangeNotPossibleError(code = error.code)
|
||||
2250 -> tryParseExchangeTooSmallAmountError(error = error)
|
||||
2251 -> tryParseExchangeTooBigAmountError(error = error)
|
||||
2260 -> tryParseExchangeNotEnoughAllowanceError(error = error)
|
||||
2270 -> DataError.ExchangeNotEnoughBalanceError(code = error.code)
|
||||
2280 -> DataError.ExchangeInvalidAddressError(code = error.code)
|
||||
2270 -> ExpressDataError.ExchangeNotEnoughBalanceError(code = error.code)
|
||||
2280 -> ExpressDataError.ExchangeInvalidAddressError(code = error.code)
|
||||
2290 -> tryParseExchangeInvalidFromDecimalsError(error = error)
|
||||
else -> DataError.UnknownErrorWithCode(error.code)
|
||||
else -> ExpressDataError.UnknownErrorWithCode(error.code)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return DataError.UnknownError
|
||||
return ExpressDataError.UnknownError
|
||||
}
|
||||
}
|
||||
|
||||
private fun tryParseExchangeTooSmallAmountError(error: ExpressError): DataError {
|
||||
val minAmount = error.value?.minAmount ?: return DataError.UnknownErrorWithCode(error.code)
|
||||
val decimals = error.value?.decimals ?: return DataError.UnknownErrorWithCode(error.code)
|
||||
private fun tryParseExchangeTooSmallAmountError(error: ExpressError): ExpressDataError {
|
||||
val minAmount = error.value?.minAmount ?: return ExpressDataError.UnknownErrorWithCode(error.code)
|
||||
val decimals = error.value?.decimals ?: return ExpressDataError.UnknownErrorWithCode(error.code)
|
||||
|
||||
return DataError.ExchangeTooSmallAmountError(
|
||||
return ExpressDataError.ExchangeTooSmallAmountError(
|
||||
code = error.code,
|
||||
amount = createFromAmountWithOffset(minAmount, decimals),
|
||||
)
|
||||
}
|
||||
|
||||
private fun tryParseExchangeTooBigAmountError(error: ExpressError): DataError {
|
||||
val minAmount = error.value?.maxAmount ?: return DataError.UnknownErrorWithCode(error.code)
|
||||
val decimals = error.value?.decimals ?: return DataError.UnknownErrorWithCode(error.code)
|
||||
private fun tryParseExchangeTooBigAmountError(error: ExpressError): ExpressDataError {
|
||||
val minAmount = error.value?.maxAmount ?: return ExpressDataError.UnknownErrorWithCode(error.code)
|
||||
val decimals = error.value?.decimals ?: return ExpressDataError.UnknownErrorWithCode(error.code)
|
||||
|
||||
return DataError.ExchangeTooBigAmountError(
|
||||
return ExpressDataError.ExchangeTooBigAmountError(
|
||||
code = error.code,
|
||||
amount = createFromAmountWithOffset(minAmount, decimals),
|
||||
)
|
||||
}
|
||||
|
||||
private fun tryParseExchangeNotEnoughAllowanceError(error: ExpressError): DataError {
|
||||
val currentAllowance = error.value?.currentAllowance ?: return DataError.UnknownErrorWithCode(error.code)
|
||||
private fun tryParseExchangeNotEnoughAllowanceError(error: ExpressError): ExpressDataError {
|
||||
val currentAllowance = error.value?.currentAllowance ?: return ExpressDataError.UnknownErrorWithCode(error.code)
|
||||
|
||||
return DataError.ExchangeNotEnoughAllowanceError(
|
||||
return ExpressDataError.ExchangeNotEnoughAllowanceError(
|
||||
code = error.code,
|
||||
currentAllowance = currentAllowance,
|
||||
)
|
||||
}
|
||||
|
||||
private fun tryParseExchangeInvalidFromDecimalsError(error: ExpressError): DataError {
|
||||
val receivedFromDecimals = error.value?.receivedFromDecimals ?: return DataError.UnknownErrorWithCode(
|
||||
private fun tryParseExchangeInvalidFromDecimalsError(error: ExpressError): ExpressDataError {
|
||||
val receivedFromDecimals = error.value?.receivedFromDecimals ?: return ExpressDataError.UnknownErrorWithCode(
|
||||
code = error.code,
|
||||
)
|
||||
val expressFromDecimals = error.value?.expressFromDecimals ?: return DataError.UnknownErrorWithCode(error.code)
|
||||
val expressFromDecimals =
|
||||
error.value?.expressFromDecimals ?: return ExpressDataError.UnknownErrorWithCode(error.code)
|
||||
|
||||
return DataError.ExchangeInvalidFromDecimalsError(
|
||||
return ExpressDataError.ExchangeInvalidFromDecimalsError(
|
||||
code = error.code,
|
||||
receivedFromDecimals = receivedFromDecimals,
|
||||
expressFromDecimals = expressFromDecimals,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package com.tangem.feature.swap.domain.api
|
|||
import arrow.core.Either
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.swap.domain.models.DataError
|
||||
import com.tangem.feature.swap.domain.models.ExpressDataError
|
||||
import com.tangem.feature.swap.domain.models.domain.*
|
||||
import java.math.BigDecimal
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ interface SwapRepository {
|
|||
toDecimals: Int,
|
||||
providerId: String,
|
||||
rateType: RateType,
|
||||
): Either<DataError, QuoteModel>
|
||||
): Either<ExpressDataError, QuoteModel>
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Throws(IllegalStateException::class)
|
||||
|
|
@ -66,7 +66,7 @@ interface SwapRepository {
|
|||
toAddress: String,
|
||||
refundAddress: String? = null, // for cex only
|
||||
refundExtraId: String? = null, // for cex only
|
||||
): Either<DataError, SwapDataModel>
|
||||
): Either<ExpressDataError, SwapDataModel>
|
||||
|
||||
// TODO: Add target error handling, remove either ([REDACTED_JIRA])
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -77,7 +77,7 @@ interface SwapRepository {
|
|||
payInAddress: String,
|
||||
txHash: String,
|
||||
payInExtraId: String?,
|
||||
): Either<DataError, Unit>
|
||||
): Either<ExpressDataError, Unit>
|
||||
|
||||
fun getNativeTokenForNetwork(networkId: String): CryptoCurrency
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ android {
|
|||
dependencies {
|
||||
/** Domain */
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.transaction.models)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.utils)
|
||||
|
|
|
|||
|
|
@ -2,49 +2,52 @@ package com.tangem.feature.swap.domain.models
|
|||
|
||||
import java.math.BigDecimal
|
||||
|
||||
sealed class DataError {
|
||||
sealed class ExpressDataError {
|
||||
|
||||
abstract val code: Int
|
||||
|
||||
data class BadRequest(override val code: Int) : DataError()
|
||||
data class BadRequest(override val code: Int) : ExpressDataError()
|
||||
|
||||
data class SwapsAreUnavailableNowError(override val code: Int) : DataError()
|
||||
data class SwapsAreUnavailableNowError(override val code: Int) : ExpressDataError()
|
||||
|
||||
data class ExchangeProviderNotFoundError(override val code: Int) : DataError()
|
||||
data class ExchangeProviderNotFoundError(override val code: Int) : ExpressDataError()
|
||||
|
||||
data class ExchangeProviderNotActiveError(override val code: Int) : DataError()
|
||||
data class ExchangeProviderNotActiveError(override val code: Int) : ExpressDataError()
|
||||
|
||||
data class ExchangeProviderNotAvailableError(override val code: Int) : DataError()
|
||||
data class ExchangeProviderNotAvailableError(override val code: Int) : ExpressDataError()
|
||||
|
||||
data class ExchangeProviderProviderInternalError(override val code: Int) : DataError()
|
||||
data class ExchangeProviderProviderInternalError(override val code: Int) : ExpressDataError()
|
||||
|
||||
data class ExchangeNotPossibleError(override val code: Int) : DataError()
|
||||
data class ExchangeNotPossibleError(override val code: Int) : ExpressDataError()
|
||||
|
||||
data class ExchangeTooSmallAmountError(override val code: Int, val amount: SwapAmount) : DataError()
|
||||
data class ExchangeTooSmallAmountError(override val code: Int, val amount: SwapAmount) : ExpressDataError()
|
||||
|
||||
data class ExchangeTooBigAmountError(override val code: Int, val amount: SwapAmount) : DataError()
|
||||
data class ExchangeTooBigAmountError(override val code: Int, val amount: SwapAmount) : ExpressDataError()
|
||||
|
||||
data class ExchangeNotEnoughAllowanceError(override val code: Int, val currentAllowance: BigDecimal) : DataError()
|
||||
data class ExchangeNotEnoughAllowanceError(
|
||||
override val code: Int,
|
||||
val currentAllowance: BigDecimal,
|
||||
) : ExpressDataError()
|
||||
|
||||
data class ExchangeNotEnoughBalanceError(override val code: Int) : DataError()
|
||||
data class ExchangeNotEnoughBalanceError(override val code: Int) : ExpressDataError()
|
||||
|
||||
data class ExchangeInvalidAddressError(override val code: Int) : DataError()
|
||||
data class ExchangeInvalidAddressError(override val code: Int) : ExpressDataError()
|
||||
|
||||
data class ExchangeInvalidFromDecimalsError(
|
||||
override val code: Int,
|
||||
val receivedFromDecimals: Int,
|
||||
val expressFromDecimals: Int,
|
||||
) : DataError()
|
||||
) : ExpressDataError()
|
||||
|
||||
data class UnknownErrorWithCode(override val code: Int) : DataError()
|
||||
data class UnknownErrorWithCode(override val code: Int) : ExpressDataError()
|
||||
|
||||
data class InvalidSignatureError(override val code: Int = 990) : DataError()
|
||||
data class InvalidSignatureError(override val code: Int = 990) : ExpressDataError()
|
||||
|
||||
data class InvalidRequestIdError(override val code: Int = 991) : DataError()
|
||||
data class InvalidRequestIdError(override val code: Int = 991) : ExpressDataError()
|
||||
|
||||
data class InvalidPayoutAddressError(override val code: Int = 992) : DataError()
|
||||
data class InvalidPayoutAddressError(override val code: Int = 992) : ExpressDataError()
|
||||
|
||||
data object UnknownError : DataError() {
|
||||
data object UnknownError : ExpressDataError() {
|
||||
override val code: Int = -1
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
package com.tangem.feature.swap.domain.models
|
||||
|
||||
class ExpressException(val dataError: DataError) : Exception()
|
||||
class ExpressException(val expressDataError: ExpressDataError) : Exception()
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.feature.swap.domain.models.ui
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.feature.swap.domain.models.DataError
|
||||
import com.tangem.feature.swap.domain.models.ExpressDataError
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.domain.*
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -34,7 +34,7 @@ sealed interface SwapState {
|
|||
|
||||
data class SwapError(
|
||||
val fromTokenInfo: TokenSwapInfo,
|
||||
val error: DataError,
|
||||
val error: ExpressDataError,
|
||||
val includeFeeInAmount: IncludeFeeInAmount,
|
||||
) : SwapState
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.feature.swap.domain.models.ui
|
||||
|
||||
import com.tangem.feature.swap.domain.models.DataError
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.feature.swap.domain.models.ExpressDataError
|
||||
import java.math.BigDecimal
|
||||
|
||||
sealed class SwapTransactionState {
|
||||
|
|
@ -15,17 +16,13 @@ sealed class SwapTransactionState {
|
|||
val timestamp: Long,
|
||||
) : SwapTransactionState()
|
||||
|
||||
data object UserCancelled : SwapTransactionState()
|
||||
|
||||
data object BlockchainError : SwapTransactionState()
|
||||
|
||||
data object TangemSdkError : SwapTransactionState()
|
||||
|
||||
data object NetworkError : SwapTransactionState()
|
||||
|
||||
data object UnknownError : SwapTransactionState()
|
||||
|
||||
data class ExpressError(val dataError: DataError) : SwapTransactionState()
|
||||
|
||||
data object DemoMode : SwapTransactionState()
|
||||
|
||||
sealed class Error : SwapTransactionState() {
|
||||
data class TransactionError(val error: SendTransactionError?) : Error()
|
||||
|
||||
data class ExpressError(val error: ExpressDataError) : Error()
|
||||
|
||||
data object UnknownError : Error()
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,14 @@ data class TokensDataStateExpress(
|
|||
}
|
||||
}
|
||||
|
||||
fun TokensDataStateExpress.getGroupWithReverse(isReverseFromTo: Boolean): CurrenciesGroup {
|
||||
return if (isReverseFromTo) {
|
||||
this.fromGroup
|
||||
} else {
|
||||
this.toGroup
|
||||
}
|
||||
}
|
||||
|
||||
data class CurrenciesGroup(
|
||||
val available: List<CryptoCurrencySwapInfo>,
|
||||
val unavailable: List<CryptoCurrencySwapInfo>,
|
||||
|
|
|
|||
|
|
@ -2,31 +2,34 @@ package com.tangem.feature.swap.domain
|
|||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress
|
||||
import com.tangem.feature.swap.domain.models.ui.getGroupWithReverse
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class DefaultInitialToCurrencyResolver(
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val swapTransactionRepository: SwapTransactionRepository,
|
||||
) : InitialToCurrencyResolver {
|
||||
|
||||
override suspend fun tryGetFromCache(
|
||||
userWallet: UserWallet,
|
||||
initialCryptoCurrency: CryptoCurrency,
|
||||
state: TokensDataStateExpress,
|
||||
isReverseFromTo: Boolean,
|
||||
): CryptoCurrencyStatus? {
|
||||
val selectedId = getSelectedWalletSyncUseCase().getOrNull() ?: return null
|
||||
val id = swapTransactionRepository.getLastSwappedCryptoCurrencyId(selectedId.walletId) ?: return null
|
||||
val id = swapTransactionRepository.getLastSwappedCryptoCurrencyId(userWallet.walletId) ?: return null
|
||||
|
||||
return if (id != initialCryptoCurrency.id.value) {
|
||||
state.toGroup.available.find { it.currencyStatus.currency.id.value == id }?.currencyStatus
|
||||
val group = state.getGroupWithReverse(isReverseFromTo)
|
||||
group.available.find { it.currencyStatus.currency.id.value == id }?.currencyStatus
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun tryGetWithMaxAmount(state: TokensDataStateExpress): CryptoCurrencyStatus? {
|
||||
return state.toGroup.available.maxByOrNull {
|
||||
override fun tryGetWithMaxAmount(state: TokensDataStateExpress, isReverseFromTo: Boolean): CryptoCurrencyStatus? {
|
||||
val group = state.getGroupWithReverse(isReverseFromTo)
|
||||
return group.available.maxByOrNull {
|
||||
it.currencyStatus.value.fiatAmount ?: BigDecimal.ZERO
|
||||
}?.currencyStatus
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,17 @@ package com.tangem.feature.swap.domain
|
|||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress
|
||||
|
||||
interface InitialToCurrencyResolver {
|
||||
|
||||
suspend fun tryGetFromCache(
|
||||
userWallet: UserWallet,
|
||||
initialCryptoCurrency: CryptoCurrency,
|
||||
state: TokensDataStateExpress,
|
||||
isReverseFromTo: Boolean,
|
||||
): CryptoCurrencyStatus?
|
||||
|
||||
fun tryGetWithMaxAmount(state: TokensDataStateExpress): CryptoCurrencyStatus?
|
||||
fun tryGetWithMaxAmount(state: TokensDataStateExpress, isReverseFromTo: Boolean): CryptoCurrencyStatus?
|
||||
}
|
||||
|
|
@ -79,9 +79,10 @@ interface SwapInteractor {
|
|||
*/
|
||||
fun getTokenBalance(token: CryptoCurrencyStatus): SwapAmount
|
||||
|
||||
suspend fun selectInitialCurrencyToSwap(
|
||||
suspend fun getInitialCurrencyToSwap(
|
||||
initialCryptoCurrency: CryptoCurrency,
|
||||
state: TokensDataStateExpress,
|
||||
isReverseFromTo: Boolean,
|
||||
): CryptoCurrencyStatus?
|
||||
|
||||
fun getNativeToken(networkId: String): CryptoCurrency
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository
|
|||
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
||||
import com.tangem.domain.tokens.repository.QuotesRepository
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.domain.transaction.models.TransactionType
|
||||
import com.tangem.domain.transaction.usecase.*
|
||||
import com.tangem.domain.utils.convertToSdkAmount
|
||||
|
|
@ -31,7 +30,7 @@ import com.tangem.domain.wallets.models.UserWalletId
|
|||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.feature.swap.domain.api.SwapRepository
|
||||
import com.tangem.feature.swap.domain.converters.SwapCurrencyConverter
|
||||
import com.tangem.feature.swap.domain.models.DataError
|
||||
import com.tangem.feature.swap.domain.models.ExpressDataError
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.domain.*
|
||||
import com.tangem.feature.swap.domain.models.toStringWithRightOffset
|
||||
|
|
@ -227,7 +226,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
),
|
||||
).getOrElse {
|
||||
Timber.e(it, "Failed to create approveTransaction")
|
||||
return SwapTransactionState.UnknownError
|
||||
return SwapTransactionState.Error.UnknownError
|
||||
}
|
||||
|
||||
val result = sendTransactionUseCase(
|
||||
|
|
@ -243,16 +242,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
timestamp = System.currentTimeMillis(),
|
||||
)
|
||||
},
|
||||
ifLeft = {
|
||||
when (it) {
|
||||
SendTransactionError.UserCancelledError -> SwapTransactionState.UserCancelled
|
||||
is SendTransactionError.BlockchainSdkError -> SwapTransactionState.BlockchainError
|
||||
is SendTransactionError.TangemSdkError -> SwapTransactionState.TangemSdkError
|
||||
is SendTransactionError.NetworkError -> SwapTransactionState.NetworkError
|
||||
is SendTransactionError.DemoCardError -> SwapTransactionState.DemoMode
|
||||
else -> SwapTransactionState.UnknownError
|
||||
}
|
||||
},
|
||||
ifLeft = { SwapTransactionState.Error.TransactionError(it) },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -591,7 +581,6 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
""".trimIndent(),
|
||||
)
|
||||
|
||||
val userWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: return SwapTransactionState.UnknownError
|
||||
val cardId = userWallet.scanResponse.card.cardId
|
||||
if (isDemoCardUseCase(cardId)) return SwapTransactionState.DemoMode
|
||||
|
||||
|
|
@ -693,12 +682,12 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
hash = dataToSign,
|
||||
).getOrElse {
|
||||
Timber.e(it, "Failed to create swap dex tx data")
|
||||
return SwapTransactionState.UnknownError
|
||||
return SwapTransactionState.Error.UnknownError
|
||||
}
|
||||
|
||||
val result = sendTransactionUseCase(
|
||||
txData = txData,
|
||||
userWallet = getUserWalletUseCase(userWalletId).getOrElse { return SwapTransactionState.UnknownError },
|
||||
userWallet = userWallet,
|
||||
network = currencyToSendStatus.currency.network,
|
||||
)
|
||||
return result.fold(
|
||||
|
|
@ -738,7 +727,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
timestamp = System.currentTimeMillis(),
|
||||
)
|
||||
},
|
||||
ifLeft = { handleSendTxError(it) },
|
||||
ifLeft = { SwapTransactionState.Error.TransactionError(it) },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -773,14 +762,14 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
toAddress = currencyToGet.value.networkAddress?.defaultAddress?.value.orEmpty(),
|
||||
refundAddress = currencyToSend.value.networkAddress?.defaultAddress?.value,
|
||||
refundExtraId = null, // currently always null
|
||||
).getOrElse { return SwapTransactionState.ExpressError(it) }
|
||||
).getOrElse { return SwapTransactionState.Error.ExpressError(it) }
|
||||
|
||||
val exchangeDataCex =
|
||||
exchangeData.transaction as? ExpressTransactionModel.CEX ?: return SwapTransactionState.UnknownError
|
||||
exchangeData.transaction as? ExpressTransactionModel.CEX ?: return SwapTransactionState.Error.UnknownError
|
||||
|
||||
val cardId = userWallet.scanResponse.card.cardId
|
||||
|
||||
if (isDemoCardUseCase(cardId)) return SwapTransactionState.UnknownError
|
||||
if (isDemoCardUseCase(cardId)) return SwapTransactionState.Error.UnknownError
|
||||
|
||||
val txData = createTransactionUseCase(
|
||||
amount = amount.value.convertToSdkAmount(currencyToSend.currency),
|
||||
|
|
@ -794,11 +783,11 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
network = currencyToSend.currency.network,
|
||||
).getOrElse {
|
||||
Timber.e(it, "Failed to create swap CEX tx data")
|
||||
return SwapTransactionState.UnknownError
|
||||
return SwapTransactionState.Error.UnknownError
|
||||
}
|
||||
|
||||
if (txData.extras == null && exchangeDataCex.txExtraId != null) {
|
||||
return SwapTransactionState.UnknownError
|
||||
return SwapTransactionState.Error.UnknownError
|
||||
}
|
||||
|
||||
val result = sendTransactionUseCase(
|
||||
|
|
@ -809,9 +798,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
|
||||
val derivationPath = currencyToSend.currency.network.derivationPath.value
|
||||
return result.fold(
|
||||
ifLeft = {
|
||||
handleSendTxError(it)
|
||||
},
|
||||
ifLeft = { SwapTransactionState.Error.TransactionError(it) },
|
||||
ifRight = { txHash ->
|
||||
repository.exchangeSent(
|
||||
txId = exchangeDataCex.txId,
|
||||
|
|
@ -856,18 +843,6 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun handleSendTxError(txError: SendTransactionError?): SwapTransactionState {
|
||||
return when (txError) {
|
||||
SendTransactionError.UserCancelledError -> SwapTransactionState.UserCancelled
|
||||
is SendTransactionError.BlockchainSdkError -> SwapTransactionState.BlockchainError
|
||||
is SendTransactionError.TangemSdkError -> SwapTransactionState.TangemSdkError
|
||||
is SendTransactionError.NetworkError -> SwapTransactionState.NetworkError
|
||||
is SendTransactionError.DemoCardError -> SwapTransactionState.DemoMode
|
||||
else -> SwapTransactionState.UnknownError
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun getFeeForTransaction(fee: TxFee, blockchain: Blockchain): Fee {
|
||||
val feeAmountValue = fee.feeValue
|
||||
val feeAmount = Amount(
|
||||
|
|
@ -1108,13 +1083,15 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
return SwapAmount(token.value.amount ?: BigDecimal.ZERO, token.currency.decimals)
|
||||
}
|
||||
|
||||
override suspend fun selectInitialCurrencyToSwap(
|
||||
override suspend fun getInitialCurrencyToSwap(
|
||||
initialCryptoCurrency: CryptoCurrency,
|
||||
state: TokensDataStateExpress,
|
||||
isReverseFromTo: Boolean,
|
||||
): CryptoCurrencyStatus? {
|
||||
return initialToCurrencyResolver.tryGetFromCache(initialCryptoCurrency, state)
|
||||
?: initialToCurrencyResolver.tryGetWithMaxAmount(state)
|
||||
?: state.toGroup.available.firstOrNull()?.currencyStatus
|
||||
val group = state.getGroupWithReverse(isReverseFromTo)
|
||||
return initialToCurrencyResolver.tryGetFromCache(userWallet, initialCryptoCurrency, state, isReverseFromTo)
|
||||
?: initialToCurrencyResolver.tryGetWithMaxAmount(state, isReverseFromTo)
|
||||
?: group.available.firstOrNull()?.currencyStatus
|
||||
}
|
||||
|
||||
override fun getNativeToken(networkId: String): CryptoCurrency {
|
||||
|
|
@ -1222,7 +1199,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
@Suppress("LongMethod")
|
||||
private suspend fun getQuotesState(
|
||||
provider: SwapProvider,
|
||||
quoteDataModel: Either<DataError, QuoteModel>,
|
||||
quoteDataModel: Either<ExpressDataError, QuoteModel>,
|
||||
amount: SwapAmount,
|
||||
fromToken: CryptoCurrencyStatus,
|
||||
toToken: CryptoCurrencyStatus,
|
||||
|
|
@ -1301,7 +1278,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
fromToken = fromToken,
|
||||
amount = amount,
|
||||
includeFeeInAmount = includeFeeInAmount,
|
||||
dataError = error,
|
||||
expressDataError = error,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -1311,7 +1288,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
fromToken: CryptoCurrencyStatus,
|
||||
amount: SwapAmount,
|
||||
includeFeeInAmount: IncludeFeeInAmount,
|
||||
dataError: DataError,
|
||||
expressDataError: ExpressDataError,
|
||||
): SwapState.SwapError {
|
||||
val rates = getQuotes(fromToken.currency.id)
|
||||
val fromTokenSwapInfo = TokenSwapInfo(
|
||||
|
|
@ -1320,7 +1297,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
?: BigDecimal.ZERO,
|
||||
cryptoCurrencyStatus = fromToken,
|
||||
)
|
||||
return SwapState.SwapError(fromTokenSwapInfo, dataError, includeFeeInAmount)
|
||||
return SwapState.SwapError(fromTokenSwapInfo, expressDataError, includeFeeInAmount)
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
|
|
@ -1667,7 +1644,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
fromToken = fromTokenStatus,
|
||||
amount = swapAmount,
|
||||
includeFeeInAmount = IncludeFeeInAmount.Excluded,
|
||||
dataError = DataError.UnknownError,
|
||||
expressDataError = ExpressDataError.UnknownError,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase
|
|||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.NetworksRepository
|
||||
import com.tangem.domain.tokens.repository.QuotesRepository
|
||||
import com.tangem.domain.transaction.usecase.*
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.feature.swap.domain.*
|
||||
import com.tangem.lib.crypto.TransactionManager
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -60,11 +58,9 @@ internal class SwapDomainModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
fun provideInitialToCurrencyResolver(
|
||||
getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
swapTransactionRepository: SwapTransactionRepository,
|
||||
): InitialToCurrencyResolver {
|
||||
return DefaultInitialToCurrencyResolver(
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
swapTransactionRepository = swapTransactionRepository,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ dependencies {
|
|||
implementation(projects.core.decompose) // For Route supertype
|
||||
|
||||
/** Domain modules **/
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.appCurrency)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
implementation(projects.domain.balanceHiding)
|
||||
|
|
@ -31,13 +32,17 @@ dependencies {
|
|||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.transaction)
|
||||
implementation(projects.domain.transaction.models)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.settings)
|
||||
implementation(projects.domain.staking)
|
||||
implementation(projects.domain.feedback)
|
||||
|
||||
/** Feature modules */
|
||||
implementation(projects.features.swap.domain)
|
||||
implementation(projects.features.swap.domain.api)
|
||||
implementation(projects.features.swap.domain.models)
|
||||
implementation(projects.domain.staking)
|
||||
|
||||
/** AndroidX */
|
||||
implementation(deps.androidx.activity.compose)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.feature.swap.converters
|
||||
|
||||
import com.tangem.common.ui.alerts.TransactionErrorAlertConverter
|
||||
import com.tangem.common.ui.alerts.models.AlertUM
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.feature.swap.domain.models.ui.SwapTransactionState
|
||||
import com.tangem.feature.swap.models.SwapAlertUM
|
||||
import com.tangem.feature.swap.utils.getExpressErrorMessage
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class SwapTransactionErrorStateConverter(
|
||||
private val onDismiss: () -> Unit,
|
||||
private val onSupportClick: (String) -> Unit,
|
||||
) : Converter<SwapTransactionState.Error, AlertUM?> {
|
||||
override fun convert(value: SwapTransactionState.Error): AlertUM? {
|
||||
return when (value) {
|
||||
is SwapTransactionState.Error.TransactionError -> {
|
||||
when (val error = value.error) {
|
||||
is SendTransactionError.UserCancelledError -> return null
|
||||
null -> SwapAlertUM.GenericError(onDismiss)
|
||||
else -> TransactionErrorAlertConverter(onDismiss, onSupportClick).convert(error)
|
||||
}
|
||||
}
|
||||
is SwapTransactionState.Error.ExpressError -> {
|
||||
SwapAlertUM.ExpressErrorAlert(
|
||||
message = getExpressErrorMessage(value.error),
|
||||
onConfirmClick = { onSupportClick(value.error.code.toString()) },
|
||||
)
|
||||
}
|
||||
SwapTransactionState.Error.UnknownError -> SwapAlertUM.GenericError(onDismiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.feature.swap.models
|
||||
|
||||
import com.tangem.common.ui.alerts.models.AlertUM
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
|
||||
sealed class SwapAlertUM : AlertUM {
|
||||
|
||||
data class GenericError(
|
||||
override val onConfirmClick: (() -> Unit),
|
||||
override val message: TextReference = resourceReference(R.string.common_unknown_error),
|
||||
) : SwapAlertUM() {
|
||||
override val title: TextReference? = null
|
||||
override val confirmButtonText: TextReference =
|
||||
resourceReference(id = R.string.common_support)
|
||||
}
|
||||
|
||||
data class ExpressErrorAlert(
|
||||
override val message: TextReference = resourceReference(R.string.common_unknown_error),
|
||||
override val onConfirmClick: (() -> Unit),
|
||||
) : SwapAlertUM() {
|
||||
override val title: TextReference? = null
|
||||
override val confirmButtonText: TextReference =
|
||||
resourceReference(id = R.string.common_support)
|
||||
}
|
||||
|
||||
data class FeesAlert(
|
||||
override val message: TextReference,
|
||||
override val onConfirmClick: (() -> Unit),
|
||||
) : SwapAlertUM() {
|
||||
override val title: TextReference = resourceReference(
|
||||
com.tangem.feature.swap.presentation.R.string.swapping_alert_title,
|
||||
)
|
||||
override val confirmButtonText: TextReference =
|
||||
resourceReference(id = R.string.common_ok)
|
||||
}
|
||||
}
|
||||
|
|
@ -6,18 +6,21 @@ import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState
|
|||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.feature.swap.domain.models.ui.PriceImpact
|
||||
import com.tangem.feature.swap.models.states.FeeItemState
|
||||
import com.tangem.feature.swap.models.states.ProviderState
|
||||
import com.tangem.feature.swap.models.states.events.SwapEvent
|
||||
|
||||
data class SwapStateHolder(
|
||||
internal data class SwapStateHolder(
|
||||
val sendCardData: SwapCardState,
|
||||
val receiveCardData: SwapCardState,
|
||||
val blockchainId: String, // not the same as networkId, its local id in app
|
||||
val warnings: List<SwapWarning> = emptyList(),
|
||||
val alert: SwapWarning.GenericWarning? = null,
|
||||
val event: StateEvent<SwapEvent> = consumedEvent(),
|
||||
val changeCardsButtonState: ChangeCardsButtonState = ChangeCardsButtonState.ENABLED,
|
||||
val providerState: ProviderState,
|
||||
|
||||
|
|
@ -110,7 +113,6 @@ sealed interface SwapWarning {
|
|||
data class GenericWarning(
|
||||
val title: TextReference? = null,
|
||||
val message: TextReference? = null,
|
||||
val type: GenericWarningType = GenericWarningType.OTHER,
|
||||
val onClick: () -> Unit,
|
||||
) : SwapWarning
|
||||
|
||||
|
|
@ -133,10 +135,6 @@ sealed interface SwapWarning {
|
|||
}
|
||||
}
|
||||
|
||||
enum class GenericWarningType {
|
||||
NETWORK, OTHER
|
||||
}
|
||||
|
||||
enum class ChangeCardsButtonState {
|
||||
ENABLED, DISABLED, UPDATE_IN_PROGRESS
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.feature.swap.models.states.events
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.alerts.models.AlertUM
|
||||
|
||||
@Immutable
|
||||
internal sealed class SwapEvent {
|
||||
data class ShowAlert(val alert: AlertUM) : SwapEvent()
|
||||
|
||||
data class ShowShareDialog(val txUrl: String) : SwapEvent()
|
||||
}
|
||||
|
|
@ -2,24 +2,31 @@ package com.tangem.feature.swap.ui
|
|||
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import com.tangem.common.ui.alerts.models.AlertDemoModeUM
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.*
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.event.triggeredEvent
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.feature.swap.converters.SwapTransactionErrorStateConverter
|
||||
import com.tangem.feature.swap.converters.TokensDataConverter
|
||||
import com.tangem.feature.swap.domain.models.DataError
|
||||
import com.tangem.feature.swap.domain.models.ExpressDataError
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.domain.*
|
||||
import com.tangem.feature.swap.domain.models.formatToUIRepresentation
|
||||
import com.tangem.feature.swap.domain.models.ui.*
|
||||
import com.tangem.feature.swap.models.*
|
||||
import com.tangem.feature.swap.models.states.*
|
||||
import com.tangem.feature.swap.models.states.events.SwapEvent
|
||||
import com.tangem.feature.swap.presentation.R
|
||||
import com.tangem.feature.swap.utils.getExpressErrorMessage
|
||||
import com.tangem.feature.swap.utils.getExpressErrorTitle
|
||||
import com.tangem.feature.swap.viewmodels.SwapProcessDataState
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.StringsSigns.DASH_SIGN
|
||||
|
|
@ -625,13 +632,13 @@ internal class StateBuilder(
|
|||
fromToken: TokenSwapInfo,
|
||||
toToken: CryptoCurrencyStatus?,
|
||||
includeFeeInAmount: IncludeFeeInAmount,
|
||||
dataError: DataError,
|
||||
expressDataError: ExpressDataError,
|
||||
isReverseSwapPossible: Boolean,
|
||||
): SwapStateHolder {
|
||||
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
|
||||
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
|
||||
val warnings = mutableListOf<SwapWarning>()
|
||||
warnings.add(getWarningForError(dataError, fromToken.cryptoCurrencyStatus.currency))
|
||||
warnings.add(getWarningForError(expressDataError, fromToken.cryptoCurrencyStatus.currency))
|
||||
if (includeFeeInAmount is IncludeFeeInAmount.Included && uiStateHolder.fee is FeeItemState.Content) {
|
||||
val feeCoverageNotification = createNetworkFeeCoverageNotificationConfig(
|
||||
uiStateHolder.fee.amountCrypto,
|
||||
|
|
@ -642,7 +649,7 @@ internal class StateBuilder(
|
|||
val providerState = getProviderStateForError(
|
||||
swapProvider = swapProvider,
|
||||
fromToken = fromToken.cryptoCurrencyStatus.currency,
|
||||
dataError = dataError,
|
||||
expressDataError = expressDataError,
|
||||
onProviderClick = actions.onProviderClick,
|
||||
selectionType = ProviderState.SelectionType.CLICK,
|
||||
)
|
||||
|
|
@ -698,28 +705,28 @@ internal class StateBuilder(
|
|||
private fun getProviderStateForError(
|
||||
swapProvider: SwapProvider,
|
||||
fromToken: CryptoCurrency,
|
||||
dataError: DataError,
|
||||
expressDataError: ExpressDataError,
|
||||
onProviderClick: (String) -> Unit,
|
||||
selectionType: ProviderState.SelectionType,
|
||||
): ProviderState {
|
||||
return when (dataError) {
|
||||
is DataError.ExchangeTooSmallAmountError -> {
|
||||
return when (expressDataError) {
|
||||
is ExpressDataError.ExchangeTooSmallAmountError -> {
|
||||
swapProvider.convertToAvailableFromProviderState(
|
||||
swapProvider = swapProvider,
|
||||
alertText = resourceReference(
|
||||
R.string.express_provider_min_amount,
|
||||
wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)),
|
||||
wrappedList(expressDataError.amount.getFormattedCryptoAmount(fromToken)),
|
||||
),
|
||||
selectionType = selectionType,
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
}
|
||||
is DataError.ExchangeTooBigAmountError -> {
|
||||
is ExpressDataError.ExchangeTooBigAmountError -> {
|
||||
swapProvider.convertToAvailableFromProviderState(
|
||||
swapProvider = swapProvider,
|
||||
alertText = resourceReference(
|
||||
R.string.express_provider_max_amount,
|
||||
wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)),
|
||||
wrappedList(expressDataError.amount.getFormattedCryptoAmount(fromToken)),
|
||||
),
|
||||
selectionType = selectionType,
|
||||
onProviderClick = onProviderClick,
|
||||
|
|
@ -731,25 +738,25 @@ internal class StateBuilder(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getWarningForError(dataError: DataError, fromToken: CryptoCurrency): SwapWarning {
|
||||
val providerErrorMessage = getProviderErrorMessage(dataError)
|
||||
val providerErrorTitle = getProviderErrorTitle(dataError)
|
||||
return when (dataError) {
|
||||
is DataError.ExchangeTooSmallAmountError -> SwapWarning.GeneralError(
|
||||
private fun getWarningForError(expressDataError: ExpressDataError, fromToken: CryptoCurrency): SwapWarning {
|
||||
val providerErrorMessage = getExpressErrorMessage(expressDataError)
|
||||
val providerErrorTitle = getExpressErrorTitle(expressDataError)
|
||||
return when (expressDataError) {
|
||||
is ExpressDataError.ExchangeTooSmallAmountError -> SwapWarning.GeneralError(
|
||||
notificationConfig = NotificationConfig(
|
||||
title = resourceReference(
|
||||
id = R.string.warning_express_too_minimal_amount_title,
|
||||
formatArgs = wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)),
|
||||
formatArgs = wrappedList(expressDataError.amount.getFormattedCryptoAmount(fromToken)),
|
||||
),
|
||||
subtitle = resourceReference(R.string.warning_express_wrong_amount_description),
|
||||
iconResId = R.drawable.ic_alert_circle_24,
|
||||
),
|
||||
)
|
||||
is DataError.ExchangeTooBigAmountError -> SwapWarning.GeneralError(
|
||||
is ExpressDataError.ExchangeTooBigAmountError -> SwapWarning.GeneralError(
|
||||
notificationConfig = NotificationConfig(
|
||||
title = resourceReference(
|
||||
id = R.string.warning_express_too_maximum_amount_title,
|
||||
formatArgs = wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)),
|
||||
formatArgs = wrappedList(expressDataError.amount.getFormattedCryptoAmount(fromToken)),
|
||||
),
|
||||
subtitle = resourceReference(R.string.warning_express_wrong_amount_description),
|
||||
iconResId = R.drawable.ic_alert_circle_24,
|
||||
|
|
@ -1045,81 +1052,43 @@ internal class StateBuilder(
|
|||
)
|
||||
}
|
||||
|
||||
fun createErrorTransaction(
|
||||
fun createErrorTransactionAlert(
|
||||
uiState: SwapStateHolder,
|
||||
swapTransactionState: SwapTransactionState,
|
||||
onAlertClick: () -> Unit,
|
||||
error: SwapTransactionState.Error,
|
||||
onDismiss: () -> Unit,
|
||||
onSupportClick: (String) -> Unit,
|
||||
): SwapStateHolder {
|
||||
val errorAlert = SwapTransactionErrorStateConverter(
|
||||
onSupportClick = onSupportClick,
|
||||
onDismiss = onDismiss,
|
||||
).convert(error)
|
||||
return uiState.copy(
|
||||
alert = SwapWarning.GenericWarning(
|
||||
message = if (swapTransactionState is SwapTransactionState.ExpressError) {
|
||||
getProviderErrorMessage(swapTransactionState.dataError)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
onClick = onAlertClick,
|
||||
type = if (swapTransactionState is SwapTransactionState.NetworkError) {
|
||||
GenericWarningType.NETWORK
|
||||
} else {
|
||||
GenericWarningType.OTHER
|
||||
},
|
||||
),
|
||||
event = errorAlert?.let {
|
||||
triggeredEvent(
|
||||
data = SwapEvent.ShowAlert(errorAlert),
|
||||
onConsume = onDismiss,
|
||||
)
|
||||
} ?: consumedEvent(),
|
||||
changeCardsButtonState = ChangeCardsButtonState.ENABLED,
|
||||
)
|
||||
}
|
||||
|
||||
fun createDemoModeAlert(uiState: SwapStateHolder, onAlertClick: () -> Unit): SwapStateHolder {
|
||||
fun createDemoModeAlert(uiState: SwapStateHolder, onDismiss: () -> Unit): SwapStateHolder {
|
||||
return uiState.copy(
|
||||
alert = SwapWarning.GenericWarning(
|
||||
title = resourceReference(id = R.string.warning_demo_mode_title),
|
||||
message = resourceReference(id = R.string.warning_demo_mode_message),
|
||||
onClick = onAlertClick,
|
||||
type = GenericWarningType.OTHER,
|
||||
event = triggeredEvent(
|
||||
data = SwapEvent.ShowAlert(AlertDemoModeUM(onDismiss)),
|
||||
onConsume = onDismiss,
|
||||
),
|
||||
changeCardsButtonState = ChangeCardsButtonState.ENABLED,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getProviderErrorMessage(dataError: DataError): TextReference {
|
||||
return when (dataError) {
|
||||
is DataError.SwapsAreUnavailableNowError -> resourceReference(
|
||||
id = R.string.express_error_swap_unavailable,
|
||||
formatArgs = wrappedList(dataError.code),
|
||||
)
|
||||
is DataError.ExchangeNotPossibleError -> resourceReference(
|
||||
id = R.string.warning_express_pair_unavailable_message,
|
||||
formatArgs = wrappedList(dataError.code),
|
||||
)
|
||||
is DataError.UnknownError -> resourceReference(R.string.common_unknown_error)
|
||||
is DataError.ExchangeProviderNotActiveError,
|
||||
is DataError.ExchangeProviderNotFoundError,
|
||||
is DataError.ExchangeProviderNotAvailableError,
|
||||
is DataError.ExchangeProviderProviderInternalError,
|
||||
-> resourceReference(
|
||||
id = R.string.express_error_swap_pair_unavailable,
|
||||
formatArgs = wrappedList(dataError.code),
|
||||
)
|
||||
else -> resourceReference(R.string.express_error_code, wrappedList(dataError.code.toString()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getProviderErrorTitle(dataError: DataError): TextReference {
|
||||
return when (dataError) {
|
||||
is DataError.ExchangeNotPossibleError -> resourceReference(
|
||||
id = R.string.warning_express_pair_unavailable_title,
|
||||
formatArgs = wrappedList(dataError.code),
|
||||
)
|
||||
is DataError.UnknownError -> resourceReference(R.string.common_error)
|
||||
else -> resourceReference(R.string.warning_express_refresh_required_title)
|
||||
}
|
||||
}
|
||||
|
||||
fun createAlert(
|
||||
uiState: SwapStateHolder,
|
||||
isPriceImpact: Boolean,
|
||||
token: String,
|
||||
providerType: ExchangeProviderType,
|
||||
onAlertClick: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
): SwapStateHolder {
|
||||
val message = when (providerType) {
|
||||
ExchangeProviderType.CEX -> resourceReference(R.string.swapping_alert_cex_description, wrappedList(token))
|
||||
|
|
@ -1136,29 +1105,38 @@ internal class StateBuilder(
|
|||
}
|
||||
}
|
||||
return uiState.copy(
|
||||
alert = SwapWarning.GenericWarning(
|
||||
title = resourceReference(R.string.swapping_alert_title),
|
||||
message = message,
|
||||
onClick = onAlertClick,
|
||||
type = GenericWarningType.OTHER,
|
||||
event = triggeredEvent(
|
||||
SwapEvent.ShowAlert(
|
||||
SwapAlertUM.FeesAlert(
|
||||
message = message,
|
||||
onConfirmClick = onDismiss,
|
||||
),
|
||||
),
|
||||
onConsume = onDismiss,
|
||||
),
|
||||
changeCardsButtonState = ChangeCardsButtonState.ENABLED,
|
||||
)
|
||||
}
|
||||
|
||||
fun addAlert(uiState: SwapStateHolder, message: TextReference? = null, onClick: () -> Unit): SwapStateHolder {
|
||||
fun addAlert(
|
||||
uiState: SwapStateHolder,
|
||||
message: TextReference = resourceReference(R.string.common_unknown_error),
|
||||
onDismiss: () -> Unit = { clearAlert(uiState) },
|
||||
): SwapStateHolder {
|
||||
return uiState.copy(
|
||||
alert = SwapWarning.GenericWarning(
|
||||
message = message,
|
||||
onClick = onClick,
|
||||
event = triggeredEvent(
|
||||
SwapEvent.ShowAlert(
|
||||
SwapAlertUM.GenericError(onDismiss, message),
|
||||
),
|
||||
onConsume = onDismiss,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun clearAlert(uiState: SwapStateHolder): SwapStateHolder = uiState.copy(alert = null)
|
||||
fun clearAlert(uiState: SwapStateHolder): SwapStateHolder = uiState.copy(event = consumedEvent())
|
||||
|
||||
fun addWarning(uiState: SwapStateHolder, message: TextReference?, onClick: () -> Unit): SwapStateHolder {
|
||||
val renewWarnings = uiState.warnings.filterNot { it is SwapWarning.GenericWarning }.toMutableList()
|
||||
val renewWarnings = uiState.warnings.toMutableList()
|
||||
renewWarnings.add(
|
||||
SwapWarning.GenericWarning(
|
||||
message = message,
|
||||
|
|
@ -1421,7 +1399,7 @@ internal class StateBuilder(
|
|||
is SwapState.SwapError -> getProviderStateForError(
|
||||
swapProvider = provider,
|
||||
fromToken = state.fromTokenInfo.cryptoCurrencyStatus.currency,
|
||||
dataError = state.error,
|
||||
expressDataError = state.error,
|
||||
onProviderClick = onProviderSelect,
|
||||
selectionType = ProviderState.SelectionType.SELECT,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
package com.tangem.feature.swap.ui
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.tangem.common.ui.alerts.models.AlertUM
|
||||
import com.tangem.core.ui.components.BasicDialog
|
||||
import com.tangem.core.ui.components.DialogButtonUM
|
||||
import com.tangem.core.ui.event.EventEffect
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.shareText
|
||||
import com.tangem.feature.swap.models.states.events.SwapEvent
|
||||
import com.tangem.feature.swap.presentation.R
|
||||
|
||||
@Composable
|
||||
internal fun SwapEventEffect(event: StateEvent<SwapEvent>) {
|
||||
val context = LocalContext.current
|
||||
var alertConfig by remember { mutableStateOf<AlertUM?>(value = null) }
|
||||
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
LaunchedEffect(key1 = alertConfig) {
|
||||
keyboardController?.hide()
|
||||
}
|
||||
|
||||
alertConfig?.let {
|
||||
SwapAlert(state = it, onDismiss = { alertConfig = null })
|
||||
}
|
||||
|
||||
EventEffect(
|
||||
event = event,
|
||||
onTrigger = { value ->
|
||||
when (value) {
|
||||
is SwapEvent.ShowAlert -> {
|
||||
alertConfig = value.alert
|
||||
}
|
||||
is SwapEvent.ShowShareDialog -> {
|
||||
context.shareText(value.txUrl)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun SwapAlert(state: AlertUM, onDismiss: () -> Unit) {
|
||||
val confirmButton = DialogButtonUM(
|
||||
title = state.confirmButtonText.resolveReference(),
|
||||
onClick = {
|
||||
state.onConfirmClick()
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
val dismissButton = DialogButtonUM(
|
||||
title = stringResource(id = R.string.common_cancel),
|
||||
onClick = onDismiss,
|
||||
)
|
||||
|
||||
BasicDialog(
|
||||
message = state.message.resolveReference(),
|
||||
confirmButton = confirmButton,
|
||||
onDismissDialog = onDismiss,
|
||||
title = state.title?.resolveReference(),
|
||||
dismissButton = dismissButton,
|
||||
)
|
||||
}
|
||||
|
|
@ -101,22 +101,9 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi
|
|||
)
|
||||
}
|
||||
|
||||
if (state.alert != null) {
|
||||
val message = if (state.alert.type == GenericWarningType.NETWORK) {
|
||||
stringResource(id = R.string.disclaimer_error_loading)
|
||||
} else {
|
||||
state.alert.message?.resolveReference() ?: stringResource(id = R.string.common_unknown_error)
|
||||
}
|
||||
BasicDialog(
|
||||
title = state.alert.title?.resolveReference(),
|
||||
message = message,
|
||||
confirmButton = DialogButtonUM(
|
||||
title = stringResource(id = R.string.common_ok),
|
||||
onClick = state.alert.onClick,
|
||||
),
|
||||
onDismissDialog = state.alert.onClick,
|
||||
)
|
||||
}
|
||||
SwapEventEffect(
|
||||
event = state.event,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.feature.swap.utils
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.feature.swap.domain.models.ExpressDataError
|
||||
import com.tangem.feature.swap.presentation.R
|
||||
|
||||
internal fun getExpressErrorMessage(expressDataError: ExpressDataError): TextReference {
|
||||
return when (expressDataError) {
|
||||
is ExpressDataError.SwapsAreUnavailableNowError -> resourceReference(
|
||||
id = R.string.express_error_swap_unavailable,
|
||||
formatArgs = wrappedList(expressDataError.code),
|
||||
)
|
||||
is ExpressDataError.ExchangeNotPossibleError -> resourceReference(
|
||||
id = R.string.warning_express_pair_unavailable_message,
|
||||
formatArgs = wrappedList(expressDataError.code),
|
||||
)
|
||||
is ExpressDataError.UnknownError -> resourceReference(R.string.common_unknown_error)
|
||||
is ExpressDataError.ExchangeProviderNotActiveError,
|
||||
is ExpressDataError.ExchangeProviderNotFoundError,
|
||||
is ExpressDataError.ExchangeProviderNotAvailableError,
|
||||
is ExpressDataError.ExchangeProviderProviderInternalError,
|
||||
-> resourceReference(
|
||||
id = R.string.express_error_swap_pair_unavailable,
|
||||
formatArgs = wrappedList(expressDataError.code),
|
||||
)
|
||||
else -> resourceReference(R.string.express_error_code, wrappedList(expressDataError.code.toString()))
|
||||
}
|
||||
}
|
||||
|
||||
internal fun getExpressErrorTitle(expressDataError: ExpressDataError): TextReference {
|
||||
return when (expressDataError) {
|
||||
is ExpressDataError.ExchangeNotPossibleError -> resourceReference(
|
||||
id = R.string.warning_express_pair_unavailable_title,
|
||||
formatArgs = wrappedList(expressDataError.code),
|
||||
)
|
||||
is ExpressDataError.UnknownError -> resourceReference(R.string.common_error)
|
||||
else -> resourceReference(R.string.warning_express_refresh_required_title)
|
||||
}
|
||||
}
|
||||
|
|
@ -19,17 +19,24 @@ import com.tangem.core.ui.utils.InputNumberFormatter
|
|||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.feedback.GetCardInfoUseCase
|
||||
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.tokens.GetCryptoCurrencyStatusSyncUseCase
|
||||
import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase
|
||||
import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.feature.swap.analytics.SwapEvents
|
||||
import com.tangem.feature.swap.domain.BlockchainInteractor
|
||||
import com.tangem.feature.swap.domain.SwapInteractor
|
||||
import com.tangem.feature.swap.domain.models.DataError
|
||||
import com.tangem.feature.swap.domain.models.ExpressDataError
|
||||
import com.tangem.feature.swap.domain.models.ExpressException
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.domain.*
|
||||
|
|
@ -64,7 +71,6 @@ typealias SuccessLoadedSwapData = Map<SwapProvider, SwapState.QuotesLoadedState>
|
|||
@Suppress("LargeClass", "LongParameterList")
|
||||
@HiltViewModel
|
||||
internal class SwapViewModel @Inject constructor(
|
||||
private val swapInteractorFactory: SwapInteractor.Factory,
|
||||
private val blockchainInteractor: BlockchainInteractor,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
|
|
@ -73,6 +79,11 @@ internal class SwapViewModel @Inject constructor(
|
|||
private val getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusSyncUseCase,
|
||||
private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase,
|
||||
private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val getCardInfoUseCase: GetCardInfoUseCase,
|
||||
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
swapInteractorFactory: SwapInteractor.Factory,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel(), DefaultLifecycleObserver {
|
||||
|
||||
|
|
@ -84,9 +95,13 @@ internal class SwapViewModel @Inject constructor(
|
|||
?.unbundle(UserWalletId.serializer())
|
||||
?: error("no expected parameter UserWalletId found")
|
||||
|
||||
private val isInitiallyReversed: Boolean = savedStateHandle.get<Boolean>(AppRoute.Swap.IS_INITIAL_REVERSE_ORDER)
|
||||
?: false
|
||||
|
||||
private val swapInteractor = swapInteractorFactory.create(userWalletId)
|
||||
|
||||
private lateinit var initialCryptoCurrencyStatus: CryptoCurrencyStatus
|
||||
private var userWallet: UserWallet by Delegates.notNull()
|
||||
|
||||
private var isBalanceHidden = true
|
||||
|
||||
|
|
@ -120,7 +135,10 @@ internal class SwapViewModel @Inject constructor(
|
|||
|
||||
private val isUserResolvableError: (SwapState) -> Boolean = {
|
||||
it is SwapState.SwapError &&
|
||||
(it.error is DataError.ExchangeTooSmallAmountError || it.error is DataError.ExchangeTooBigAmountError)
|
||||
(
|
||||
it.error is ExpressDataError.ExchangeTooSmallAmountError ||
|
||||
it.error is ExpressDataError.ExchangeTooBigAmountError
|
||||
)
|
||||
}
|
||||
|
||||
private val fromTokenBalanceJobHolder = JobHolder()
|
||||
|
|
@ -133,11 +151,13 @@ internal class SwapViewModel @Inject constructor(
|
|||
viewModelScope.launch(dispatchers.io) {
|
||||
val cryptoCurrencyStatus =
|
||||
getCryptoCurrencyStatusUseCase(userWalletId, initialCryptoCurrency.id).getOrNull()
|
||||
if (cryptoCurrencyStatus == null) {
|
||||
uiState = stateBuilder.addAlert(uiState = uiState, onClick = swapRouter::back)
|
||||
val wallet = getUserWalletUseCase(userWalletId).getOrNull()
|
||||
if (cryptoCurrencyStatus == null || wallet == null) {
|
||||
uiState = stateBuilder.addAlert(uiState = uiState, onDismiss = swapRouter::back)
|
||||
} else {
|
||||
userWallet = wallet
|
||||
initialCryptoCurrencyStatus = cryptoCurrencyStatus
|
||||
initTokens()
|
||||
initTokens(isInitiallyReversed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -180,18 +200,21 @@ internal class SwapViewModel @Inject constructor(
|
|||
analyticsEventHandler.send(SwapEvents.ChooseTokenScreenOpened(availableTokens = isAnyAvailableTokens))
|
||||
}
|
||||
|
||||
private fun initTokens() {
|
||||
private fun initTokens(isReverseFromTo: Boolean) {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
runCatching(dispatchers.io) {
|
||||
swapInteractor.getTokensDataState(initialCryptoCurrency)
|
||||
}.onSuccess { state ->
|
||||
updateTokensState(state)
|
||||
val selectedCurrency = swapInteractor.getInitialCurrencyToSwap(
|
||||
initialCryptoCurrency = initialCryptoCurrency,
|
||||
state = state,
|
||||
isReverseFromTo = isReverseFromTo,
|
||||
)
|
||||
applyInitialTokenChoice(
|
||||
state,
|
||||
swapInteractor.selectInitialCurrencyToSwap(
|
||||
initialCryptoCurrency,
|
||||
state,
|
||||
),
|
||||
state = state,
|
||||
selectedCurrency = selectedCurrency,
|
||||
isReverseFromTo = isReverseFromTo,
|
||||
)
|
||||
|
||||
(dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Coin)?.let {
|
||||
|
|
@ -215,11 +238,12 @@ internal class SwapViewModel @Inject constructor(
|
|||
applyInitialTokenChoice(
|
||||
state = TokensDataStateExpress.EMPTY,
|
||||
selectedCurrency = null,
|
||||
isReverseFromTo = isReverseFromTo,
|
||||
)
|
||||
|
||||
uiState = stateBuilder.createInitialErrorState(
|
||||
uiState,
|
||||
(it as? ExpressException)?.dataError?.code ?: DataError.UnknownError.code,
|
||||
(it as? ExpressException)?.expressDataError?.code ?: ExpressDataError.UnknownError.code,
|
||||
) {
|
||||
uiState = stateBuilder.createInitialLoadingState(
|
||||
initialCurrency = initialCryptoCurrency,
|
||||
|
|
@ -227,33 +251,43 @@ internal class SwapViewModel @Inject constructor(
|
|||
initialCryptoCurrency.network.backendId,
|
||||
),
|
||||
)
|
||||
initTokens()
|
||||
initTokens(isReverseFromTo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyInitialTokenChoice(state: TokensDataStateExpress, selectedCurrency: CryptoCurrencyStatus?) {
|
||||
val fromCurrencyStatus = initialCryptoCurrencyStatus
|
||||
dataState = dataState.copy(
|
||||
fromCryptoCurrency = fromCurrencyStatus,
|
||||
toCryptoCurrency = selectedCurrency,
|
||||
tokensDataState = state,
|
||||
)
|
||||
private fun applyInitialTokenChoice(
|
||||
state: TokensDataStateExpress,
|
||||
selectedCurrency: CryptoCurrencyStatus?,
|
||||
isReverseFromTo: Boolean,
|
||||
) {
|
||||
// exceptional case
|
||||
if (selectedCurrency == null) {
|
||||
analyticsEventHandler.send(SwapEvents.NoticeNoAvailableTokensToSwap)
|
||||
uiState = stateBuilder.createNoAvailableTokensToSwapState(
|
||||
uiStateHolder = uiState,
|
||||
fromToken = fromCurrencyStatus,
|
||||
)
|
||||
} else {
|
||||
startLoadingQuotes(
|
||||
fromToken = fromCurrencyStatus,
|
||||
toToken = selectedCurrency,
|
||||
amount = lastAmount.value,
|
||||
toProvidersList = findSwapProviders(fromCurrencyStatus, selectedCurrency),
|
||||
fromToken = initialCryptoCurrencyStatus,
|
||||
)
|
||||
return
|
||||
}
|
||||
isOrderReversed = isReverseFromTo
|
||||
val (fromCurrencyStatus, toCurrencyStatus) = if (isOrderReversed) {
|
||||
selectedCurrency to initialCryptoCurrencyStatus
|
||||
} else {
|
||||
initialCryptoCurrencyStatus to selectedCurrency
|
||||
}
|
||||
dataState = dataState.copy(
|
||||
fromCryptoCurrency = fromCurrencyStatus,
|
||||
toCryptoCurrency = toCurrencyStatus,
|
||||
tokensDataState = state,
|
||||
)
|
||||
startLoadingQuotes(
|
||||
fromToken = fromCurrencyStatus,
|
||||
toToken = toCurrencyStatus,
|
||||
amount = lastAmount.value,
|
||||
toProvidersList = findSwapProviders(fromCurrencyStatus, toCurrencyStatus),
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateTokensState(tokenDataState: TokensDataStateExpress) {
|
||||
|
|
@ -397,7 +431,7 @@ internal class SwapViewModel @Inject constructor(
|
|||
swapProvider = provider,
|
||||
fromToken = state.fromTokenInfo,
|
||||
toToken = dataState.toCryptoCurrency,
|
||||
dataError = state.error,
|
||||
expressDataError = state.error,
|
||||
includeFeeInAmount = state.includeFeeInAmount,
|
||||
isReverseSwapPossible = isReverseSwapPossible(),
|
||||
)
|
||||
|
|
@ -406,7 +440,7 @@ internal class SwapViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun sendErrorAnalyticsEvent(error: DataError, provider: SwapProvider) {
|
||||
private fun sendErrorAnalyticsEvent(error: ExpressDataError, provider: SwapProvider) {
|
||||
val receiveToken = dataState.toCryptoCurrency?.currency?.let {
|
||||
"${it.network.backendId}:${it.symbol}"
|
||||
}
|
||||
|
|
@ -556,20 +590,19 @@ internal class SwapViewModel @Inject constructor(
|
|||
|
||||
swapRouter.openScreen(SwapNavScreen.Success)
|
||||
}
|
||||
is SwapTransactionState.UserCancelled -> {
|
||||
startLoadingQuotesFromLastState()
|
||||
}
|
||||
is SwapTransactionState.DemoMode -> {
|
||||
startLoadingQuotesFromLastState()
|
||||
SwapTransactionState.DemoMode -> {
|
||||
uiState = stateBuilder.createDemoModeAlert(uiState) {
|
||||
uiState = stateBuilder.clearAlert(uiState)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
is SwapTransactionState.Error -> {
|
||||
startLoadingQuotesFromLastState()
|
||||
uiState = stateBuilder.createErrorTransaction(uiState, it) {
|
||||
uiState = stateBuilder.clearAlert(uiState)
|
||||
}
|
||||
uiState = stateBuilder.createErrorTransactionAlert(
|
||||
uiState = uiState,
|
||||
error = it,
|
||||
onDismiss = { uiState = stateBuilder.clearAlert(uiState) },
|
||||
onSupportClick = ::onFailedTxEmailClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
|
|
@ -640,23 +673,28 @@ internal class SwapViewModel @Inject constructor(
|
|||
uiState = stateBuilder.dismissBottomSheet(uiState)
|
||||
startLoadingQuotesFromLastState(isSilent = true)
|
||||
}
|
||||
is SwapTransactionState.UserCancelled -> Unit
|
||||
else -> {
|
||||
uiState = stateBuilder.createErrorTransaction(uiState, it) {
|
||||
is SwapTransactionState.Error -> {
|
||||
uiState = stateBuilder.createErrorTransactionAlert(
|
||||
uiState = uiState,
|
||||
error = it,
|
||||
onDismiss = { uiState = stateBuilder.clearAlert(uiState) },
|
||||
onSupportClick = ::onFailedTxEmailClick,
|
||||
)
|
||||
}
|
||||
SwapTransactionState.DemoMode -> {
|
||||
uiState = stateBuilder.createDemoModeAlert(uiState) {
|
||||
uiState = stateBuilder.clearAlert(uiState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onFailure { makeDefaultAlert() }
|
||||
}.onFailure { showGenericError(it.message.orEmpty()) }
|
||||
}.onFailure {
|
||||
Timber.e(it.message.orEmpty())
|
||||
makeDefaultAlert()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showGenericError(message: String) {
|
||||
makeDefaultAlert(resourceReference(R.string.common_unknown_error))
|
||||
Timber.e(message)
|
||||
}
|
||||
|
||||
private fun onSearchEntered(searchQuery: String) {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val tokenDataState = dataState.tokensDataState ?: return@launch
|
||||
|
|
@ -863,15 +901,11 @@ internal class SwapViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun makeDefaultAlert() {
|
||||
uiState = stateBuilder.addAlert(uiState) {
|
||||
uiState = stateBuilder.clearAlert(uiState)
|
||||
}
|
||||
uiState = stateBuilder.addAlert(uiState)
|
||||
}
|
||||
|
||||
private fun makeDefaultAlert(message: TextReference) {
|
||||
uiState = stateBuilder.addAlert(uiState, message) {
|
||||
uiState = stateBuilder.clearAlert(uiState)
|
||||
}
|
||||
uiState = stateBuilder.addAlert(uiState, message)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
|
|
@ -1113,7 +1147,8 @@ internal class SwapViewModel @Inject constructor(
|
|||
toToken.currency.id.value
|
||||
}
|
||||
|
||||
return groupToFind.available.find { idToFind == it.currencyStatus.currency.id.value }?.providers ?: emptyList()
|
||||
return groupToFind.available.find { idToFind == it.currencyStatus.currency.id.value }?.providers
|
||||
?: emptyList()
|
||||
}
|
||||
|
||||
private fun Map<SwapProvider, SwapState>.getLastLoadedSuccessStates(): SuccessLoadedSwapData {
|
||||
|
|
@ -1201,6 +1236,33 @@ internal class SwapViewModel @Inject constructor(
|
|||
analyticsEventHandler.send(event = event)
|
||||
}
|
||||
|
||||
private fun onFailedTxEmailClick(errorMessage: String) {
|
||||
viewModelScope.launch {
|
||||
val network = initialCryptoCurrencyStatus.currency.network
|
||||
val cardInfo = getCardInfoUseCase(userWallet.scanResponse).getOrElse { error("CardInfo must be not null") }
|
||||
|
||||
saveBlockchainErrorUseCase(
|
||||
error = BlockchainErrorInfo(
|
||||
errorMessage = errorMessage,
|
||||
blockchainId = network.id.value,
|
||||
derivationPath = network.derivationPath.value,
|
||||
destinationAddress = dataState.swapDataModel?.transaction?.txTo.orEmpty(),
|
||||
tokenSymbol = initialCryptoCurrency.symbol,
|
||||
amount = dataState.amount.orEmpty(),
|
||||
fee = dataState.selectedFee?.feeCryptoFormatted.orEmpty(),
|
||||
),
|
||||
)
|
||||
|
||||
val email = FeedbackEmailType.SwapProblem(
|
||||
cardInfo = cardInfo,
|
||||
providerName = dataState.selectedProvider?.name.orEmpty(),
|
||||
txId = dataState.swapDataModel?.transaction?.txId.orEmpty(),
|
||||
)
|
||||
|
||||
sendFeedbackEmailUseCase(email)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val INITIAL_AMOUNT = ""
|
||||
const val UPDATE_DELAY = 10000L
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue