Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-14 10:09:45 +05:00
commit 41c98cfee7
22 changed files with 186 additions and 18 deletions

View file

@ -1,9 +1,6 @@
package com.tangem.tap.di.domain
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.promo.ShouldShowRingPromoUseCase
import com.tangem.domain.promo.ShouldShowSwapPromoTokenUseCase
import com.tangem.domain.promo.ShouldShowSwapPromoWalletUseCase
import com.tangem.domain.promo.*
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -33,4 +30,10 @@ internal object PromoDomainModule {
fun provideShouldShowSwapPromoTokenUseCase(promoRepository: PromoRepository): ShouldShowSwapPromoTokenUseCase {
return ShouldShowSwapPromoTokenUseCase(promoRepository)
}
@Provides
@Singleton
fun provideShouldShowSwapStoriesUseCase(promoRepository: PromoRepository): ShouldShowSwapStoriesUseCase {
return ShouldShowSwapStoriesUseCase(promoRepository)
}
}

View file

@ -26,5 +26,9 @@
{
"name": "NAVIGATION_REFACTORING",
"version": "undefined"
},
{
"name": "SWAP_STORIES_ENABLED",
"version": "undefined"
}
]

View file

@ -113,6 +113,8 @@ object PreferencesKeys {
val WAS_LOG_FILE_CLEARED by lazy { booleanPreferencesKey(name = "wasLogFileCleared") }
val SHOULD_SHOW_SWAP_STORIES_KEY by lazy { booleanPreferencesKey("shouldShowSwapStories") }
// region Permission
fun getShouldShowPermission(permission: String) = booleanPreferencesKey("shouldShowPushPermission_$permission")

View file

@ -56,15 +56,19 @@ inline fun <reified T : StoryConfig> StoriesContainer(
onNextStory = storyState::nextStory,
)
currentStoryContent(storyState.currentStory, isPaused)
StoriesProgressBar(
steps = storyState.steps,
currentStep = storyState.currentIndex.intValue,
stepDuration = storyState.currentStory.duration,
paused = isPaused,
onStepFinish = storyState::nextStory,
onStepFinish = {
if (storyState.nextStory()) {
config.onClose()
}
},
)
currentStoryContent(storyState.currentStory, isPaused)
}
}
@ -97,6 +101,7 @@ private data class StoryContainerPreviewProviderData(
StoryConfigPreviewProviderData(title = "Wallet"),
),
override val isRestartable: Boolean = true,
override val onClose: () -> Unit = {},
) : StoriesContentConfig<StoryConfigPreviewProviderData>
private data class StoryConfigPreviewProviderData(

View file

@ -80,6 +80,7 @@ fun StoriesProgressBar(
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.statusBarsPadding()
.padding(
start = 16.dp,
end = 16.dp,

View file

@ -11,6 +11,7 @@ import kotlinx.collections.immutable.ImmutableList
interface StoriesContentConfig<T : StoryConfig> {
val stories: ImmutableList<T>
val isRestartable: Boolean
val onClose: () -> Unit
}
interface StoryConfig {

View file

@ -8,7 +8,9 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.ADDED_WALLETS_WIT
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_RING_PROMO_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_SWAP_STORIES_KEY
import com.tangem.datasource.local.preferences.utils.get
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.datasource.local.preferences.utils.store
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.promo.models.PromoBanner
@ -89,6 +91,21 @@ internal class DefaultPromoRepository(
appPreferencesStore.store(key = SHOULD_SHOW_RING_PROMO_KEY, value = false)
}
override fun isReadyToShowSwapStories(): Flow<Boolean> {
return appPreferencesStore.get(SHOULD_SHOW_SWAP_STORIES_KEY, true)
}
override suspend fun isReadyToShowSwapStoriesSync(): Boolean {
return appPreferencesStore.getSyncOrDefault(SHOULD_SHOW_SWAP_STORIES_KEY, true)
}
override suspend fun setNeverToShowSwapStories() {
appPreferencesStore.store(
key = SHOULD_SHOW_SWAP_STORIES_KEY,
value = false,
)
}
private companion object {
private const val CHANGELLY_NAME = "changelly"
private const val OKX = "okx"

View file

@ -23,4 +23,10 @@ interface PromoRepository {
fun isReadyToShowRingPromo(userWalletId: UserWalletId): Flow<Boolean>
suspend fun setNeverToShowRingPromo()
fun isReadyToShowSwapStories(): Flow<Boolean>
suspend fun isReadyToShowSwapStoriesSync(): Boolean
suspend fun setNeverToShowSwapStories()
}

View file

@ -0,0 +1,10 @@
package com.tangem.domain.promo
import kotlinx.coroutines.flow.Flow
class ShouldShowSwapStoriesUseCase(private val promoRepository: PromoRepository) {
operator fun invoke(): Flow<Boolean> = promoRepository.isReadyToShowSwapStories()
suspend fun invokeSync(): Boolean = promoRepository.isReadyToShowSwapStoriesSync()
suspend fun neverToShow() = promoRepository.setNeverToShowSwapStories()
}

View file

@ -1,18 +1,10 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.kotlin.serialization)
alias(deps.plugins.hilt.android)
id("kotlin-parcelize")
id("configuration")
}
android {
namespace = "com.tangem.feature.swap.api"
}
dependencies {
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
namespace = "com.tangem.features.swap.api"
}

View file

@ -0,0 +1,5 @@
package com.tangem.features.swap
interface SwapFeatureToggles {
val isPromoStoriesEnabled: Boolean
}

View file

@ -38,12 +38,14 @@ dependencies {
implementation(projects.domain.settings)
implementation(projects.domain.staking)
implementation(projects.domain.feedback)
implementation(projects.domain.promo)
/** Feature modules */
implementation(projects.features.swap.domain)
implementation(projects.features.swap.domain.api)
implementation(projects.features.swap.domain.models)
implementation(projects.features.wallet.api)
implementation(projects.features.swap.api)
/** AndroidX */
implementation(deps.androidx.activity.compose)

View file

@ -0,0 +1,11 @@
package com.tangem.feature.swap
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.features.swap.SwapFeatureToggles
internal class DefaultSwapFeatureToggles(
private val featureToggles: FeatureTogglesManager,
) : SwapFeatureToggles {
override val isPromoStoriesEnabled: Boolean
get() = featureToggles.isFeatureEnabled("SWAP_STORIES_ENABLED")
}

View file

@ -0,0 +1,21 @@
package com.tangem.feature.swap.di
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.feature.swap.DefaultSwapFeatureToggles
import com.tangem.features.swap.SwapFeatureToggles
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 SwapFeatureModule {
@Provides
@Singleton
fun provideSwapFeatureToggles(featureToggles: FeatureTogglesManager): SwapFeatureToggles {
return DefaultSwapFeatureToggles(featureToggles)
}
}

View file

@ -34,6 +34,7 @@ internal data class SwapStateHolder(
val successState: SwapSuccessStateHolder? = null,
val selectTokenState: SwapSelectTokenStateHolder? = null,
val bottomSheetConfig: TangemBottomSheetConfig? = null,
val storiesConfig: SwapStoriesContentConfig? = null,
val swapButton: SwapButton,
val shouldShowMaxAmount: Boolean,

View file

@ -0,0 +1,19 @@
package com.tangem.feature.swap.models
import androidx.annotation.DrawableRes
import com.tangem.core.ui.components.stories.model.StoriesContentConfig
import com.tangem.core.ui.components.stories.model.StoryConfig
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(
@DrawableRes val imageRes: Int,
) : StoryConfig {
override val duration: Int = 2000
}

View file

@ -21,6 +21,7 @@ data class UiActions(
val openPermissionBottomSheet: () -> Unit,
val onChangeApproveType: (ApproveType) -> Unit,
// region new actions
val onStoriesClose: () -> Unit,
val onRetryClick: () -> Unit,
val onClickFee: () -> Unit,
val onSelectFeeType: (TxFee) -> Unit,

View file

@ -13,6 +13,7 @@ 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
@ -46,6 +47,14 @@ class SwapFragment : ComposeFragment() {
label = "",
) { screen ->
when (screen) {
SwapNavScreen.PromoStories -> {
val storiesConfig = viewModel.uiState.storiesConfig
if (storiesConfig != null) {
SwapStoriesScreen(config = storiesConfig)
} else {
SwapScreen(stateHolder = viewModel.uiState)
}
}
SwapNavScreen.Main -> SwapScreen(stateHolder = viewModel.uiState)
SwapNavScreen.Success -> {
val successState = viewModel.uiState.successState

View file

@ -67,5 +67,5 @@ internal class SwapRouter(
}
enum class SwapNavScreen {
Main, Success, SelectToken
Main, Success, SelectToken, PromoStories
}

View file

@ -125,6 +125,26 @@ internal class StateBuilder(
)
}
fun createStoriesState(uiStateHolder: SwapStateHolder): SwapStateHolder {
return uiStateHolder.copy(
// todo stories [REDACTED_TASK_KEY] fix when design is ready
storiesConfig = SwapStoriesContentConfig(
stories = persistentListOf(
SwapStoryConfig(
imageRes = R.drawable.img_card_with_ring,
),
SwapStoryConfig(
imageRes = R.drawable.ill_businessman_3d,
),
SwapStoryConfig(
imageRes = R.drawable.ill_one_inch_powered,
),
),
onClose = actions.onStoriesClose,
),
)
}
fun createNoAvailableTokensToSwapState(
uiStateHolder: SwapStateHolder,
fromToken: CryptoCurrencyStatus,

View file

@ -0,0 +1,20 @@
package com.tangem.feature.swap.ui
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.painterResource
import com.tangem.core.ui.components.stories.StoriesContainer
import com.tangem.feature.swap.models.SwapStoriesContentConfig
@Composable
internal fun SwapStoriesScreen(config: SwapStoriesContentConfig) {
StoriesContainer(
config = config,
) { current, _ ->
// todo stories [REDACTED_TASK_KEY] fix when design is ready
Image(
painter = painterResource(current.imageRes),
contentDescription = null,
)
}
}

View file

@ -27,6 +27,7 @@ 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.promo.ShouldShowSwapStoriesUseCase
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
@ -50,6 +51,7 @@ import com.tangem.feature.swap.router.SwapNavScreen
import com.tangem.feature.swap.router.SwapRouter
import com.tangem.feature.swap.ui.StateBuilder
import com.tangem.feature.swap.utils.formatToUIRepresentation
import com.tangem.features.swap.SwapFeatureToggles
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.*
import com.tangem.utils.isNullOrZero
@ -86,6 +88,8 @@ internal class SwapViewModel @Inject constructor(
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase,
private val shouldShowSwapStoriesUseCase: ShouldShowSwapStoriesUseCase,
private val featureToggles: SwapFeatureToggles,
swapInteractorFactory: SwapInteractor.Factory,
private val savedStateHandle: SavedStateHandle,
private val urlOpener: UrlOpener,
@ -168,6 +172,13 @@ internal class SwapViewModel @Inject constructor(
val fromStatus = getCryptoCurrencyStatusUseCase(userWalletId, initialCurrencyFrom.id).getOrNull()
val toStatus = initialCurrencyTo?.let { getCryptoCurrencyStatusUseCase(userWalletId, it.id).getOrNull() }
val wallet = getUserWalletUseCase(userWalletId).getOrNull()
val isShowPromoStories = shouldShowSwapStoriesUseCase.invokeSync() && featureToggles.isPromoStoriesEnabled
if (isShowPromoStories) {
initStories()
swapRouter.openScreen(SwapNavScreen.PromoStories)
}
if (fromStatus == null || wallet == null) {
uiState = stateBuilder.addAlert(uiState = uiState, onDismiss = swapRouter::back)
} else {
@ -271,6 +282,10 @@ internal class SwapViewModel @Inject constructor(
}
}
private fun initStories() {
uiState = stateBuilder.createStoriesState(uiState)
}
private fun applyInitialTokenChoice(
state: TokensDataStateExpress,
selectedCurrency: CryptoCurrencyStatus?,
@ -1115,6 +1130,9 @@ internal class SwapViewModel @Inject constructor(
onSuccess = {
swapRouter.openScreen(SwapNavScreen.Success)
},
onStoriesClose = {
swapRouter.openScreen(SwapNavScreen.Main)
},
)
}