Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-21 09:28:48 +01:00
parent 0bedc513a7
commit c846d8c276
15 changed files with 862 additions and 3 deletions

View file

@ -25,6 +25,7 @@ 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
@ -111,6 +112,7 @@ 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")
@ -692,6 +694,13 @@ internal class ChildFactory @Inject constructor(
componentFactory = feedEntryComponentFactory,
)
}
is AppRoute.AddressSync -> {
createComponentChild(
context = context,
params = AddressSyncComponent.Params(data = Unit),
componentFactory = addressSyncComponentFactory,
)
}
}
}
}

View file

@ -484,4 +484,7 @@ 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

@ -7,7 +7,6 @@
"name": "VISA_ONBOARDING_ENABLED",
"version": "undefined"
},
{
"name": "STAKING_ETH_ENABLED",
"version": "undefined"
@ -67,5 +66,9 @@
{
"name": "WALLET_CONNECT_BITCOIN_ENABLED",
"version": "undefined"
},
{
"name": "ADDRESS_SYNC_ENABLED",
"version": "undefined"
}
]

View file

@ -11,10 +11,15 @@ android {
namespace = "com.tangem.features.createwalletstart.impl"
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
dependencies {
/** Api */
implementation(projects.features.createWalletStart.api)
implementation(projects.features.hotWallet.api)
implementation(projects.features.onboardingV2.api)
/** Project - Domain */
implementation(projects.domain.card)
@ -71,4 +76,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

@ -33,14 +33,15 @@ import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.features.createwalletstart.entity.CreateWalletStartUM
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.logging.TangemLogger
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import com.tangem.utils.logging.TangemLogger
import javax.inject.Inject
private const val HIDE_PROGRESS_DELAY = 400L
@ -65,6 +66,7 @@ internal class CreateWalletStartModel @Inject constructor(
private val trackingContextProxy: TrackingContextProxy,
private val analyticsEventHandler: AnalyticsEventHandler,
private val appsFlyerStore: AppsFlyerStore,
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
) : Model() {
private val params = paramsContainer.require<CreateWalletStartComponent.Params>()
@ -234,7 +236,12 @@ internal class CreateWalletStartModel @Inject constructor(
},
ifRight = {
setLoading(false)
appRouter.replaceAll(AppRoute.Wallet)
val route = if (onboardingV2FeatureToggles.isAddressSyncEnabled) {
AppRoute.AddressSync
} else {
AppRoute.Wallet
}
appRouter.replaceAll(route)
},
)
}

View file

@ -0,0 +1,651 @@
package com.tangem.features.createwalletstart
import arrow.core.left
import arrow.core.right
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent
import com.tangem.core.analytics.utils.TrackingContextProxy
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.core.navigation.url.UrlOpener
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.analytics.IntroductionProcess
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.error.SaveWalletError
import com.tangem.domain.hotwallet.IsHotWalletCreationSupported
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.models.AppsFlyerConversionData
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.features.createwalletstart.entity.CreateWalletStartUM
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.*
import kotlinx.collections.immutable.persistentListOf
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 CreateWalletStartModelTest {
private val router: Router = mockk(relaxed = true)
private val appRouter: AppRouter = mockk(relaxed = true)
private val scanCardProcessor: ScanCardProcessor = mockk()
private val cardSdkConfigRepository: CardSdkConfigRepository = mockk(relaxed = true)
private val settingsRepository: SettingsRepository = mockk()
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory = mockk()
private val coldUserWalletBuilder: ColdUserWalletBuilder = mockk()
private val saveWalletUseCase: SaveWalletUseCase = mockk()
private val isHotWalletCreationSupported: IsHotWalletCreationSupported = mockk()
private val userWalletsListRepository: UserWalletsListRepository = mockk()
private val uiMessageSender: UiMessageSender = mockk(relaxed = true)
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase = mockk()
private val urlOpener: UrlOpener = mockk(relaxed = true)
private val trackingContextProxy: TrackingContextProxy = mockk(relaxed = true)
private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true)
private val appsFlyerStore: AppsFlyerStore = mockk()
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles = mockk()
private val testUserWalletId = UserWalletId("1234567890ABCDEF")
private val testScanResponse: ScanResponse = mockk(relaxed = true)
private val testColdWallet: UserWallet.Cold = mockk(relaxed = true) {
every { walletId } returns testUserWalletId
}
@BeforeEach
fun setUp() {
coEvery { appsFlyerStore.get() } returns null
coEvery { settingsRepository.shouldSaveAccessCodes() } returns false
every { coldUserWalletBuilderFactory.create(any()) } returns coldUserWalletBuilder
every { coldUserWalletBuilder.build() } returns testColdWallet
every { onboardingV2FeatureToggles.isAddressSyncEnabled } returns false
coEvery {
scanCardProcessor.scan(
analyticsSource = any(),
shouldCheckIsAlreadyActivated = any(),
cardId = any(),
onProgressStateChange = any(),
onWalletNotCreated = any(),
disclaimerWillShow = any(),
onCancel = any(),
onFailure = any(),
onSuccess = any()
)
} just Runs
}
@Test
fun `GIVEN ColdWallet mode WHEN onScanClick THEN ButtonScanCard event sent AND scan called`() = runTest {
val model = createModel(testScope = this, mode = CreateWalletStartComponent.Mode.ColdWallet)
advanceUntilIdle()
model.uiState.value.onScanClick.invoke()
advanceUntilIdle()
verify {
analyticsEventHandler.send(
match<IntroductionProcess.ButtonScanCard> {
it.source == AnalyticsParam.ScreensSources.CreateWalletIntro
},
)
}
coVerify {
scanCardProcessor.scan(
analyticsSource = AnalyticsParam.ScreensSources.Intro,
shouldCheckIsAlreadyActivated = true,
cardId = null,
onProgressStateChange = any(),
onWalletNotCreated = any(),
disclaimerWillShow = any(),
onCancel = any(),
onFailure = any(),
onSuccess = any()
)
}
}
@Test
fun `GIVEN HotWallet mode WHEN onScanClick THEN ButtonScanCard event sent AND scan called`() = runTest {
val model = createModel(testScope = this, mode = CreateWalletStartComponent.Mode.HotWallet)
advanceUntilIdle()
model.uiState.value.onScanClick.invoke()
advanceUntilIdle()
verify {
analyticsEventHandler.send(
match<IntroductionProcess.ButtonScanCard> {
it.source == AnalyticsParam.ScreensSources.CreateWalletIntro
},
)
}
coVerify {
scanCardProcessor.scan(
analyticsSource = AnalyticsParam.ScreensSources.Intro,
shouldCheckIsAlreadyActivated = true,
cardId = null,
onProgressStateChange = any(),
onWalletNotCreated = any(),
disclaimerWillShow = any(),
onCancel = any(),
onFailure = any(),
onSuccess = any()
)
}
}
@Test
fun `GIVEN ColdWallet AND hot wallet not supported WHEN otherMethodClick THEN dialog sent`() = runTest {
every { isHotWalletCreationSupported() } returns false
every { isHotWalletCreationSupported.getLeastVersionName() } returns "13"
val model = createModel(testScope = this, mode = CreateWalletStartComponent.Mode.ColdWallet)
advanceUntilIdle()
model.uiState.value.otherMethodClick.invoke()
advanceUntilIdle()
verify { trackingContextProxy.addHotWalletContext() }
verify {
analyticsEventHandler.send(
match<OnboardingAnalyticsEvent.Onboarding.ButtonMobileWallet> { true },
)
}
verify { uiMessageSender.send(match<DialogMessage> { true }) }
verify(exactly = 0) { router.push(any(), any()) }
}
@Test
fun `GIVEN HotWallet AND hot wallet supported WHEN onPrimaryButtonClick THEN CreateMobileWallet pushed`() =
runTest {
every { isHotWalletCreationSupported() } returns true
val model = createModel(testScope = this, mode = CreateWalletStartComponent.Mode.HotWallet)
advanceUntilIdle()
model.uiState.value.onPrimaryButtonClick.invoke()
advanceUntilIdle()
verify { trackingContextProxy.addHotWalletContext() }
verify {
analyticsEventHandler.send(
match<OnboardingAnalyticsEvent.Onboarding.ButtonMobileWallet> { true },
)
}
verify {
router.push(
route = AppRoute.CreateMobileWallet(
source = AnalyticsParam.ScreensSources.CreateWalletIntro.value,
),
onComplete = any(),
)
}
verify(exactly = 0) { uiMessageSender.send(any()) }
}
@Test
fun `GIVEN ColdWallet mode WHEN onPrimaryButtonClick THEN buy link opened`() = runTest {
val testUrl = "https://buy.tangem.com"
coEvery {
generateBuyTangemCardLinkUseCase.invoke(GenerateBuyTangemCardLinkUseCase.Source.Creation)
} returns testUrl
val model = createModel(testScope = this, mode = CreateWalletStartComponent.Mode.ColdWallet)
advanceUntilIdle()
model.uiState.value.onPrimaryButtonClick.invoke()
advanceUntilIdle()
verify {
analyticsEventHandler.send(
match<Basic.ButtonBuy> { true },
)
}
coVerify { generateBuyTangemCardLinkUseCase.invoke(GenerateBuyTangemCardLinkUseCase.Source.Creation) }
verify { urlOpener.openUrl(testUrl) }
}
@Test
fun `GIVEN HotWallet mode WHEN onBuyClick THEN buy link opened`() = runTest {
val testUrl = "https://buy.tangem.com"
coEvery {
generateBuyTangemCardLinkUseCase.invoke(GenerateBuyTangemCardLinkUseCase.Source.Creation)
} returns testUrl
val model = createModel(testScope = this, mode = CreateWalletStartComponent.Mode.HotWallet)
advanceUntilIdle()
model.uiState.value.otherMethodClick.invoke()
advanceUntilIdle()
verify {
analyticsEventHandler.send(
match<Basic.ButtonBuy> { true },
)
}
coVerify { generateBuyTangemCardLinkUseCase.invoke(GenerateBuyTangemCardLinkUseCase.Source.Creation) }
verify { urlOpener.openUrl(testUrl) }
}
@Test
fun `WHEN scanCard THEN access code policy set AND scanProcessor called`() = runTest {
coEvery { settingsRepository.shouldSaveAccessCodes() } returns true
val model = createModel(testScope = this, mode = CreateWalletStartComponent.Mode.ColdWallet)
advanceUntilIdle()
model.uiState.value.onScanClick.invoke()
advanceUntilIdle()
verify { cardSdkConfigRepository.setAccessCodeRequestPolicy(isBiometricsRequestPolicy = true) }
coVerify {
scanCardProcessor.scan(
analyticsSource = AnalyticsParam.ScreensSources.Intro,
shouldCheckIsAlreadyActivated = true,
cardId = null,
onProgressStateChange = any(),
onWalletNotCreated = any(),
disclaimerWillShow = any(),
onCancel = any(),
onFailure = any(),
onSuccess = any()
)
}
}
@Test
fun `GIVEN NfcFeatureIsUnavailable WHEN handleScanError THEN nfcFeatureUnavailable dialog sent`() = runTest {
val error = TangemSdkError.NfcFeatureIsUnavailable()
coEvery {
scanCardProcessor.scan(
analyticsSource = any(),
shouldCheckIsAlreadyActivated = any(),
cardId = any(),
onProgressStateChange = any(),
onWalletNotCreated = any(),
disclaimerWillShow = any(),
onCancel = any(),
onFailure = any(),
onSuccess = any()
)
} coAnswers {
val onFailure = arg<suspend (TangemError) -> Unit>(7)
onFailure.invoke(error)
}
val model = createModel(testScope = this, mode = CreateWalletStartComponent.Mode.ColdWallet)
advanceUntilIdle()
model.uiState.value.onScanClick.invoke()
advanceUntilIdle()
verify { uiMessageSender.send(match<DialogMessage> { true }) }
}
@Test
fun `GIVEN generic TangemSdkError WHEN handleScanError THEN no dialog sent`() = runTest {
val error: TangemSdkError = mockk(relaxed = true)
coEvery {
scanCardProcessor.scan(
analyticsSource = any(),
shouldCheckIsAlreadyActivated = any(),
cardId = any(),
onProgressStateChange = any(),
onWalletNotCreated = any(),
disclaimerWillShow = any(),
onCancel = any(),
onFailure = any(),
onSuccess = any()
)
} coAnswers {
val onFailure = arg<suspend (TangemError) -> Unit>(7)
onFailure.invoke(error)
}
val model = createModel(testScope = this, mode = CreateWalletStartComponent.Mode.ColdWallet)
advanceUntilIdle()
model.uiState.value.onScanClick.invoke()
advanceUntilIdle()
verify(exactly = 0) { uiMessageSender.send(any()) }
}
@Test
fun `GIVEN non-sdk TangemError WHEN handleScanError THEN no dialog sent`() = runTest {
val error: TangemError = mockk(relaxed = true)
coEvery {
scanCardProcessor.scan(
analyticsSource = any(),
shouldCheckIsAlreadyActivated = any(),
cardId = any(),
onProgressStateChange = any(),
onWalletNotCreated = any(),
disclaimerWillShow = any(),
onCancel = any(),
onFailure = any(),
onSuccess = any()
)
} coAnswers {
val onFailure = arg<suspend (TangemError) -> Unit>(7)
onFailure.invoke(error)
}
val model = createModel(testScope = this, mode = CreateWalletStartComponent.Mode.ColdWallet)
advanceUntilIdle()
model.uiState.value.onScanClick.invoke()
advanceUntilIdle()
verify(exactly = 0) { uiMessageSender.send(any()) }
}
@Test
fun `GIVEN builder returns null WHEN proceedWithScanResponse THEN saveWalletUseCase not called`() = runTest {
every { coldUserWalletBuilder.build() } returns null
coEvery {
scanCardProcessor.scan(
analyticsSource = any(),
shouldCheckIsAlreadyActivated = any(),
cardId = any(),
onProgressStateChange = any(),
onWalletNotCreated = any(),
disclaimerWillShow = any(),
onCancel = any(),
onFailure = any(),
onSuccess = any()
)
} coAnswers {
val onSuccess = arg<suspend (ScanResponse) -> Unit>(8)
onSuccess.invoke(testScanResponse)
}
val model = createModel(testScope = this, mode = CreateWalletStartComponent.Mode.ColdWallet)
advanceUntilIdle()
model.uiState.value.onScanClick.invoke()
advanceUntilIdle()
verify { coldUserWalletBuilderFactory.create(scanResponse = testScanResponse) }
coVerify(exactly = 0) { saveWalletUseCase(userWallet = any(), canOverride = any(), analyticsSource = any()) }
verify(exactly = 0) { appRouter.replaceAll(routes = anyVararg(), onComplete = any()) }
}
@Test
fun `GIVEN save returns WalletAlreadySaved WHEN proceedWithScanResponse THEN unlock called AND Wallet replaced`() =
runTest {
coEvery {
saveWalletUseCase(userWallet = any(), canOverride = any(), analyticsSource = any())
} returns SaveWalletError.WalletAlreadySaved(messageId = 0).left()
coEvery {
userWalletsListRepository.unlock(
userWalletId = testUserWalletId,
unlockMethod = any(),
)
} returns Unit.right()
coEvery {
scanCardProcessor.scan(
analyticsSource = any(),
shouldCheckIsAlreadyActivated = any(),
cardId = any(),
onProgressStateChange = any(),
onWalletNotCreated = any(),
disclaimerWillShow = any(),
onCancel = any(),
onFailure = any(),
onSuccess = any()
)
} coAnswers {
val onSuccess = arg<suspend (ScanResponse) -> Unit>(8)
onSuccess.invoke(testScanResponse)
}
val model = createModel(testScope = this, mode = CreateWalletStartComponent.Mode.ColdWallet)
advanceUntilIdle()
model.uiState.value.onScanClick.invoke()
advanceUntilIdle()
coVerify {
userWalletsListRepository.unlock(
userWalletId = testUserWalletId,
unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(testScanResponse),
)
}
verify { appRouter.replaceAll(routes = arrayOf(AppRoute.Wallet), onComplete = any()) }
}
@Test
fun `GIVEN save returns DataError WHEN proceedWithScanResponse THEN no unlock AND no replace`() = runTest {
coEvery {
saveWalletUseCase(userWallet = any(), canOverride = any(), analyticsSource = any())
} returns SaveWalletError.DataError(messageId = 0).left()
coEvery {
scanCardProcessor.scan(
analyticsSource = any(),
shouldCheckIsAlreadyActivated = any(),
cardId = any(),
onProgressStateChange = any(),
onWalletNotCreated = any(),
disclaimerWillShow = any(),
onCancel = any(),
onFailure = any(),
onSuccess = any()
)
} coAnswers {
val onSuccess = arg<suspend (ScanResponse) -> Unit>(8)
onSuccess.invoke(testScanResponse)
}
val model = createModel(testScope = this, mode = CreateWalletStartComponent.Mode.ColdWallet)
advanceUntilIdle()
model.uiState.value.onScanClick.invoke()
advanceUntilIdle()
coVerify(exactly = 0) { userWalletsListRepository.unlock(any(), any()) }
verify(exactly = 0) { appRouter.replaceAll(routes = anyVararg(), onComplete = any()) }
}
@Test
fun `GIVEN save success AND isAddressSyncEnabled disabled WHEN proceedWithScanResponse THEN Wallet replaced`() =
runTest {
every { onboardingV2FeatureToggles.isAddressSyncEnabled } returns false
coEvery {
saveWalletUseCase(userWallet = any(), canOverride = any(), analyticsSource = any())
} returns Unit.right()
coEvery {
scanCardProcessor.scan(
analyticsSource = any(),
shouldCheckIsAlreadyActivated = any(),
cardId = any(),
onProgressStateChange = any(),
onWalletNotCreated = any(),
disclaimerWillShow = any(),
onCancel = any(),
onFailure = any(),
onSuccess = any()
)
} coAnswers {
val onSuccess = arg<suspend (ScanResponse) -> Unit>(8)
onSuccess.invoke(testScanResponse)
}
val model = createModel(testScope = this, mode = CreateWalletStartComponent.Mode.ColdWallet)
advanceUntilIdle()
model.uiState.value.onScanClick.invoke()
advanceUntilIdle()
verify { appRouter.replaceAll(routes = arrayOf(AppRoute.Wallet), onComplete = any()) }
}
@Test
fun `GIVEN save success AND isAddressSyncEnabled WHEN proceedWithScanResponse THEN AddressSync replaced`() =
runTest {
every { onboardingV2FeatureToggles.isAddressSyncEnabled } returns true
coEvery {
saveWalletUseCase(userWallet = any(), canOverride = any(), analyticsSource = any())
} returns Unit.right()
coEvery {
scanCardProcessor.scan(
analyticsSource = any(),
shouldCheckIsAlreadyActivated = any(),
cardId = any(),
onProgressStateChange = any(),
onWalletNotCreated = any(),
disclaimerWillShow = any(),
onCancel = any(),
onFailure = any(),
onSuccess = any()
)
} coAnswers {
val onSuccess = arg<suspend (ScanResponse) -> Unit>(8)
onSuccess.invoke(testScanResponse)
}
val model = createModel(testScope = this, mode = CreateWalletStartComponent.Mode.ColdWallet)
advanceUntilIdle()
model.uiState.value.onScanClick.invoke()
advanceUntilIdle()
verify { appRouter.replaceAll(routes = arrayOf(AppRoute.AddressSync), onComplete = any()) }
}
@Test
fun `WHEN model initialized THEN CreateWalletIntroScreenOpened event sent with referral id`() = runTest {
val refcode = "referralCode"
coEvery { appsFlyerStore.get() } returns AppsFlyerConversionData(refcode = refcode, campaign = null)
createModel(testScope = this, mode = CreateWalletStartComponent.Mode.ColdWallet)
advanceUntilIdle()
verify {
analyticsEventHandler.send(
match<IntroductionProcess.CreateWalletIntroScreenOpened> { true },
)
}
}
@Test
fun `WHEN ColdWallet WHEN get model THEN correct resources`() = runTest {
val model = createModel(testScope = this, mode = CreateWalletStartComponent.Mode.ColdWallet)
val state = model.uiState.value
assert(state.title == resourceReference(R.string.common_tangem_wallet))
assert(state.description == resourceReference(R.string.welcome_create_wallet_hardware_description))
assert(
state.featureItems == persistentListOf(
CreateWalletStartUM.FeatureItem(
iconResId = R.drawable.ic_shield_check_16,
text = resourceReference(R.string.welcome_create_wallet_feature_class),
),
CreateWalletStartUM.FeatureItem(
iconResId = R.drawable.ic_flash_16,
text = resourceReference(R.string.welcome_create_wallet_feature_delivery),
),
CreateWalletStartUM.FeatureItem(
iconResId = R.drawable.ic_sparkles_16,
text = resourceReference(R.string.welcome_create_wallet_feature_use),
)
)
)
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.isScanInProgress.not())
}
@Test
fun `WHEN HotWallet WHEN get model THEN correct resources`() = runTest {
val model = createModel(testScope = this, mode = CreateWalletStartComponent.Mode.HotWallet)
val state = model.uiState.value
assert(state.title == resourceReference(R.string.hw_mobile_wallet))
assert(state.description == resourceReference(R.string.welcome_create_wallet_mobile_description_full))
assert(
state.featureItems == persistentListOf(
CreateWalletStartUM.FeatureItem(
iconResId = R.drawable.ic_shield_check_16,
text = resourceReference(R.string.welcome_create_wallet_feature_seamless),
),
CreateWalletStartUM.FeatureItem(
iconResId = R.drawable.ic_flash_16,
text = resourceReference(R.string.welcome_create_wallet_feature_one_tap),
),
CreateWalletStartUM.FeatureItem(
iconResId = R.drawable.ic_stack_fill_new_16,
text = resourceReference(R.string.welcome_create_wallet_feature_assets),
),
)
)
assert(state.imageResId == R.drawable.img_mobile_wallet)
assert(state.shouldShowScanSecondaryButton.not())
assert(state.primaryButtonText == resourceReference(R.string.welcome_create_wallet_mobile_title))
assert(state.otherMethodTitle == resourceReference(R.string.welcome_create_wallet_use_hardware_title))
assert(state.otherMethodDescription == resourceReference(R.string.welcome_create_wallet_use_hardware_description))
assert(state.isScanInProgress.not())
}
private fun createModel(
testScope: TestScope,
mode: CreateWalletStartComponent.Mode,
paramsContainer: ParamsContainer = MutableParamsContainer(
value = CreateWalletStartComponent.Params(mode = mode)
),
): CreateWalletStartModel {
return CreateWalletStartModel(
paramsContainer = paramsContainer,
dispatchers = testScope.createTestingCoroutineDispatcherProvider(),
router = router,
scanCardProcessor = scanCardProcessor,
cardSdkConfigRepository = cardSdkConfigRepository,
settingsRepository = settingsRepository,
appRouter = appRouter,
coldUserWalletBuilderFactory = coldUserWalletBuilderFactory,
saveWalletUseCase = saveWalletUseCase,
isHotWalletCreationSupported = isHotWalletCreationSupported,
userWalletsListRepository = userWalletsListRepository,
uiMessageSender = uiMessageSender,
generateBuyTangemCardLinkUseCase = generateBuyTangemCardLinkUseCase,
urlOpener = urlOpener,
trackingContextProxy = trackingContextProxy,
analyticsEventHandler = analyticsEventHandler,
appsFlyerStore = appsFlyerStore,
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
)
}
private fun TestScope.createTestingCoroutineDispatcherProvider(): TestingCoroutineDispatcherProvider {
val testDispatcher = StandardTestDispatcher(testScheduler)
return TestingCoroutineDispatcherProvider(
main = testDispatcher,
mainImmediate = testDispatcher,
io = testDispatcher,
default = testDispatcher,
single = testDispatcher,
)
}
}

View file

@ -2,4 +2,5 @@ package com.tangem.features.onboarding.v2
interface OnboardingV2FeatureToggles {
val isVisaOnboardingEnabled: Boolean
val isAddressSyncEnabled: Boolean
}

View file

@ -9,4 +9,6 @@ internal class DefaultOnboardingV2FeatureToggles @Inject constructor(
) : OnboardingV2FeatureToggles {
override val isVisaOnboardingEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.VISA_ONBOARDING_ENABLED)
override val isAddressSyncEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.ADDRESS_SYNC_ENABLED)
}

View file

@ -0,0 +1,13 @@
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>
}

View file

@ -0,0 +1,39 @@
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.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.onboarding.v2.addresssync.model.AddressSyncModel
import com.tangem.features.onboarding.v2.addresssync.ui.AddressSyncContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultAddressSyncComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: AddressSyncComponent.Params,
) : AppComponentContext by appComponentContext, AddressSyncComponent {
private val model: AddressSyncModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
AddressSyncContent(modifier = modifier, state = state)
BackHandler(onBack = state.onBackClick)
}
@AssistedFactory
interface Factory : AddressSyncComponent.Factory {
override fun create(
context: AppComponentContext,
params: AddressSyncComponent.Params,
): DefaultAddressSyncComponent
}
}

View file

@ -0,0 +1,18 @@
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

@ -0,0 +1,20 @@
package com.tangem.features.onboarding.v2.addresssync.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.onboarding.v2.addresssync.model.AddressSyncModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(ModelComponent::class)
internal interface AddressSyncModelModule {
@Binds
@IntoMap
@ClassKey(AddressSyncModel::class)
fun bindAddressSyncModel(model: AddressSyncModel): Model
}

View file

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

View file

@ -0,0 +1,22 @@
package com.tangem.features.onboarding.v2.addresssync.model
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.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@ModelScoped
internal class AddressSyncModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
val uiState: StateFlow<AddressSyncUM>
field: MutableStateFlow<AddressSyncUM> = MutableStateFlow(getInitialState())
private fun getInitialState(): AddressSyncUM {
TODO("Will be implemented during [REDACTED_TASK_KEY]")
}
}

View file

@ -0,0 +1,54 @@
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.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) {
Column(
modifier = modifier
.background(color = TangemTheme.colors.background.secondary)
.fillMaxSize()
.imePadding()
.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),
) {}
}
}
@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) {
TangemThemePreview {
AddressSyncContent(state = state)
}
}
private class PreviewStateProvider : CollectionPreviewParameterProvider<AddressSyncUM>(
buildList {
TODO("Will be implemented during [REDACTED_TASK_KEY]")
},
)