Updated on 2026-08-14
This commit is contained in:
parent
428f1076f0
commit
850c904233
9 changed files with 399 additions and 74 deletions
|
|
@ -51,7 +51,10 @@ inline fun <reified T : StoryConfig> StoriesContainer(
|
|||
) {
|
||||
var watchedCounter by remember { mutableIntStateOf(1) }
|
||||
var isPressed by remember { mutableStateOf(value = false) }
|
||||
val storyState by remember(config) {
|
||||
// Key on content (stories + repeatability), not the whole config object: an unrelated change
|
||||
// to the config instance (e.g. a caller folding a changing flag into it) must not recreate the
|
||||
// state machine and rewind stories to the first page.
|
||||
val storyState by remember(config.stories, config.isRestartable) {
|
||||
mutableStateOf(
|
||||
StoriesStepStateMachine(
|
||||
stories = config.stories,
|
||||
|
|
@ -59,7 +62,9 @@ inline fun <reified T : StoryConfig> StoriesContainer(
|
|||
),
|
||||
)
|
||||
}
|
||||
BackHandler(onBack = { config.onClose(watchedCounter) })
|
||||
if (config.isCloseButtonVisible) {
|
||||
BackHandler(onBack = { config.onClose(watchedCounter) })
|
||||
}
|
||||
|
||||
val isPaused = isPressed || isPauseStories
|
||||
|
||||
|
|
@ -90,22 +95,24 @@ inline fun <reified T : StoryConfig> StoriesContainer(
|
|||
paused = isPaused,
|
||||
onStepFinish = onNextClick,
|
||||
)
|
||||
Icon(
|
||||
painter = rememberVectorPainter(
|
||||
image = ImageVector.vectorResource(R.drawable.ic_close_24),
|
||||
),
|
||||
tint = TangemTheme.colors.icon.constant,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.align(Alignment.End)
|
||||
.padding(top = 14.dp, end = 16.dp)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = LocalIndication.current,
|
||||
onClick = { config.onClose(watchedCounter) },
|
||||
)
|
||||
.testTag(SwapStoriesScreenTestTags.CLOSE_BUTTON),
|
||||
)
|
||||
if (config.isCloseButtonVisible) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(
|
||||
image = ImageVector.vectorResource(R.drawable.ic_close_24),
|
||||
),
|
||||
tint = TangemTheme.colors.icon.constant,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.align(Alignment.End)
|
||||
.padding(top = 14.dp, end = 16.dp)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = LocalIndication.current,
|
||||
onClick = { config.onClose(watchedCounter) },
|
||||
)
|
||||
.testTag(SwapStoriesScreenTestTags.CLOSE_BUTTON),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,18 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
/**
|
||||
* Config for stories component
|
||||
*
|
||||
* @property stories configuration list
|
||||
* @property stories configuration list
|
||||
* @property isRestartable indicates than stories progressions starts
|
||||
* @property isCloseButtonVisible whether the top-right close button (and back handler) is shown.
|
||||
* Set `false` for non-closable stories (e.g. a root intro screen); [onClose] is then irrelevant.
|
||||
* @property onClose invoked with the number of watched stories when the user closes them.
|
||||
* Only meaningful when [isCloseButtonVisible] is `true`; defaults to a no-op for non-closable stories.
|
||||
*/
|
||||
interface StoriesContentConfig<T : StoryConfig> {
|
||||
val stories: ImmutableList<T>
|
||||
val isRestartable: Boolean
|
||||
val onClose: (Int) -> Unit
|
||||
val isCloseButtonVisible: Boolean get() = true
|
||||
val onClose: (Int) -> Unit get() = {}
|
||||
}
|
||||
|
||||
interface StoryConfig {
|
||||
|
|
|
|||
|
|
@ -69,4 +69,7 @@ dependencies {
|
|||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
}
|
||||
|
||||
/** Tests */
|
||||
testImplementation(projects.test.core)
|
||||
}
|
||||
|
|
@ -3,8 +3,6 @@ package com.tangem.features.home.impl.model
|
|||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRoute.ManageTokens.Source
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.routing.entity.InitScreenLaunchMode
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
|
|
@ -16,11 +14,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.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.message.dialog.Dialogs
|
||||
import com.tangem.domain.card.ScanCardProcessor
|
||||
import com.tangem.domain.card.analytics.IntroductionProcess
|
||||
import com.tangem.domain.card.analytics.Shop
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.common.wallets.error.SaveWalletError
|
||||
|
|
@ -30,11 +26,11 @@ import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
|
|||
import com.tangem.domain.settings.usercountry.models.UserCountry
|
||||
import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions
|
||||
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
||||
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.feature.referral.domain.ShouldShowMobileWalletPromoUseCase
|
||||
import com.tangem.features.home.api.HomeComponent
|
||||
import com.tangem.features.home.api.HomeFeatureToggles
|
||||
import com.tangem.features.home.impl.ui.state.HomeStoriesConfig
|
||||
import com.tangem.features.home.impl.ui.state.HomeUM
|
||||
import com.tangem.features.home.impl.ui.state.Stories
|
||||
import com.tangem.features.home.impl.ui.state.getRestrictedStories
|
||||
|
|
@ -60,12 +56,9 @@ internal class HomeModel @Inject constructor(
|
|||
private val settingsRepository: SettingsRepository,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val router: Router,
|
||||
private val appRouter: AppRouter,
|
||||
private val getUserCountryUseCase: GetUserCountryUseCase,
|
||||
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val shouldShowMobileWalletPromoUseCase: ShouldShowMobileWalletPromoUseCase,
|
||||
private val homeFeatureToggles: HomeFeatureToggles,
|
||||
|
|
@ -76,18 +69,8 @@ internal class HomeModel @Inject constructor(
|
|||
|
||||
val params = paramsContainer.require<HomeComponent.Params>()
|
||||
|
||||
private val _uiState = MutableStateFlow(
|
||||
HomeUM(
|
||||
scanInProgress = false,
|
||||
isStoriesContainerEnabled = homeFeatureToggles.isStoriesContainerEnabled,
|
||||
stories = getRestrictedStories().toImmutableList(),
|
||||
onShopClick = ::onShopClick,
|
||||
onSearchTokensClick = ::onSearchTokensClick,
|
||||
onGetStartedClick = ::onGetStartedClick,
|
||||
),
|
||||
)
|
||||
|
||||
val uiState = _uiState.asStateFlow()
|
||||
val uiState: StateFlow<HomeUM>
|
||||
field = MutableStateFlow(createInitialState())
|
||||
|
||||
init {
|
||||
analyticsEventHandler.send(IntroductionProcess.ScreenOpened())
|
||||
|
|
@ -99,6 +82,17 @@ internal class HomeModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun createInitialState(): HomeUM {
|
||||
val initialStories = getRestrictedStories().toImmutableList()
|
||||
return HomeUM(
|
||||
scanInProgress = false,
|
||||
isStoriesContainerEnabled = homeFeatureToggles.isStoriesContainerEnabled,
|
||||
stories = initialStories,
|
||||
storiesConfig = HomeStoriesConfig(stories = initialStories),
|
||||
onGetStartedClick = ::onGetStartedClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun observeUserCountryChanges() {
|
||||
getUserCountryUseCase.invoke()
|
||||
.distinctUntilChanged()
|
||||
|
|
@ -117,35 +111,21 @@ internal class HomeModel @Inject constructor(
|
|||
} else {
|
||||
Stories.entries
|
||||
}
|
||||
.toImmutableList()
|
||||
|
||||
_uiState.update {
|
||||
it.copy(stories = stories.toImmutableList())
|
||||
uiState.update {
|
||||
it.copy(stories = stories, storiesConfig = HomeStoriesConfig(stories = stories))
|
||||
}
|
||||
}
|
||||
|
||||
private fun onShopClick() {
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards())
|
||||
analyticsEventHandler.send(Shop.ScreenOpened())
|
||||
modelScope.launch {
|
||||
generateBuyTangemCardLinkUseCase.invoke(null).let { urlOpener.openUrl(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSearchTokensClick() {
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonTokensList())
|
||||
router.push(AppRoute.ManageTokens(Source.STORIES))
|
||||
}
|
||||
|
||||
private fun onGetStartedClick() {
|
||||
debouncer.debounce(modelScope) {
|
||||
modelScope.launch {
|
||||
val mode = if (shouldShowMobileWalletPromoUseCase()) {
|
||||
AppRoute.CreateWalletStart.Mode.HotWallet
|
||||
} else {
|
||||
AppRoute.CreateWalletStart.Mode.ColdWallet
|
||||
}
|
||||
router.push(AppRoute.CreateWalletStart(mode = mode))
|
||||
val mode = if (shouldShowMobileWalletPromoUseCase()) {
|
||||
AppRoute.CreateWalletStart.Mode.HotWallet
|
||||
} else {
|
||||
AppRoute.CreateWalletStart.Mode.ColdWallet
|
||||
}
|
||||
router.push(AppRoute.CreateWalletStart(mode = mode))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -201,13 +181,13 @@ internal class HomeModel @Inject constructor(
|
|||
setLoading(false)
|
||||
when (error) {
|
||||
is SaveWalletError.DataError -> TangemLogger.e("Unable to save user wallet: $error")
|
||||
is SaveWalletError.WalletAlreadySaved -> appRouter.replaceAll(AppRoute.Wallet)
|
||||
is SaveWalletError.WalletAlreadySaved -> router.replaceAll(AppRoute.Wallet)
|
||||
}
|
||||
},
|
||||
ifRight = {
|
||||
setLoading(false)
|
||||
sendSignedInCardAnalyticsEvent(scanResponse, userWallet.isImported)
|
||||
appRouter.replaceAll(AppRoute.Wallet)
|
||||
router.replaceAll(AppRoute.Wallet)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -224,7 +204,7 @@ internal class HomeModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun setLoading(isLoading: Boolean) {
|
||||
_uiState.update { it.copy(scanInProgress = isLoading) }
|
||||
uiState.update { it.copy(scanInProgress = isLoading) }
|
||||
}
|
||||
|
||||
private fun handleScanError(error: TangemError) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import androidx.compose.ui.Modifier
|
|||
import com.tangem.core.ui.components.SystemBarsIconsDisposable
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.utils.ChangeRootBackgroundColorEffect
|
||||
import com.tangem.features.home.impl.ui.compose.HomeStoriesScreen
|
||||
import com.tangem.features.home.impl.ui.compose.StoriesScreenV2
|
||||
import com.tangem.features.home.impl.ui.state.HomeUM
|
||||
|
||||
|
|
@ -12,11 +13,18 @@ import com.tangem.features.home.impl.ui.state.HomeUM
|
|||
internal fun Home(state: HomeUM, modifier: Modifier = Modifier) {
|
||||
SystemBarsIconsDisposable(darkIcons = false)
|
||||
|
||||
StoriesScreenV2(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
onGetStartedClick = state.onGetStartedClick,
|
||||
)
|
||||
if (state.isStoriesContainerEnabled) {
|
||||
HomeStoriesScreen(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
)
|
||||
} else {
|
||||
StoriesScreenV2(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
onGetStartedClick = state.onGetStartedClick,
|
||||
)
|
||||
}
|
||||
|
||||
ChangeRootBackgroundColorEffect(TangemColorPalette.Black)
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
package com.tangem.features.home.impl.ui.compose
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
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.layout.ContentScale
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.stories.StoriesContainer
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.test.StoriesScreenTestTags
|
||||
import com.tangem.features.home.impl.ui.compose.content.FirstStoriesContent
|
||||
import com.tangem.features.home.impl.ui.compose.content.StoriesCurrencies
|
||||
import com.tangem.features.home.impl.ui.compose.content.StoriesRevolutionaryWallet
|
||||
import com.tangem.features.home.impl.ui.compose.content.StoriesUltraSecureBackup
|
||||
import com.tangem.features.home.impl.ui.compose.content.StoriesWalletForEveryone
|
||||
import com.tangem.features.home.impl.ui.compose.content.StoriesWeb3
|
||||
import com.tangem.features.home.impl.ui.compose.views.HomeButtonsV2
|
||||
import com.tangem.features.home.impl.ui.state.HomeUM
|
||||
import com.tangem.features.home.impl.ui.state.Stories
|
||||
|
||||
private const val BACKGROUND_COLOR = 0xFF010101L
|
||||
|
||||
/**
|
||||
* Home stories built on the shared [StoriesContainer].
|
||||
* The container provides the progress bar, tap/hold navigation and pause; this screen supplies the
|
||||
* per-story content, the Tangem logo and the persistent "Get Started" button.
|
||||
*/
|
||||
@Composable
|
||||
internal fun HomeStoriesScreen(state: HomeUM, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(BACKGROUND_COLOR))
|
||||
.testTag(StoriesScreenTestTags.SCREEN_CONTAINER),
|
||||
) {
|
||||
StoriesContainer(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
config = state.storiesConfig,
|
||||
isPauseStories = state.scanInProgress,
|
||||
) { story, isPaused ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.statusBarsPadding()
|
||||
.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_tangem_logo),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillHeight,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
top = TangemTheme.dimens.spacing16,
|
||||
)
|
||||
.height(TangemTheme.dimens.size18)
|
||||
.align(Alignment.Start),
|
||||
)
|
||||
when (story) {
|
||||
Stories.TangemIntro -> FirstStoriesContent(isPaused = isPaused, duration = story.duration)
|
||||
Stories.RevolutionaryWallet -> StoriesRevolutionaryWallet()
|
||||
Stories.UltraSecureBackup -> StoriesUltraSecureBackup(
|
||||
isPaused = isPaused,
|
||||
stepDuration = story.duration,
|
||||
)
|
||||
Stories.Currencies -> StoriesCurrencies(isPaused, story.duration)
|
||||
Stories.Web3 -> StoriesWeb3(isPaused, story.duration)
|
||||
Stories.WalletForEveryone -> StoriesWalletForEveryone(story.duration)
|
||||
}
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.navigationBarsPadding()
|
||||
.padding(bottom = TangemTheme.dimens.spacing16)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
HomeButtonsV2(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onGetStartedClick = state.onGetStartedClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -27,7 +27,9 @@ import com.tangem.features.home.impl.ui.state.Stories
|
|||
import kotlin.math.max
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.features.home.impl.ui.state.HomeUM
|
||||
import com.tangem.utils.annotations.RemoveWithToggle
|
||||
|
||||
@RemoveWithToggle("AND_15901_STORIES_CONTAINER_ENABLED")
|
||||
@Composable
|
||||
internal fun StoriesScreenV2(state: HomeUM, onGetStartedClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
var currentStory by remember { mutableStateOf(state.firstStory) }
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
package com.tangem.features.home.impl.ui.state
|
||||
|
||||
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 HomeUM(
|
||||
val scanInProgress: Boolean,
|
||||
val isStoriesContainerEnabled: Boolean,
|
||||
val stories: ImmutableList<Stories>,
|
||||
val onShopClick: () -> Unit,
|
||||
val onSearchTokensClick: () -> Unit,
|
||||
val storiesConfig: HomeStoriesConfig,
|
||||
val onGetStartedClick: () -> Unit,
|
||||
) {
|
||||
val firstStory: Stories get() = stories[0]
|
||||
|
|
@ -15,7 +16,17 @@ data class HomeUM(
|
|||
fun stepOf(story: Stories): Int = stories.indexOf(story)
|
||||
}
|
||||
|
||||
enum class Stories(val duration: Int = 6000) {
|
||||
/**
|
||||
* Config for the redesigned Home stories ([StoriesContainer]). The Home intro loops forever and is
|
||||
* not closable, so [isCloseButtonVisible] is `false` and [onClose] keeps its no-op default.
|
||||
*/
|
||||
data class HomeStoriesConfig(
|
||||
override val stories: ImmutableList<Stories>,
|
||||
override val isRestartable: Boolean = true,
|
||||
override val isCloseButtonVisible: Boolean = false,
|
||||
) : StoriesContentConfig<Stories>
|
||||
|
||||
enum class Stories(override val duration: Int = 6000) : StoryConfig {
|
||||
TangemIntro,
|
||||
RevolutionaryWallet,
|
||||
UltraSecureBackup,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,207 @@
|
|||
package com.tangem.features.home.impl.model
|
||||
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.common.routing.entity.InitScreenLaunchMode
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.model.MutableParamsContainer
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.domain.card.ScanCardProcessor
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
|
||||
import com.tangem.domain.settings.usercountry.models.UserCountry
|
||||
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.feature.referral.domain.ShouldShowMobileWalletPromoUseCase
|
||||
import com.tangem.features.home.api.HomeComponent
|
||||
import com.tangem.features.home.api.HomeFeatureToggles
|
||||
import com.tangem.features.home.impl.ui.state.Stories
|
||||
import com.tangem.features.home.impl.ui.state.getRestrictedStories
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class HomeModelTest {
|
||||
|
||||
private val scanCardProcessor: ScanCardProcessor = mockk()
|
||||
private val cardSdkConfigRepository: CardSdkConfigRepository = mockk(relaxed = true)
|
||||
private val settingsRepository: SettingsRepository = mockk()
|
||||
private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true)
|
||||
private val router: Router = mockk(relaxed = true)
|
||||
private val getUserCountryUseCase: GetUserCountryUseCase = mockk()
|
||||
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory = mockk(relaxed = true)
|
||||
private val saveWalletUseCase: SaveWalletUseCase = mockk(relaxed = true)
|
||||
private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxed = true)
|
||||
private val shouldShowMobileWalletPromoUseCase: ShouldShowMobileWalletPromoUseCase = mockk(relaxed = true)
|
||||
private val homeFeatureToggles: HomeFeatureToggles = mockk()
|
||||
private val uiMessageSender: UiMessageSender = mockk(relaxed = true)
|
||||
|
||||
private val progressSlot = slot<suspend (Boolean) -> Unit>()
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
every { homeFeatureToggles.isStoriesContainerEnabled } returns false
|
||||
every { getUserCountryUseCase.invoke() } returns emptyFlow()
|
||||
coEvery { settingsRepository.shouldSaveAccessCodes() } returns false
|
||||
coEvery {
|
||||
scanCardProcessor.scan(
|
||||
analyticsSource = any(),
|
||||
shouldCheckIsAlreadyActivated = any(),
|
||||
cardId = any(),
|
||||
onProgressStateChange = capture(progressSlot),
|
||||
onWalletNotCreated = any(),
|
||||
onCancel = any(),
|
||||
onFailure = any(),
|
||||
onSuccess = any(),
|
||||
)
|
||||
} just Runs
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN toggle enabled WHEN model created THEN isStoriesContainerEnabled is true`() = runTest {
|
||||
// Arrange
|
||||
every { homeFeatureToggles.isStoriesContainerEnabled } returns true
|
||||
|
||||
// Act
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
assertThat(model.uiState.value.isStoriesContainerEnabled).isTrue()
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN toggle disabled WHEN model created THEN isStoriesContainerEnabled is false`() = runTest {
|
||||
// Arrange
|
||||
every { homeFeatureToggles.isStoriesContainerEnabled } returns false
|
||||
|
||||
// Act
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
assertThat(model.uiState.value.isStoriesContainerEnabled).isFalse()
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN model created WHEN no country emitted THEN storiesConfig is non-closable looping and in sync`() =
|
||||
runTest {
|
||||
// Act
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
val state = model.uiState.value
|
||||
assertThat(state.storiesConfig.isRestartable).isTrue()
|
||||
assertThat(state.storiesConfig.isCloseButtonVisible).isFalse()
|
||||
assertThat(state.storiesConfig.stories).isEqualTo(state.stories)
|
||||
assertThat(state.stories).containsExactlyElementsIn(getRestrictedStories()).inOrder()
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN FCA-restricted country WHEN model created THEN Currencies excluded and config in sync`() = runTest {
|
||||
// Arrange
|
||||
every { getUserCountryUseCase.invoke() } returns flowOf(UserCountry.Other(code = "GB").right())
|
||||
|
||||
// Act
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
val state = model.uiState.value
|
||||
assertThat(state.stories).containsExactlyElementsIn(getRestrictedStories()).inOrder()
|
||||
assertThat(state.stories).doesNotContain(Stories.Currencies)
|
||||
assertThat(state.storiesConfig.stories).isEqualTo(state.stories)
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN non-restricted country WHEN model created THEN all stories shown and config in sync`() = runTest {
|
||||
// Arrange
|
||||
every { getUserCountryUseCase.invoke() } returns flowOf(UserCountry.Russia.right())
|
||||
|
||||
// Act
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
val state = model.uiState.value
|
||||
assertThat(state.stories).containsExactlyElementsIn(Stories.entries).inOrder()
|
||||
assertThat(state.storiesConfig.stories).isEqualTo(state.stories)
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN scan in progress WHEN loading toggles THEN storiesConfig instance is not replaced`() = runTest {
|
||||
// Arrange
|
||||
val model = createModel(testScope = this, launchMode = InitScreenLaunchMode.WithCardScan)
|
||||
advanceUntilIdle()
|
||||
val initialConfig = model.uiState.value.storiesConfig
|
||||
|
||||
// Act + Assert — loading on
|
||||
progressSlot.captured.invoke(true)
|
||||
advanceUntilIdle()
|
||||
assertThat(model.uiState.value.scanInProgress).isTrue()
|
||||
assertThat(model.uiState.value.storiesConfig).isSameInstanceAs(initialConfig)
|
||||
|
||||
// Act + Assert — loading off
|
||||
progressSlot.captured.invoke(false)
|
||||
advanceUntilIdle()
|
||||
assertThat(model.uiState.value.scanInProgress).isFalse()
|
||||
assertThat(model.uiState.value.storiesConfig).isSameInstanceAs(initialConfig)
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
private fun createModel(
|
||||
testScope: TestScope,
|
||||
launchMode: InitScreenLaunchMode = InitScreenLaunchMode.Standard,
|
||||
paramsContainer: ParamsContainer = MutableParamsContainer(
|
||||
value = HomeComponent.Params(launchMode = launchMode),
|
||||
),
|
||||
): HomeModel {
|
||||
return HomeModel(
|
||||
paramsContainer = paramsContainer,
|
||||
dispatchers = testScope.createTestingCoroutineDispatcherProvider(),
|
||||
scanCardProcessor = scanCardProcessor,
|
||||
cardSdkConfigRepository = cardSdkConfigRepository,
|
||||
settingsRepository = settingsRepository,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
router = router,
|
||||
getUserCountryUseCase = getUserCountryUseCase,
|
||||
coldUserWalletBuilderFactory = coldUserWalletBuilderFactory,
|
||||
saveWalletUseCase = saveWalletUseCase,
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
shouldShowMobileWalletPromoUseCase = shouldShowMobileWalletPromoUseCase,
|
||||
homeFeatureToggles = homeFeatureToggles,
|
||||
uiMessageSender = uiMessageSender,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TestScope.createTestingCoroutineDispatcherProvider(): TestingCoroutineDispatcherProvider {
|
||||
val testDispatcher = StandardTestDispatcher(testScheduler)
|
||||
return TestingCoroutineDispatcherProvider(
|
||||
main = testDispatcher,
|
||||
mainImmediate = testDispatcher,
|
||||
io = testDispatcher,
|
||||
default = testDispatcher,
|
||||
single = testDispatcher,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue