Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-10 11:58:43 +05:00
commit b03396d7b1
44 changed files with 569 additions and 92 deletions

View file

@ -177,6 +177,8 @@ dependencies {
implementation(projects.features.onramp.impl)
implementation(projects.features.onboardingV2.api)
implementation(projects.features.onboardingV2.impl)
implementation(projects.features.stories.api)
implementation(projects.features.stories.impl)
/** AndroidX libraries */
implementation(deps.androidx.core.ktx)

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

@ -4,6 +4,7 @@ import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.feature.referral.ReferralFragment
import com.tangem.feature.stories.api.StoriesComponent
import com.tangem.feature.swap.presentation.SwapFragment
import com.tangem.feature.walletsettings.component.WalletSettingsComponent
import com.tangem.features.details.component.DetailsComponent
@ -56,6 +57,7 @@ internal class ChildFactory @Inject constructor(
private val swapSelectTokensComponentFactory: SwapSelectTokensComponent.Factory,
private val onboardingEntryComponentFactory: OnboardingEntryComponent.Factory,
private val welcomeComponentFactory: WelcomeComponent.Factory,
private val storiesComponentFactory: StoriesComponent.Factory,
private val sendRouter: SendRouter,
private val tokenDetailsRouter: TokenDetailsRouter,
private val walletRouter: WalletRouter,
@ -199,6 +201,16 @@ internal class ChildFactory @Inject constructor(
componentFactory = onboardingEntryComponentFactory,
)
}
is AppRoute.Stories -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = StoriesComponent.Params(
storyId = route.storyId,
nextScreen = route.nextScreen,
),
componentFactory = storiesComponentFactory,
)
}
is AppRoute.AccessCodeRecovery,
is AppRoute.AppCurrencySelector,
is AppRoute.SaveWallet,
@ -407,6 +419,16 @@ internal class ChildFactory @Inject constructor(
componentFactory = onboardingEntryComponentFactory,
)
}
is AppRoute.Stories -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = StoriesComponent.Params(
storyId = route.storyId,
nextScreen = route.nextScreen,
),
componentFactory = storiesComponentFactory,
)
}
}
// endregion
}

View file

@ -341,4 +341,12 @@ sealed class AppRoute(val path: String) : Route {
AddBackup, // continue backup process for existing wallet 1
}
}
@Serializable
data class Stories(
val storyId: String,
val nextScreen: AppRoute,
) : AppRoute(path = "/stories$storyId"), RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
}
}

View file

@ -36,6 +36,7 @@ dependencies {
implementation(projects.domain.transaction.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.onramp.models)
implementation(projects.domain.promo.models)
implementation(deps.tangem.card.core)
implementation(deps.tangem.blockchain) {

View file

@ -0,0 +1,48 @@
package com.tangem.common.ui.swapStoriesScreen
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.promo.models.StoryContent
import kotlinx.collections.immutable.persistentListOf
object SwapStoriesFactory {
// WARNING! Be careful with indices. Temporary solution.
// Use all data from v1/stories api (image url, title, subtitle)
@Suppress("MagicNumber")
fun createStoriesState(swapStory: StoryContent, onStoriesClose: () -> Unit): SwapStoriesUM {
val storyOrderedImageUrls = swapStory.getImageUrls()
if (storyOrderedImageUrls.size != 5) return SwapStoriesUM.Empty
return SwapStoriesUM.Content(
stories = persistentListOf(
SwapStoriesUM.Content.Config(
imageUrl = storyOrderedImageUrls[0],
title = resourceReference(R.string.swap_story_first_title),
subtitle = resourceReference(R.string.swap_story_first_subtitle),
),
SwapStoriesUM.Content.Config(
imageUrl = storyOrderedImageUrls[1],
title = resourceReference(R.string.swap_story_second_title),
subtitle = resourceReference(R.string.swap_story_second_subtitle),
),
SwapStoriesUM.Content.Config(
imageUrl = storyOrderedImageUrls[2],
title = resourceReference(R.string.swap_story_third_title),
subtitle = resourceReference(R.string.swap_story_third_subtitle),
),
SwapStoriesUM.Content.Config(
imageUrl = storyOrderedImageUrls[3],
title = resourceReference(R.string.swap_story_forth_title),
subtitle = resourceReference(R.string.swap_story_forth_subtitle),
),
SwapStoriesUM.Content.Config(
imageUrl = storyOrderedImageUrls[4],
title = resourceReference(R.string.swap_story_fifth_title),
subtitle = resourceReference(R.string.swap_story_fifth_subtitle),
),
),
onClose = onStoriesClose,
)
}
}

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

@ -0,0 +1,26 @@
package com.tangem.common.ui.swapStoriesScreen
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
sealed class SwapStoriesUM {
data object Empty : SwapStoriesUM()
data class Content(
override val stories: ImmutableList<Config>,
override val onClose: () -> Unit,
) : SwapStoriesUM(), StoriesContentConfig<Content.Config> {
override val isRestartable: Boolean = false
data class Config(
val imageUrl: String,
val title: TextReference,
val subtitle: TextReference,
) : StoryConfig {
override val duration: Int = 2000
}
}
}

View file

@ -4,20 +4,23 @@ import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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 com.tangem.core.ui.R
import com.tangem.core.ui.components.buttons.actions.ActionButton
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -25,6 +28,7 @@ import kotlinx.collections.immutable.persistentListOf
fun HorizontalActionChips(
buttons: ImmutableList<ActionButtonConfig>,
modifier: Modifier = Modifier,
containerColor: Color = TangemTheme.colors.background.secondary,
contentPadding: PaddingValues = PaddingValues(TangemTheme.dimens.spacing0),
) {
LazyRow(
@ -36,7 +40,7 @@ fun HorizontalActionChips(
// do not use key cause when change items order, list is scrolled
items(
items = buttons,
itemContent = { ActionButton(config = it) },
itemContent = { ActionButton(config = it, containerColor = containerColor) },
)
}
}
@ -46,22 +50,31 @@ fun HorizontalActionChips(
@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_HorizontalActionChips(
@PreviewParameter(ActionButtonConfigProvider::class) buttons: HorizontalActionChips,
@PreviewParameter(ActionButtonConfigProvider::class) buttons: HorizontalActionChipsData,
) {
TangemThemePreview {
HorizontalActionChips(buttons = buttons.buttons)
HorizontalActionChips(
buttons = buttons.buttons,
modifier = Modifier.padding(4.dp),
)
}
}
private class ActionButtonConfigProvider : CollectionPreviewParameterProvider<HorizontalActionChips>(
private class ActionButtonConfigProvider : CollectionPreviewParameterProvider<HorizontalActionChipsData>(
collection = listOf(
HorizontalActionChips(
HorizontalActionChipsData(
buttons = persistentListOf(
ActionButtonConfig(
text = TextReference.Str(value = "Buy"),
iconResId = R.drawable.ic_plus_24,
onClick = {},
),
ActionButtonConfig(
text = TextReference.Str(value = "Exchange"),
iconResId = R.drawable.ic_exchange_vertical_24,
onClick = {},
showBadge = true,
),
ActionButtonConfig(
text = TextReference.Str(value = "Send"),
iconResId = R.drawable.ic_arrow_up_24,
@ -72,17 +85,12 @@ private class ActionButtonConfigProvider : CollectionPreviewParameterProvider<Ho
iconResId = R.drawable.ic_arrow_down_24,
onClick = {},
),
ActionButtonConfig(
text = TextReference.Str(value = "Exchange"),
iconResId = R.drawable.ic_exchange_vertical_24,
onClick = {},
),
),
),
),
)
private data class HorizontalActionChips(
private data class HorizontalActionChipsData(
val buttons: ImmutableList<ActionButtonConfig>,
)
// endregion Preview

View file

@ -13,6 +13,8 @@ import com.tangem.core.ui.extensions.TextReference
* @property enabled enabled
* @property dimContent determines whether the button content will be dimmed. This property will be ignored if [enabled]
* is `false`.
* @property isInProgress indicates progress state of button
* @property showBadge display dot in upper right corner
*
[REDACTED_AUTHOR]
*/
@ -24,4 +26,5 @@ data class ActionButtonConfig(
val enabled: Boolean = true,
val dimContent: Boolean = false,
val isInProgress: Boolean = false,
val showBadge: Boolean = false,
)

View file

@ -17,7 +17,10 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
@ -29,6 +32,7 @@ import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -81,6 +85,7 @@ fun ActionButton(
config: ActionButtonConfig,
modifier: Modifier = Modifier,
color: Color = TangemTheme.colors.button.secondary,
containerColor: Color = TangemTheme.colors.background.secondary,
) {
ActionBaseButton(
config = config,
@ -97,6 +102,7 @@ fun ActionButton(
},
modifier = modifier,
color = color,
containerColor = containerColor,
)
}
@ -108,6 +114,7 @@ fun ActionBaseButton(
content: @Composable (modifier: Modifier) -> Unit,
modifier: Modifier = Modifier,
color: Color = TangemTheme.colors.button.secondary,
containerColor: Color = TangemTheme.colors.background.secondary,
) {
val context = LocalContext.current
val backgroundColor by animateColorAsState(
@ -119,6 +126,12 @@ fun ActionBaseButton(
modifier = modifier
.heightIn(min = 36.dp)
.widthIn(min = 100.dp)
.drawWithContent {
drawContent()
if (config.showBadge) {
drawBadge(containerColor = containerColor)
}
}
.clip(shape)
.combinedClickable(
enabled = config.enabled,
@ -212,6 +225,20 @@ fun getTextColor(config: ActionButtonConfig): Color {
}
}
private fun DrawScope.drawBadge(containerColor: Color) {
val width = size.width
drawCircle(
color = containerColor,
center = Offset(x = width - 2.dp.toPx(), y = 2.dp.toPx()),
radius = 5.dp.toPx(),
)
drawCircle(
color = TangemColorPalette.Azure,
center = Offset(x = width - 2.dp.toPx(), y = 2.dp.toPx()),
radius = 3.dp.toPx(),
)
}
@Preview(group = "RoundedActionButton", showBackground = true)
@Preview(group = "RoundedActionButton", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
@ -237,6 +264,7 @@ private class ActionStateProvider : CollectionPreviewParameterProvider<ActionBut
iconResId = R.drawable.ic_arrow_up_24,
enabled = true,
onClick = {},
showBadge = true,
),
ActionButtonConfig(
text = TextReference.Str(value = "Dimmed"),

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()

1
features/stories/api/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,20 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("kotlin-parcelize")
id("configuration")
}
android {
namespace = "com.tangem.features.stories.api"
}
dependencies {
/* Project - Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
/* Compose */
implementation(deps.compose.runtime)
implementation(projects.common.routing)
}

View file

@ -0,0 +1,15 @@
package com.tangem.feature.stories.api
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
interface StoriesComponent : ComposableContentComponent {
data class Params(
val storyId: String,
val nextScreen: AppRoute,
)
interface Factory : ComponentFactory<Params, StoriesComponent>
}

1
features/stories/impl/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,43 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
id("configuration")
}
android {
namespace = "com.tangem.features.stories.impl"
}
dependencies {
/** Feature modules */
implementation(projects.features.stories.api)
implementation(projects.features.swap.api)
/** Domain modules */
implementation(projects.domain.promo)
implementation(projects.domain.promo.models)
/** Project - Common */
implementation(projects.common.routing)
implementation(projects.common.ui)
/** Project - Core */
implementation(projects.core.configToggles)
implementation(projects.core.decompose)
implementation(projects.core.navigation)
implementation(projects.core.ui)
/** AndroidX */
implementation(deps.androidx.activity.compose)
implementation(deps.lifecycle.compose)
/** Others */
implementation(deps.timber)
implementation(deps.arrow.core)
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
}

View file

@ -0,0 +1,34 @@
package com.tangem.feature.stories.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.ui.swapStoriesScreen.SwapStoriesScreen
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.feature.stories.api.StoriesComponent
import com.tangem.feature.stories.impl.model.StoriesModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Stable
internal class DefaultStoriesComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted params: StoriesComponent.Params,
) : StoriesComponent, AppComponentContext by context {
private val model: StoriesModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state = model.state.collectAsStateWithLifecycle()
SwapStoriesScreen(state.value)
}
@AssistedFactory
interface Factory : StoriesComponent.Factory {
override fun create(context: AppComponentContext, params: StoriesComponent.Params): DefaultStoriesComponent
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.feature.stories.impl.di
import com.tangem.feature.stories.api.StoriesComponent
import com.tangem.feature.stories.impl.DefaultStoriesComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface StoriesComponentModule {
@Binds
@Singleton
fun bindSwapStoriesComponentFactory(factory: DefaultStoriesComponent.Factory): StoriesComponent.Factory
}

View file

@ -0,0 +1,20 @@
package com.tangem.feature.stories.impl.di
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.model.Model
import com.tangem.feature.stories.impl.model.StoriesModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(DecomposeComponent::class)
internal interface StoriesModelModule {
@Binds
@IntoMap
@ClassKey(StoriesModel::class)
fun bindStoriesModel(model: StoriesModel): Model
}

View file

@ -0,0 +1,64 @@
package com.tangem.feature.stories.impl.model
import com.tangem.common.ui.swapStoriesScreen.SwapStoriesFactory
import com.tangem.common.ui.swapStoriesScreen.SwapStoriesUM
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.domain.promo.GetStoryContentUseCase
import com.tangem.domain.promo.ShouldShowSwapStoriesUseCase
import com.tangem.domain.promo.models.StoryContentIds
import com.tangem.feature.stories.api.StoriesComponent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
internal class StoriesModel @Inject constructor(
paramsContainer: ParamsContainer,
private val router: Router,
override val dispatchers: CoroutineDispatcherProvider,
private val getStoryContentUseCase: GetStoryContentUseCase,
private val shouldShowSwapStoriesUseCase: ShouldShowSwapStoriesUseCase,
) : Model() {
val state: StateFlow<SwapStoriesUM> get() = _state
private val params = paramsContainer.require<StoriesComponent.Params>()
private val _state: MutableStateFlow<SwapStoriesUM> = MutableStateFlow(
value = SwapStoriesUM.Empty,
)
init {
initStories()
}
private fun openScreen() {
modelScope.launch {
shouldShowSwapStoriesUseCase.neverToShow()
router.pop()
router.push(params.nextScreen)
}
}
private fun initStories() {
modelScope.launch {
getStoryContentUseCase(params.storyId).fold(
ifLeft = {
Timber.e("Unable to load stories for ${StoryContentIds.STORY_FIRST_TIME_SWAP.id}")
openScreen() // Fallback to target screen
},
ifRight = { swapStory ->
_state.update {
SwapStoriesFactory.createStoriesState(
swapStory = swapStory,
onStoriesClose = ::openScreen,
)
}
},
)
}
}
}

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),
)
}

View file

@ -94,6 +94,7 @@ internal fun TokenDetailsBalanceBlock(
end.linkTo(anchor = parent.end)
bottom.linkTo(anchor = parent.bottom, margin = spacing12)
},
containerColor = TangemTheme.colors.background.primary,
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing12),
)
}

View file

@ -105,6 +105,7 @@ dependencies {
implementation(projects.features.markets.api)
implementation(projects.features.onramp.api)
implementation(projects.features.onboardingV2.api)
implementation(projects.features.swap.api)
/** Common modules */
implementation(projects.common)

View file

@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
import com.tangem.core.deeplink.DeepLinksRegistry
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.promo.ShouldShowSwapStoriesUseCase
import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.wallets.models.UserWallet
@ -12,10 +13,12 @@ import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarnin
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletActionButtonsSubscriber
import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletTokenListSubscriber
import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletWarningsSubscriber
import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
import com.tangem.features.swap.SwapFeatureToggles
@Suppress("LongParameterList")
internal class MultiWalletContentLoader(
@ -31,6 +34,8 @@ internal class MultiWalletContentLoader(
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory,
private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
private val shouldShowSwapStoriesUseCase: ShouldShowSwapStoriesUseCase,
private val swapFeatureToggles: SwapFeatureToggles,
private val deepLinksRegistry: DeepLinksRegistry,
) : WalletContentLoader(id = userWallet.walletId) {
@ -56,6 +61,12 @@ internal class MultiWalletContentLoader(
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,
walletWarningsSingleEventSender = walletWarningsSingleEventSender,
),
MultiWalletActionButtonsSubscriber(
userWallet = userWallet,
stateHolder = stateHolder,
shouldShowSwapStoriesUseCase = shouldShowSwapStoriesUseCase,
swapFeatureToggles = swapFeatureToggles,
),
)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
import com.tangem.core.deeplink.DeepLinksRegistry
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.promo.ShouldShowSwapStoriesUseCase
import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.wallets.models.UserWallet
@ -13,6 +14,7 @@ import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenList
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
import com.tangem.features.swap.SwapFeatureToggles
import dagger.hilt.android.scopes.ViewModelScoped
import javax.inject.Inject
@ -29,6 +31,8 @@ internal class MultiWalletContentLoaderFactory @Inject constructor(
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender,
private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
private val shouldShowSwapStoriesUseCase: ShouldShowSwapStoriesUseCase,
private val swapFeatureToggles: SwapFeatureToggles,
private val deepLinksRegistry: DeepLinksRegistry,
) {
@ -46,6 +50,8 @@ internal class MultiWalletContentLoaderFactory @Inject constructor(
walletWarningsSingleEventSender = walletWarningsSingleEventSender,
applyTokenListSortingUseCase = applyTokenListSortingUseCase,
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
shouldShowSwapStoriesUseCase = shouldShowSwapStoriesUseCase,
swapFeatureToggles = swapFeatureToggles,
deepLinksRegistry = deepLinksRegistry,
)
}

View file

@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
import com.tangem.core.deeplink.DeepLinksRegistry
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.promo.ShouldShowSwapStoriesUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
@ -11,10 +12,12 @@ import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarnin
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletActionButtonsSubscriber
import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletWarningsSubscriber
import com.tangem.feature.wallet.presentation.wallet.subscribers.SingleWalletWithTokenListSubscriber
import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
import com.tangem.features.swap.SwapFeatureToggles
@Suppress("LongParameterList")
internal class SingleWalletWithTokenContentLoader(
@ -29,6 +32,8 @@ internal class SingleWalletWithTokenContentLoader(
private val tokenListStore: MultiWalletTokenListStore,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
private val shouldShowSwapStoriesUseCase: ShouldShowSwapStoriesUseCase,
private val swapFeatureToggles: SwapFeatureToggles,
private val deepLinksRegistry: DeepLinksRegistry,
) : WalletContentLoader(id = userWallet.walletId) {
@ -53,6 +58,12 @@ internal class SingleWalletWithTokenContentLoader(
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,
walletWarningsSingleEventSender = walletWarningsSingleEventSender,
),
MultiWalletActionButtonsSubscriber(
userWallet = userWallet,
stateHolder = stateHolder,
shouldShowSwapStoriesUseCase = shouldShowSwapStoriesUseCase,
swapFeatureToggles = swapFeatureToggles,
),
)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
import com.tangem.core.deeplink.DeepLinksRegistry
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.promo.ShouldShowSwapStoriesUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
@ -12,6 +13,7 @@ import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenList
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
import com.tangem.features.swap.SwapFeatureToggles
import javax.inject.Inject
// TODO: Refactor
@ -26,6 +28,8 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor(
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender,
private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
private val shouldShowSwapStoriesUseCase: ShouldShowSwapStoriesUseCase,
private val swapFeatureToggles: SwapFeatureToggles,
private val deepLinksRegistry: DeepLinksRegistry,
) {
@ -42,6 +46,8 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor(
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,
walletWarningsSingleEventSender = walletWarningsSingleEventSender,
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
shouldShowSwapStoriesUseCase = shouldShowSwapStoriesUseCase,
swapFeatureToggles = swapFeatureToggles,
deepLinksRegistry = deepLinksRegistry,
)
}

View file

@ -143,6 +143,7 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) {
override val dimContent: Boolean,
override val onClick: () -> Unit,
val isInProgress: Boolean = false,
val showBadge: Boolean = false,
) : WalletManageButton(
config = ActionButtonConfig(
text = TextReference.Res(id = R.string.swapping_swap_action),
@ -151,6 +152,7 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) {
enabled = enabled,
dimContent = dimContent,
isInProgress = isInProgress,
showBadge = showBadge,
),
)
}

View file

@ -0,0 +1,19 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.utils.showSwapBadge
internal class UpdateMultiWalletActionButtonBadgeTransformer(
private val showSwapBadge: Boolean,
userWalletId: UserWalletId,
) : WalletStateTransformer(userWalletId) {
override fun transform(prevState: WalletState): WalletState {
return when (prevState) {
is WalletState.MultiCurrency.Content -> {
prevState.copy(buttons = prevState.showSwapBadge(showBadge = showSwapBadge))
}
else -> prevState
}
}
}

View file

@ -24,4 +24,15 @@ private fun WalletState.MultiCurrency.Content.changeAvailability(enabled: Boolea
}
}
.toPersistentList()
}
internal fun WalletState.MultiCurrency.Content.showSwapBadge(showBadge: Boolean): PersistentList<WalletManageButton> {
return buttons
.map { action ->
when (action) {
is WalletManageButton.Swap -> action.copy(showBadge = showBadge)
else -> action
}
}
.toPersistentList()
}

View file

@ -0,0 +1,30 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.domain.promo.ShouldShowSwapStoriesUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.transformers.UpdateMultiWalletActionButtonBadgeTransformer
import com.tangem.features.swap.SwapFeatureToggles
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
internal class MultiWalletActionButtonsSubscriber(
private val userWallet: UserWallet,
private val stateHolder: WalletStateController,
private val shouldShowSwapStoriesUseCase: ShouldShowSwapStoriesUseCase,
private val swapFeatureToggles: SwapFeatureToggles,
) : WalletSubscriber() {
override fun create(coroutineScope: CoroutineScope): Flow<*> = shouldShowSwapStoriesUseCase()
.distinctUntilChanged()
.map { showSwapBadge ->
val showBadge = swapFeatureToggles.isPromoStoriesEnabled && showSwapBadge
stateHolder.update(
UpdateMultiWalletActionButtonBadgeTransformer(
userWalletId = userWallet.walletId,
showSwapBadge = showBadge,
),
)
}
}

View file

@ -28,6 +28,8 @@ import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.promo.ShouldShowSwapStoriesUseCase
import com.tangem.domain.promo.models.StoryContentIds
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
@ -51,6 +53,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensLis
import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender
import com.tangem.features.onramp.OnrampFeatureToggles
import com.tangem.features.swap.SwapFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.collections.immutable.toImmutableList
@ -110,6 +113,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
private val removeCurrencyUseCase: RemoveCurrencyUseCase,
private val getExploreUrlUseCase: GetExploreUrlUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val shouldShowSwapStoriesUseCase: ShouldShowSwapStoriesUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val dispatchers: CoroutineDispatcherProvider,
private val reduxStateHolder: ReduxStateHolder,
@ -119,6 +123,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
private val appRouter: AppRouter,
private val rampStateManager: RampStateManager,
private val onrampFeatureToggles: OnrampFeatureToggles,
private val swapFeatureToggles: SwapFeatureToggles,
) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents {
override fun onSendClick(
@ -430,11 +435,16 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
return
}
onMultiWalletActionClick(
statusFlow = rampStateManager.getExpressInitializationStatus(userWalletId),
route = AppRoute.SwapCrypto(userWalletId = userWalletId),
eventCreator = MainScreenAnalyticsEvent::ButtonSwap,
)
viewModelScope.launch {
val swapRoute = getSwapRoute(
AppRoute.SwapCrypto(userWalletId = userWalletId),
)
onMultiWalletActionClick(
statusFlow = rampStateManager.getExpressInitializationStatus(userWalletId),
route = swapRoute,
eventCreator = MainScreenAnalyticsEvent::ButtonSwap,
)
}
}
override fun onMultiWalletBuyClick(userWalletId: UserWalletId) {
@ -597,4 +607,19 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
event = WalletEvent.ShowAlert(state = WalletAlertState.ProvidersStillLoading),
)
}
private suspend fun getSwapRoute(targetRoute: AppRoute): AppRoute {
val isSwapStoriesEnabled = swapFeatureToggles.isPromoStoriesEnabled
val shouldShowSwapStories = shouldShowSwapStoriesUseCase.invokeSync()
val showSwapStories = shouldShowSwapStories && isSwapStoriesEnabled
return if (showSwapStories) {
AppRoute.Stories(
storyId = StoryContentIds.STORY_FIRST_TIME_SWAP.id,
nextScreen = targetRoute,
)
} else {
targetRoute
}
}
}

View file

@ -205,6 +205,9 @@ include(":features:markets:impl")
include(":features:onramp:api")
include(":features:onramp:impl")
include(":features:stories:api")
include(":features:stories:impl")
// endregion Feature modules
// region Domain modules