Updated on 2026-08-14
This commit is contained in:
parent
0bedc513a7
commit
c846d8c276
15 changed files with 862 additions and 3 deletions
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,4 +2,5 @@ package com.tangem.features.onboarding.v2
|
|||
|
||||
interface OnboardingV2FeatureToggles {
|
||||
val isVisaOnboardingEnabled: Boolean
|
||||
val isAddressSyncEnabled: Boolean
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.features.onboarding.v2.addresssync.entity
|
||||
|
||||
internal data class AddressSyncUM(
|
||||
val onBackClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -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]")
|
||||
}
|
||||
}
|
||||
|
|
@ -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]")
|
||||
},
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue