Updated on 2026-08-14
This commit is contained in:
commit
858a91d095
22 changed files with 387 additions and 103 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -2,38 +2,138 @@ 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,
|
||||
) { child ->
|
||||
child.instance.Content(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,
|
||||
shouldReplace = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDenied() {
|
||||
model.onIntent(
|
||||
AddressSyncIntent.Next(
|
||||
step = AddressSyncStep.ASK_NOTIFICATIONS,
|
||||
shouldReplace = 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,
|
||||
shouldReplace = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDenySystemPermission() {
|
||||
model.onIntent(
|
||||
AddressSyncIntent.Next(
|
||||
step = AddressSyncStep.ADDRESS_SYNC,
|
||||
shouldReplace = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDismiss() {
|
||||
model.onIntent(
|
||||
AddressSyncIntent.Next(
|
||||
step = AddressSyncStep.ADDRESS_SYNC,
|
||||
shouldReplace = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
source = AppRoute.PushNotification.Source.Onboarding,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package com.tangem.features.onboarding.v2.addresssync.entity
|
||||
|
||||
internal data class AddressSyncUM(
|
||||
val onBackClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -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 shouldReplace: Boolean) : AddressSyncIntent
|
||||
data object Back : AddressSyncIntent
|
||||
}
|
||||
|
|
@ -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 shouldShowAskBiometry = canUseBiometryUseCase.strict() && shouldShowAskBiometryUseCase()
|
||||
if (shouldShowAskBiometry.not()) {
|
||||
nextScreen(AddressSyncIntent.Next(step = AddressSyncStep.ASK_NOTIFICATIONS, shouldReplace = true))
|
||||
}
|
||||
}
|
||||
AddressSyncStep.ASK_NOTIFICATIONS -> {
|
||||
val shouldShowAskNotification = shouldAskPermissionUseCase(PUSH_PERMISSION)
|
||||
if (shouldShowAskNotification.not()) {
|
||||
nextScreen(AddressSyncIntent.Next(step = AddressSyncStep.ADDRESS_SYNC, shouldReplace = true))
|
||||
}
|
||||
}
|
||||
AddressSyncStep.ADDRESS_SYNC -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun goBack() {
|
||||
stackNavigation.pop()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.features.onboarding.v2.addresssync.navigation
|
||||
|
||||
enum class AddressSyncStep {
|
||||
ASK_BIOMETRY,
|
||||
ASK_NOTIFICATIONS,
|
||||
ADDRESS_SYNC,
|
||||
}
|
||||
|
|
@ -2,20 +2,19 @@ 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 +23,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]")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ data class OnboardingMultiWalletState(
|
|||
SeedPhrase,
|
||||
ScanPrimary,
|
||||
AddBackupDevice,
|
||||
AddressSync,
|
||||
Finalize,
|
||||
Done,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]")
|
||||
}
|
||||
|
|
@ -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, shouldReplace = 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, shouldReplace = 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, shouldReplace = 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, shouldReplace = 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, shouldReplace = 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue