Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-21 11:50:28 +01:00
parent d30f231520
commit 333640b3a7
22 changed files with 393 additions and 103 deletions

View file

@ -25,7 +25,6 @@ import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.component.ManageTokensMode
import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.nft.component.NFTComponent
import com.tangem.features.onboarding.v2.addresssync.AddressSyncComponent
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent
import com.tangem.features.onramp.component.*
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
@ -112,7 +111,6 @@ internal class ChildFactory @Inject constructor(
private val kycComponentFactory: KycComponent.Factory,
private val yieldSupplyEntryComponentFactory: YieldSupplyEntryComponent.Factory,
private val feedEntryComponentFactory: FeedEntryComponent.Factory,
private val addressSyncComponentFactory: AddressSyncComponent.Factory,
) {
@Suppress("LongMethod", "CyclomaticComplexMethod")
@ -262,6 +260,8 @@ internal class ChildFactory @Inject constructor(
OnboardingEntryComponent.Mode.ContinueFinalize
is AppRoute.Onboarding.Mode.UpgradeHotWallet ->
OnboardingEntryComponent.Mode.UpgradeHotWallet(mode.userWalletId)
is AppRoute.Onboarding.Mode.AddressSync ->
OnboardingEntryComponent.Mode.AddressSync
},
),
componentFactory = onboardingEntryComponentFactory,
@ -694,13 +694,6 @@ internal class ChildFactory @Inject constructor(
componentFactory = feedEntryComponentFactory,
)
}
is AppRoute.AddressSync -> {
createComponentChild(
context = context,
params = AddressSyncComponent.Params(data = Unit),
componentFactory = addressSyncComponentFactory,
)
}
}
}
}

View file

@ -330,6 +330,7 @@ sealed class AppRoute(val path: String) : Route {
data object RecreateWalletTwin : Mode() // reset twins
data object ContinueFinalize : Mode() // continue finalize process (unfinished backup dialog)
data class UpgradeHotWallet(val userWalletId: UserWalletId) : Mode() // upgrade hot wallet
data object AddressSync : Mode()
}
}
@ -484,7 +485,4 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class NewsDetails(val newsId: Int) : AppRoute(path = "/news_details/$newsId")
@Serializable
data object AddressSync : AppRoute(path = "/address_sync")
}

View file

@ -237,7 +237,10 @@ internal class CreateWalletStartModel @Inject constructor(
ifRight = {
setLoading(false)
val route = if (onboardingV2FeatureToggles.isAddressSyncEnabled) {
AppRoute.AddressSync
AppRoute.Onboarding(
scanResponse = scanResponse,
mode = AppRoute.Onboarding.Mode.AddressSync
)
} else {
AppRoute.Wallet
}

View file

@ -531,7 +531,17 @@ internal class CreateWalletStartModelTest {
model.uiState.value.onScanClick.invoke()
advanceUntilIdle()
verify { appRouter.replaceAll(routes = arrayOf(AppRoute.AddressSync), onComplete = any()) }
verify {
appRouter.replaceAll(
routes = arrayOf(
AppRoute.Onboarding(
scanResponse = testScanResponse,
mode = AppRoute.Onboarding.Mode.AddressSync,
)
),
onComplete = any()
)
}
}
@Test
@ -573,9 +583,9 @@ internal class CreateWalletStartModelTest {
)
assert(state.imageResId == R.drawable.img_hardware_wallet)
assert(state.shouldShowScanSecondaryButton)
assert(state.primaryButtonText == resourceReference(R.string.details_buy_wallet),)
assert(state.otherMethodTitle == resourceReference(R.string.welcome_create_wallet_mobile_title),)
assert(state.otherMethodDescription == null,)
assert(state.primaryButtonText == resourceReference(R.string.details_buy_wallet))
assert(state.otherMethodTitle == resourceReference(R.string.welcome_create_wallet_mobile_title))
assert(state.otherMethodDescription == null)
assert(state.isScanInProgress.not())
}

View file

@ -19,6 +19,7 @@ interface OnboardingEntryComponent : ComposableContentComponent {
data object RecreateWalletTwin : Mode()
data object ContinueFinalize : Mode()
data class UpgradeHotWallet(val userWalletId: UserWalletId) : Mode()
data object AddressSync : Mode()
}
interface Factory : ComponentFactory<Params, OnboardingEntryComponent>

View file

@ -11,11 +11,16 @@ android {
namespace = "com.tangem.features.onboarding.v2.impl"
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
dependencies {
/** Api */
implementation(projects.features.onboardingV2.api)
implementation(projects.features.manageTokens.api)
implementation(projects.features.biometry.api)
implementation(projects.features.pushNotifications.api)
implementation(projects.features.hotWallet.api)
implementation(projects.features.tokenRecieve.api)
@ -87,4 +92,11 @@ dependencies {
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
/** Test */
testImplementation(deps.test.junit5)
testRuntimeOnly(deps.test.junit5.engine)
testImplementation(deps.test.mockk)
testImplementation(deps.test.truth)
testImplementation(deps.test.coroutine)
}

View file

@ -1,13 +1,5 @@
package com.tangem.features.onboarding.v2.addresssync
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
interface AddressSyncComponent : ComposableContentComponent {
data class Params(
val data: Any, // TODO("Will be implemented during [REDACTED_TASK_KEY]")
)
interface Factory : ComponentFactory<Params, AddressSyncComponent>
}
interface AddressSyncComponent : ComposableContentComponent

View file

@ -2,38 +2,141 @@ package com.tangem.features.onboarding.v2.addresssync
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.DelicateDecomposeApi
import com.arkivanov.decompose.extensions.compose.stack.Children
import com.arkivanov.decompose.router.stack.ChildStack
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.value.Value
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.onboarding.v2.addresssync.model.AddressSyncIntent
import com.tangem.features.onboarding.v2.addresssync.model.AddressSyncModel
import com.tangem.features.onboarding.v2.addresssync.navigation.AddressSyncStep
import com.tangem.features.onboarding.v2.addresssync.ui.AddressSyncContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks
import com.tangem.features.pushnotifications.api.PushNotificationsParams
internal class DefaultAddressSyncComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: AddressSyncComponent.Params,
@OptIn(DelicateDecomposeApi::class)
internal class DefaultAddressSyncComponent(
appComponentContext: AppComponentContext,
private val askBiometryComponentFactory: AskBiometryComponent.Factory,
private val pushNotificationsComponentFactory: PushNotificationsComponent.Factory,
) : AppComponentContext by appComponentContext, AddressSyncComponent {
private val model: AddressSyncModel = getOrCreateModel(params)
private val model: AddressSyncModel = getOrCreateModel()
private val childStack: Value<ChildStack<AddressSyncStep, ComposableContentComponent>> =
childStack(
key = "innerStack",
source = model.stackNavigation,
serializer = null,
initialConfiguration = AddressSyncStep.ASK_BIOMETRY,
handleBackButton = true,
childFactory = { configuration, factoryContext ->
createChild(
step = configuration,
childContext = childByContext(factoryContext),
)
},
)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
AddressSyncContent(
modifier = modifier,
childContent = {
Children(
stack = childStack
) {
it.instance.Content(modifier = modifier)
}
}
)
AddressSyncContent(modifier = modifier, state = state)
BackHandler(onBack = state.onBackClick)
BackHandler {
model.onIntent(AddressSyncIntent.Back)
}
}
@AssistedFactory
interface Factory : AddressSyncComponent.Factory {
override fun create(
context: AppComponentContext,
params: AddressSyncComponent.Params,
): DefaultAddressSyncComponent
private fun createChild(
step: AddressSyncStep,
childContext: AppComponentContext,
): ComposableContentComponent {
return when (step) {
AddressSyncStep.ASK_BIOMETRY -> createAskBiometryComponent(childContext)
AddressSyncStep.ASK_NOTIFICATIONS -> createPushNotificationComponent(childContext)
AddressSyncStep.ADDRESS_SYNC -> TODO("Will be implemented during [REDACTED_TASK_KEY]")
}
}
private fun createAskBiometryComponent(childContext: AppComponentContext): AskBiometryComponent {
return askBiometryComponentFactory.create(
context = childContext,
params = AskBiometryComponent.Params(
isBottomSheetVariant = false,
modelCallbacks = object : AskBiometryComponent.ModelCallbacks {
override fun onAllowed() {
model.onIntent(
AddressSyncIntent.Next(
step = AddressSyncStep.ASK_NOTIFICATIONS,
replace = true
)
)
}
override fun onDenied() {
model.onIntent(
AddressSyncIntent.Next(
step = AddressSyncStep.ASK_NOTIFICATIONS,
replace = false
)
)
}
}
)
)
}
private fun createPushNotificationComponent(childContext: AppComponentContext): PushNotificationsComponent {
return pushNotificationsComponentFactory.create(
context = childContext,
params = PushNotificationsParams(
modelCallbacks = object : PushNotificationsModelCallbacks {
override fun onAllowSystemPermission() {
model.onIntent(
AddressSyncIntent.Next(
step = AddressSyncStep.ADDRESS_SYNC,
replace = true
)
)
}
override fun onDenySystemPermission() {
model.onIntent(
AddressSyncIntent.Next(
step = AddressSyncStep.ADDRESS_SYNC,
replace = false
)
)
}
override fun onDismiss() {
model.onIntent(
AddressSyncIntent.Next(
step = AddressSyncStep.ADDRESS_SYNC,
replace = false
)
)
}
},
source = AppRoute.PushNotification.Source.Onboarding,
)
)
}
}

View file

@ -1,18 +0,0 @@
package com.tangem.features.onboarding.v2.addresssync.di
import com.tangem.features.onboarding.v2.addresssync.AddressSyncComponent
import com.tangem.features.onboarding.v2.addresssync.DefaultAddressSyncComponent
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 AddressSyncComponentModule {
@Binds
@Singleton
fun bindAddressSyncComponentFactory(factory: DefaultAddressSyncComponent.Factory): AddressSyncComponent.Factory
}

View file

@ -1,5 +0,0 @@
package com.tangem.features.onboarding.v2.addresssync.entity
internal data class AddressSyncUM(
val onBackClick: () -> Unit,
)

View file

@ -0,0 +1,8 @@
package com.tangem.features.onboarding.v2.addresssync.model
import com.tangem.features.onboarding.v2.addresssync.navigation.AddressSyncStep
sealed interface AddressSyncIntent {
data class Next(val step: AddressSyncStep, val replace: Boolean) : AddressSyncIntent
data object Back : AddressSyncIntent
}

View file

@ -1,22 +1,68 @@
package com.tangem.features.onboarding.v2.addresssync.model
import com.arkivanov.decompose.DelicateDecomposeApi
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.decompose.router.stack.push
import com.arkivanov.decompose.router.stack.replaceCurrent
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.features.onboarding.v2.addresssync.entity.AddressSyncUM
import com.tangem.domain.settings.CanUseBiometryUseCase
import com.tangem.domain.settings.ShouldAskPermissionUseCase
import com.tangem.domain.settings.ShouldShowAskBiometryUseCase
import com.tangem.features.onboarding.v2.addresssync.navigation.AddressSyncStep
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@OptIn(DelicateDecomposeApi::class)
@ModelScoped
internal class AddressSyncModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val shouldShowAskBiometryUseCase: ShouldShowAskBiometryUseCase,
private val canUseBiometryUseCase: CanUseBiometryUseCase,
private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase,
) : Model() {
val uiState: StateFlow<AddressSyncUM>
field: MutableStateFlow<AddressSyncUM> = MutableStateFlow(getInitialState())
val stackNavigation = StackNavigation<AddressSyncStep>()
private fun getInitialState(): AddressSyncUM {
TODO("Will be implemented during [REDACTED_TASK_KEY]")
fun onIntent(intent: AddressSyncIntent) {
when (intent) {
is AddressSyncIntent.Next -> nextScreen(intent)
AddressSyncIntent.Back -> goBack()
}
}
private fun nextScreen(next: AddressSyncIntent.Next) {
val (nextStep, replace) = next
if (replace) {
stackNavigation.replaceCurrent(configuration = nextStep)
} else {
stackNavigation.push(configuration = nextStep)
}
modelScope.launch { trySkippingScreen(next) }
}
private suspend fun trySkippingScreen(next: AddressSyncIntent.Next) {
when (next.step) {
AddressSyncStep.ASK_BIOMETRY -> {
val showBiometry = canUseBiometryUseCase.strict() && shouldShowAskBiometryUseCase()
if (showBiometry.not()) {
nextScreen(AddressSyncIntent.Next(step = AddressSyncStep.ASK_NOTIFICATIONS, replace = true))
}
}
AddressSyncStep.ASK_NOTIFICATIONS -> {
val showNotification = shouldAskPermissionUseCase(PUSH_PERMISSION)
if (showNotification.not()) {
nextScreen(AddressSyncIntent.Next(step = AddressSyncStep.ADDRESS_SYNC, replace = true))
}
}
AddressSyncStep.ADDRESS_SYNC -> Unit
}
}
private fun goBack() {
stackNavigation.pop()
}
}

View file

@ -0,0 +1,7 @@
package com.tangem.features.onboarding.v2.addresssync.navigation
enum class AddressSyncStep {
ASK_BIOMETRY,
ASK_NOTIFICATIONS,
ADDRESS_SYNC,
}

View file

@ -2,20 +2,22 @@ package com.tangem.features.onboarding.v2.addresssync.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.onboarding.v2.addresssync.entity.AddressSyncUM
@Composable
internal fun AddressSyncContent(state: AddressSyncUM, modifier: Modifier = Modifier) {
internal fun AddressSyncContent(
modifier: Modifier = Modifier,
childContent: @Composable (Modifier) -> Unit = {},
) {
Column(
modifier = modifier
.background(color = TangemTheme.colors.background.secondary)
@ -24,31 +26,15 @@ internal fun AddressSyncContent(state: AddressSyncUM, modifier: Modifier = Modif
.systemBarsPadding(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
AppBarWithBackButton(
onBackClick = state.onBackClick,
modifier = Modifier.height(TangemTheme.dimens.size56),
)
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = TangemTheme.dimens.spacing16)
.weight(1f),
) {}
childContent(modifier)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun AddressSyncContentPreview(@PreviewParameter(PreviewStateProvider::class) state: AddressSyncUM) {
private fun AddressSyncContentPreview() {
TangemThemePreview {
AddressSyncContent(state = state)
AddressSyncContent()
}
}
private class PreviewStateProvider : CollectionPreviewParameterProvider<AddressSyncUM>(
buildList {
TODO("Will be implemented during [REDACTED_TASK_KEY]")
},
)
}

View file

@ -82,6 +82,7 @@ internal class OnboardingEntryModel @Inject constructor(
is Mode.UpgradeHotWallet -> OnboardingMultiWalletComponent.Mode.UpgradeHotWallet(
userWalletId = mode.userWalletId,
)
is Mode.AddressSync -> OnboardingMultiWalletComponent.Mode.AddressSync
else -> error("Incorrect onboarding type")
}

View file

@ -24,6 +24,7 @@ interface OnboardingMultiWalletComponent : ComposableContentComponent, InnerNavi
data object AddBackup : Mode()
data object ContinueFinalize : Mode()
data class UpgradeHotWallet(val userWalletId: UserWalletId) : Mode()
data object AddressSync : Mode()
}
interface Factory : ComponentFactory<Params, OnboardingMultiWalletComponent>

View file

@ -25,8 +25,10 @@ import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.decompose.navigation.inner.InnerNavigation
import com.tangem.core.decompose.navigation.inner.InnerNavigationState
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.onboarding.v2.addresssync.DefaultAddressSyncComponent
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.MultiWalletAccessCodeComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.MultiWalletBackupComponent
@ -42,6 +44,7 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiW
import com.tangem.features.onboarding.v2.multiwallet.impl.ui.OnboardingMultiWallet
import com.tangem.features.onboarding.v2.multiwallet.impl.ui.WalletArtworksState
import com.tangem.features.onboarding.v2.util.ResetCardsComponent
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -53,6 +56,8 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
@Assisted private val params: OnboardingMultiWalletComponent.Params,
private val analyticsHandler: AnalyticsEventHandler,
private val resetCardsComponentFactory: ResetCardsComponent.Factory,
private val askBiometryComponentFactory: AskBiometryComponent.Factory,
private val pushNotificationsComponentFactory: PushNotificationsComponent.Factory,
) : OnboardingMultiWalletComponent, AppComponentContext by context {
private val model: OnboardingMultiWalletModel = getOrCreateModel(params)
@ -60,6 +65,7 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
private val artworksState = instanceKeeper.getOrCreateSimple(key = "artworksState") {
MutableStateFlow(
when (model.state.value.currentStep) {
AddressSync -> WalletArtworksState.Hidden
UpgradeWallet -> WalletArtworksState.Folded
CreateWallet -> WalletArtworksState.Folded
ChooseBackupOption -> WalletArtworksState.Fan
@ -186,6 +192,11 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
onBack = { model.onBack() },
onEvent = ::handleFinalizeComponentEvent,
)
AddressSync -> DefaultAddressSyncComponent(
appComponentContext = childContext,
askBiometryComponentFactory = askBiometryComponentFactory,
pushNotificationsComponentFactory = pushNotificationsComponentFactory,
)
Done -> error("Unexpected Done state")
}
}

View file

@ -265,6 +265,7 @@ internal class MultiWalletFinalizeModel @Inject constructor(
val userWallet = when (params.parentParams.mode) {
OnboardingMultiWalletComponent.Mode.Onboarding,
OnboardingMultiWalletComponent.Mode.ContinueFinalize,
OnboardingMultiWalletComponent.Mode.AddressSync,
-> {
saveWalletUseCase.invoke(
userWallet = userWalletCreated.copy(

View file

@ -120,8 +120,12 @@ internal class OnboardingMultiWalletModel @Inject constructor(
params.mode is OnboardingMultiWalletComponent.Mode.UpgradeHotWallet -> {
OnboardingMultiWalletState.Step.UpgradeWallet
}
params.mode == OnboardingMultiWalletComponent.Mode.ContinueFinalize ->
params.mode == OnboardingMultiWalletComponent.Mode.ContinueFinalize -> {
OnboardingMultiWalletState.Step.Finalize
}
params.mode == OnboardingMultiWalletComponent.Mode.AddressSync -> {
OnboardingMultiWalletState.Step.AddressSync
}
// Add backup button
// Wallet1 without backup and userwallet's scanResponse doesn't contain primary card.
card.wallets.isNotEmpty() && card.backupStatus == CardDTO.BackupStatus.NoBackup &&

View file

@ -38,6 +38,7 @@ data class OnboardingMultiWalletState(
SeedPhrase,
ScanPrimary,
AddBackupDevice,
AddressSync,
Finalize,
Done,
}

View file

@ -15,4 +15,5 @@ fun screenTitleByStep(step: OnboardingMultiWalletState.Step): TextReference = wh
OnboardingMultiWalletState.Step.Finalize -> resourceReference(R.string.onboarding_button_finalize_backup)
OnboardingMultiWalletState.Step.Done -> resourceReference(R.string.common_done)
OnboardingMultiWalletState.Step.UpgradeWallet -> resourceReference(R.string.common_tangem)
OnboardingMultiWalletState.Step.AddressSync -> TODO("Will be implemented during [REDACTED_TASK_KEY]")
}

View file

@ -0,0 +1,134 @@
package com.tangem.features.onboarding.v2.addresssync.model
import com.arkivanov.decompose.router.stack.StackNavigation
import com.tangem.domain.settings.CanUseBiometryUseCase
import com.tangem.domain.settings.ShouldAskPermissionUseCase
import com.tangem.domain.settings.ShouldShowAskBiometryUseCase
import com.tangem.features.onboarding.v2.addresssync.navigation.AddressSyncStep
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
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 AddressSyncModelTest {
private val shouldShowAskBiometryUseCase: ShouldShowAskBiometryUseCase = mockk()
private val canUseBiometryUseCase: CanUseBiometryUseCase = mockk()
private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase = mockk()
@BeforeEach
fun setUp() {
coEvery { canUseBiometryUseCase.strict() } returns false
coEvery { shouldShowAskBiometryUseCase() } returns false
coEvery { shouldAskPermissionUseCase(PUSH_PERMISSION) } returns false
}
@Test
fun `GIVEN biometry allowed AND should show biometry WHEN Next ASK_BIOMETRY THEN ASK_BIOMETRY on top`() = runTest {
coEvery { canUseBiometryUseCase.strict() } returns true
coEvery { shouldShowAskBiometryUseCase() } returns true
val model = createModel(this)
val stack = model.stackNavigation.trackStack()
model.onIntent(AddressSyncIntent.Next(step = AddressSyncStep.ASK_BIOMETRY, replace = false))
advanceUntilIdle()
assert(stack == listOf(AddressSyncStep.ASK_BIOMETRY))
}
@Test
fun `GIVEN biometry skipped AND notifications required WHEN Next ASK_BIOMETRY THEN ASK_NOTIFICATIONS on top`() =
runTest {
coEvery { canUseBiometryUseCase.strict() } returns true
coEvery { shouldShowAskBiometryUseCase() } returns false
coEvery { shouldAskPermissionUseCase(PUSH_PERMISSION) } returns true
val model = createModel(this)
val stack = model.stackNavigation.trackStack()
model.onIntent(AddressSyncIntent.Next(step = AddressSyncStep.ASK_BIOMETRY, replace = false))
advanceUntilIdle()
assert(stack == listOf(AddressSyncStep.ASK_NOTIFICATIONS))
}
@Test
fun `GIVEN biometry skipped AND notifications skipped WHEN Next ASK_BIOMETRY THEN ADDRESS_SYNC on top`() = runTest {
coEvery { canUseBiometryUseCase.strict() } returns true
coEvery { shouldShowAskBiometryUseCase() } returns false
coEvery { shouldAskPermissionUseCase(PUSH_PERMISSION) } returns false
val model = createModel(this)
val stack = model.stackNavigation.trackStack()
model.onIntent(AddressSyncIntent.Next(step = AddressSyncStep.ASK_BIOMETRY, replace = false))
advanceUntilIdle()
assert(stack == listOf(AddressSyncStep.ADDRESS_SYNC))
}
@Test
fun `GIVEN notifications required WHEN Next ASK_NOTIFICATIONS THEN ASK_NOTIFICATIONS on top`() = runTest {
coEvery { shouldAskPermissionUseCase(PUSH_PERMISSION) } returns true
val model = createModel(this)
val stack = model.stackNavigation.trackStack()
model.onIntent(AddressSyncIntent.Next(step = AddressSyncStep.ASK_NOTIFICATIONS, replace = false))
advanceUntilIdle()
assert(stack == listOf(AddressSyncStep.ASK_NOTIFICATIONS))
}
@Test
fun `GIVEN notifications skipped WHEN Next ASK_NOTIFICATIONS THEN ADDRESS_SYNC on top`() = runTest {
coEvery { shouldAskPermissionUseCase(PUSH_PERMISSION) } returns false
val model = createModel(this)
val stack = model.stackNavigation.trackStack()
model.onIntent(AddressSyncIntent.Next(step = AddressSyncStep.ASK_NOTIFICATIONS, replace = false))
advanceUntilIdle()
assert(stack == listOf(AddressSyncStep.ADDRESS_SYNC))
}
private fun StackNavigation<AddressSyncStep>.trackStack(): List<AddressSyncStep> {
val tracked = mutableListOf<AddressSyncStep>()
subscribe { event ->
val newStack = event.transformer(tracked.toList())
tracked.clear()
tracked.addAll(newStack)
}
return tracked
}
private fun createModel(testScope: TestScope): AddressSyncModel {
return AddressSyncModel(
dispatchers = testScope.createTestingCoroutineDispatcherProvider(),
shouldShowAskBiometryUseCase = shouldShowAskBiometryUseCase,
canUseBiometryUseCase = canUseBiometryUseCase,
shouldAskPermissionUseCase = shouldAskPermissionUseCase,
)
}
private fun TestScope.createTestingCoroutineDispatcherProvider(): TestingCoroutineDispatcherProvider {
val testDispatcher = StandardTestDispatcher(testScheduler)
return TestingCoroutineDispatcherProvider(
main = testDispatcher,
mainImmediate = testDispatcher,
io = testDispatcher,
default = testDispatcher,
single = testDispatcher,
)
}
}