Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-10 11:08:04 +05:00
parent 380e5c19e0
commit 0cbf7b74e2
14 changed files with 64 additions and 75 deletions

View file

@ -1,10 +1,12 @@
package com.tangem.tap.di.domain
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.repository.*
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.features.swap.SwapFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -227,6 +229,8 @@ internal object TokensDomainModule {
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
promoRepository: PromoRepository,
swapFeatureToggles: SwapFeatureToggles,
dispatchers: CoroutineDispatcherProvider,
): GetCryptoCurrencyActionsUseCase {
return GetCryptoCurrencyActionsUseCase(
@ -236,6 +240,8 @@ internal object TokensDomainModule {
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
promoRepository = promoRepository,
swapFeatureToggles = swapFeatureToggles,
dispatchers = dispatchers,
)
}

View file

@ -1,4 +1,4 @@
package com.tangem.feature.swap.ui
package com.tangem.common.ui.swapStoriesScreen
import android.content.res.Configuration
import androidx.activity.compose.BackHandler
@ -33,15 +33,15 @@ import com.tangem.core.ui.res.LocalWindowSize
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.swap.models.SwapStoriesContentConfig
import com.tangem.feature.swap.models.SwapStoryConfig
import kotlinx.collections.immutable.persistentListOf
private val SubtitleColor = Color(0xFF868692)
private const val STORIES_RELATIVE_PADDING = 0.7
@Composable
internal fun SwapStoriesScreen(config: SwapStoriesContentConfig) {
fun SwapStoriesScreen(config: SwapStoriesUM) {
if (config !is SwapStoriesUM.Content) return
BackHandler(onBack = config.onClose)
SystemBarsIconsDisposable(darkIcons = false)
@ -106,9 +106,9 @@ internal fun SwapStoriesScreen(config: SwapStoriesContentConfig) {
private fun SwapStoriesScreen_Preview() {
TangemThemePreview {
SwapStoriesScreen(
SwapStoriesContentConfig(
SwapStoriesUM.Content(
stories = persistentListOf(
SwapStoryConfig(
SwapStoriesUM.Content.Config(
imageUrl = "https://devweb.tangem.com/images/stories/swap/image1.png",
title = stringReference("Exchange With Us"),
subtitle = stringReference(

View file

@ -25,11 +25,14 @@ dependencies {
implementation(projects.domain.settings)
implementation(projects.features.swap.domain.api)
implementation(projects.features.swap.domain.models)
implementation(projects.domain.promo.models)
implementation(projects.domain.promo)
/** Project - Api */
implementation(projects.features.send.api)
implementation(projects.features.staking.api)
implementation(projects.features.markets.api)
implementation(projects.features.swap.api)
/** Project - Other */
implementation(projects.core.utils)

View file

@ -2,6 +2,7 @@ package com.tangem.domain.tokens
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.*
@ -12,9 +13,9 @@ import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.transaction.models.AssetRequirementsCondition
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.swap.SwapFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.isNullOrZero
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withTimeoutOrNull
@ -31,10 +32,11 @@ class GetCryptoCurrencyActionsUseCase(
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
private val stakingRepository: StakingRepository,
private val promoRepository: PromoRepository,
private val dispatchers: CoroutineDispatcherProvider,
private val swapFeatureToggles: SwapFeatureToggles,
) {
@OptIn(ExperimentalCoroutinesApi::class)
suspend operator fun invoke(
userWallet: UserWallet,
cryptoCurrencyStatus: CryptoCurrencyStatus,
@ -62,13 +64,16 @@ class GetCryptoCurrencyActionsUseCase(
includeQuotes = false,
)
}
val flow = networkFlow.mapLatest { maybeCoinStatus ->
val flow = combine(
flow = networkFlow,
flow2 = promoRepository.isReadyToShowSwapStories().distinctUntilChanged(),
) { maybeCoinStatus, shouldShowSwapStories ->
createTokenActionsState(
userWallet = userWallet,
coinStatus = maybeCoinStatus.getOrNull(),
cryptoCurrencyStatus = cryptoCurrencyStatus,
requirements = requirements,
shouldShowSwapStories = shouldShowSwapStories,
)
}
@ -81,6 +86,7 @@ class GetCryptoCurrencyActionsUseCase(
coinStatus: CryptoCurrencyStatus?,
cryptoCurrencyStatus: CryptoCurrencyStatus,
requirements: AssetRequirementsCondition?,
shouldShowSwapStories: Boolean,
): TokenActionsState {
return TokenActionsState(
walletId = userWallet.walletId,
@ -90,6 +96,7 @@ class GetCryptoCurrencyActionsUseCase(
coinStatus = coinStatus,
cryptoCurrencyStatus = cryptoCurrencyStatus,
requirements = requirements,
shouldShowSwapStories = shouldShowSwapStories,
),
)
}
@ -104,6 +111,7 @@ class GetCryptoCurrencyActionsUseCase(
coinStatus: CryptoCurrencyStatus?,
cryptoCurrencyStatus: CryptoCurrencyStatus,
requirements: AssetRequirementsCondition?,
shouldShowSwapStories: Boolean,
): List<TokenActionsState.ActionState> {
val cryptoCurrency = cryptoCurrencyStatus.currency
if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.MissedDerivation) {
@ -173,11 +181,18 @@ class GetCryptoCurrencyActionsUseCase(
rampManager.availableForSwap(userWallet.walletId, cryptoCurrency)
} ?: false
if (isExchangeable && cryptoCurrencyStatus.value !is CryptoCurrencyStatus.NoQuote) {
activeList.add(TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.None))
val swapStoriesEnabled = swapFeatureToggles.isPromoStoriesEnabled
activeList.add(
TokenActionsState.ActionState.Swap(
unavailabilityReason = ScenarioUnavailabilityReason.None,
showBadge = shouldShowSwapStories && swapStoriesEnabled,
),
)
} else {
disabledList.add(
TokenActionsState.ActionState.Swap(
unavailabilityReason = ScenarioUnavailabilityReason.NotExchangeable(cryptoCurrency.name),
showBadge = false,
),
)
}
@ -233,7 +248,12 @@ class GetCryptoCurrencyActionsUseCase(
)
}
actionsList.add(TokenActionsState.ActionState.Send(ScenarioUnavailabilityReason.Unreachable))
actionsList.add(TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.Unreachable))
actionsList.add(
TokenActionsState.ActionState.Swap(
unavailabilityReason = ScenarioUnavailabilityReason.Unreachable,
showBadge = false,
),
)
actionsList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.Unreachable))
if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) {
val scenario = getReceiveScenario(requirements)

View file

@ -26,7 +26,10 @@ data class TokenActionsState(
val yield: Yield?,
) : ActionState()
data class Swap(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState()
data class Swap(
override val unavailabilityReason: ScenarioUnavailabilityReason,
val showBadge: Boolean,
) : ActionState()
data class Send(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState()

View file

@ -4,6 +4,7 @@ import androidx.annotation.DrawableRes
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.common.ui.swapStoriesScreen.SwapStoriesUM
import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.event.StateEvent
@ -34,7 +35,7 @@ internal data class SwapStateHolder(
val successState: SwapSuccessStateHolder? = null,
val selectTokenState: SwapSelectTokenStateHolder? = null,
val bottomSheetConfig: TangemBottomSheetConfig? = null,
val storiesConfig: SwapStoriesContentConfig? = null,
val storiesConfig: SwapStoriesUM? = null,
val swapButton: SwapButton,
val shouldShowMaxAmount: Boolean,

View file

@ -1,21 +0,0 @@
package com.tangem.feature.swap.models
import com.tangem.core.ui.components.stories.model.StoriesContentConfig
import com.tangem.core.ui.components.stories.model.StoryConfig
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
data class SwapStoriesContentConfig(
override val stories: ImmutableList<SwapStoryConfig>,
override val onClose: () -> Unit,
) : StoriesContentConfig<SwapStoryConfig> {
override val isRestartable: Boolean = false
}
data class SwapStoryConfig(
val imageUrl: String,
val title: TextReference,
val subtitle: TextReference,
) : StoryConfig {
override val duration: Int = 2000
}

View file

@ -6,13 +6,13 @@ import androidx.compose.foundation.background
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import com.tangem.common.ui.swapStoriesScreen.SwapStoriesScreen
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.feature.swap.router.SwapNavScreen
import com.tangem.feature.swap.ui.SwapScreen
import com.tangem.feature.swap.ui.SwapSelectTokenScreen
import com.tangem.feature.swap.ui.SwapStoriesScreen
import com.tangem.feature.swap.ui.SwapSuccessScreen
import com.tangem.feature.swap.viewmodels.SwapViewModel
import dagger.hilt.android.AndroidEntryPoint

View file

@ -5,6 +5,7 @@ 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.common.ui.notifications.NotificationUM
import com.tangem.common.ui.swapStoriesScreen.SwapStoriesFactory
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.event.consumedEvent
@ -126,43 +127,11 @@ internal class StateBuilder(
)
}
// WARNING! Be careful with indices. Temporary solution.
// Use all data from v1/stories api (image url, title, subtitle)
@Suppress("MagicNumber")
fun createStoriesState(uiStateHolder: SwapStateHolder, swapStory: StoryContent): SwapStateHolder {
val storyOrderedImageUrls = swapStory.getImageUrls()
if (storyOrderedImageUrls.size != 5) return uiStateHolder
return uiStateHolder.copy(
storiesConfig = SwapStoriesContentConfig(
stories = persistentListOf(
SwapStoryConfig(
imageUrl = storyOrderedImageUrls[0],
title = resourceReference(R.string.swap_story_first_title),
subtitle = resourceReference(R.string.swap_story_first_subtitle),
),
SwapStoryConfig(
imageUrl = storyOrderedImageUrls[1],
title = resourceReference(R.string.swap_story_second_title),
subtitle = resourceReference(R.string.swap_story_second_subtitle),
),
SwapStoryConfig(
imageUrl = storyOrderedImageUrls[2],
title = resourceReference(R.string.swap_story_third_title),
subtitle = resourceReference(R.string.swap_story_third_subtitle),
),
SwapStoryConfig(
imageUrl = storyOrderedImageUrls[3],
title = resourceReference(R.string.swap_story_forth_title),
subtitle = resourceReference(R.string.swap_story_forth_subtitle),
),
SwapStoryConfig(
imageUrl = storyOrderedImageUrls[4],
title = resourceReference(R.string.swap_story_fifth_title),
subtitle = resourceReference(R.string.swap_story_fifth_subtitle),
),
),
onClose = actions.onStoriesClose,
storiesConfig = SwapStoriesFactory.createStoriesState(
swapStory = swapStory,
onStoriesClose = actions.onStoriesClose,
),
)
}

View file

@ -85,6 +85,7 @@ dependencies {
implementation(projects.domain.onramp)
implementation(projects.domain.onramp.models)
implementation(projects.domain.promo)
implementation(projects.domain.promo.models)
/** Temp dependency to swap domain */
implementation(projects.features.swap.domain)
@ -97,5 +98,6 @@ dependencies {
implementation(projects.features.staking.api)
implementation(projects.features.markets.api)
implementation(projects.features.onramp.api)
implementation(projects.features.swap.api)
}

View file

@ -100,7 +100,7 @@ internal object TokenDetailsPreviewData {
TokenDetailsActionButton.Buy(dimContent = false, onClick = {}),
TokenDetailsActionButton.Send(dimContent = false, onClick = {}),
TokenDetailsActionButton.Receive(onClick = {}, onLongClick = null),
TokenDetailsActionButton.Swap(dimContent = false, onClick = {}),
TokenDetailsActionButton.Swap(dimContent = false, onClick = {}, showBadge = true),
)
private val balanceSegmentedButtonConfig = persistentListOf(

View file

@ -83,12 +83,17 @@ internal sealed class TokenDetailsActionButton(val config: ActionButtonConfig) {
* @property dimContent determines whether the button content will be dimmed
* @property onClick lambda be invoked when Swap button is clicked
*/
data class Swap(val dimContent: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton(
data class Swap(
val dimContent: Boolean,
val showBadge: Boolean,
override val onClick: () -> Unit,
) : TokenDetailsActionButton(
config = ActionButtonConfig(
text = TextReference.Res(id = R.string.swapping_swap_action),
iconResId = R.drawable.ic_exchange_vertical_24,
onClick = onClick,
dimContent = dimContent,
showBadge = showBadge,
),
)
}

View file

@ -54,6 +54,7 @@ internal class TokenDetailsActionButtonsConverter(
TokenDetailsActionButton.Swap(
dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None,
onClick = { clickIntents.onSwapClick(action.unavailabilityReason) },
showBadge = action.showBadge,
)
}
else -> {

View file

@ -1,12 +1,12 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
import arrow.core.getOrElse
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.card.NetworkHasDerivationUseCase
import com.tangem.domain.staking.GetStakingIntegrationIdUseCase
@ -122,7 +122,7 @@ internal class TokenDetailsSkeletonStateConverter(
TokenDetailsActionButton.Send(dimContent = false, onClick = {}),
TokenDetailsActionButton.Receive(onClick = {}, onLongClick = null),
TokenDetailsActionButton.Sell(dimContent = false, onClick = {}),
TokenDetailsActionButton.Swap(dimContent = false, onClick = {}),
TokenDetailsActionButton.Swap(dimContent = false, onClick = {}, showBadge = false),
)
}