diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/stories/StoriesContainer.kt b/core/ui/src/main/java/com/tangem/core/ui/components/stories/StoriesContainer.kt index bc292ff0d0..acf73b18d4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/stories/StoriesContainer.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/stories/StoriesContainer.kt @@ -51,7 +51,10 @@ inline fun 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 StoriesContainer( ), ) } - BackHandler(onBack = { config.onClose(watchedCounter) }) + if (config.isCloseButtonVisible) { + BackHandler(onBack = { config.onClose(watchedCounter) }) + } val isPaused = isPressed || isPauseStories @@ -90,22 +95,24 @@ inline fun 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), + ) + } } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/stories/model/StoriesContentConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/stories/model/StoriesContentConfig.kt index 07e03327ab..e8beb9581b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/stories/model/StoriesContentConfig.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/stories/model/StoriesContentConfig.kt @@ -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 { val stories: ImmutableList val isRestartable: Boolean - val onClose: (Int) -> Unit + val isCloseButtonVisible: Boolean get() = true + val onClose: (Int) -> Unit get() = {} } interface StoryConfig { diff --git a/features/home/impl/build.gradle.kts b/features/home/impl/build.gradle.kts index 60bb2f652f..b3051da10d 100644 --- a/features/home/impl/build.gradle.kts +++ b/features/home/impl/build.gradle.kts @@ -69,4 +69,7 @@ dependencies { /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) -} \ No newline at end of file + + /** Tests */ + testImplementation(projects.test.core) +} \ No newline at end of file diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt index 86f2175a22..44a3b475ae 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt @@ -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() - 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 + 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) { diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/Home.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/Home.kt index b245ef97b4..09492c1c74 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/Home.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/Home.kt @@ -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) } \ No newline at end of file diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/HomeStoriesScreen.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/HomeStoriesScreen.kt new file mode 100644 index 0000000000..279753d624 --- /dev/null +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/HomeStoriesScreen.kt @@ -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, + ) + } + } +} \ No newline at end of file diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/StoriesScreenV2.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/StoriesScreenV2.kt index e3a485d2cd..77d8dcb1ea 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/StoriesScreenV2.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/StoriesScreenV2.kt @@ -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) } diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/state/HomeUM.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/state/HomeUM.kt index bdc99712d2..9ab0da9271 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/state/HomeUM.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/state/HomeUM.kt @@ -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, - 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, + override val isRestartable: Boolean = true, + override val isCloseButtonVisible: Boolean = false, +) : StoriesContentConfig + +enum class Stories(override val duration: Int = 6000) : StoryConfig { TangemIntro, RevolutionaryWallet, UltraSecureBackup, diff --git a/features/home/impl/src/test/kotlin/com/tangem/features/home/impl/model/HomeModelTest.kt b/features/home/impl/src/test/kotlin/com/tangem/features/home/impl/model/HomeModelTest.kt new file mode 100644 index 0000000000..089e0b31b4 --- /dev/null +++ b/features/home/impl/src/test/kotlin/com/tangem/features/home/impl/model/HomeModelTest.kt @@ -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 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, + ) + } +} \ No newline at end of file