Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-11 12:04:22 +04:00
parent 935a967deb
commit bf50e374e1
18 changed files with 617 additions and 64 deletions

View file

@ -395,7 +395,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
componentScope.launch(dispatchers.main) {
backupServiceHolder.backupService.get()?.discardSavedBackup()
val unfinishedBackup = onboardingRepository.getUnfinishedFinalizeOnboarding() ?: return@launch
cardRepository.finishCardActivation(unfinishedBackup.card.cardId)
cardRepository.finishCardActivation(cardId = unfinishedBackup.card.cardId, hasBackupError = true)
onboardingRepository.clearUnfinishedFinalizeOnboarding()
analyticsEventHandler.send(OnboardingAnalyticsEvent.Onboarding.Finished())
}

View file

@ -13,4 +13,6 @@ data class UsedCardInfo(
val isActivationStarted: Boolean = false,
@Json(name = "isActivationFinished")
val isActivationFinished: Boolean = false,
@Json(name = "hasBackupError")
val hasBackupError: Boolean = false,
)

View file

@ -0,0 +1,107 @@
package com.tangem.datasource.local.card
import com.google.common.truth.Truth.assertThat
import com.squareup.moshi.JsonDataException
import com.squareup.moshi.Moshi
import com.tangem.test.core.ProvideTestModels
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
/**
* Tests Moshi serialization/deserialization of [UsedCardInfo].
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class UsedCardInfoSerializationTest {
private val adapter = Moshi.Builder().build().adapter(UsedCardInfo::class.java)
@Test
fun `GIVEN full model WHEN toJson THEN all fields serialized in declaration order`() {
// Arrange
val model = UsedCardInfo(
cardId = "card-1",
isScanned = true,
isActivationStarted = true,
isActivationFinished = false,
hasBackupError = true,
)
// Act
val json = adapter.toJson(model)
// Assert
assertThat(json).isEqualTo(
"""{"cardId":"card-1","isScanned":true,"isActivationStarted":true,""" +
""""isActivationFinished":false,"hasBackupError":true}""",
)
}
@Test
fun `GIVEN full json WHEN fromJson THEN model fully populated`() {
// Arrange
val json = """{"cardId":"card-2","isScanned":false,"isActivationStarted":true,""" +
""""isActivationFinished":true,"hasBackupError":false}"""
// Act
val result = adapter.fromJson(json)
// Assert
assertThat(result).isEqualTo(
UsedCardInfo(
cardId = "card-2",
isScanned = false,
isActivationStarted = true,
isActivationFinished = true,
hasBackupError = false,
),
)
}
@Test
fun `GIVEN json with only cardId WHEN fromJson THEN boolean fields fall back to defaults`() {
// Arrange
val json = """{"cardId":"card-3"}"""
// Act
val result = adapter.fromJson(json)
// Assert
assertThat(result).isEqualTo(UsedCardInfo(cardId = "card-3"))
}
@Test
fun `GIVEN json without cardId WHEN fromJson THEN throws`() {
// Arrange
val json = """{"isScanned":true}"""
// Act
val error = runCatching { adapter.fromJson(json) }.exceptionOrNull()
// Assert
assertThat(error).isInstanceOf(JsonDataException::class.java)
}
@ParameterizedTest
@ProvideTestModels
fun roundTrip(model: UsedCardInfo) {
// Act
val restored = adapter.fromJson(adapter.toJson(model))
// Assert
assertThat(restored).isEqualTo(model)
}
private fun provideTestModels() = listOf(
UsedCardInfo(cardId = "default-only"),
UsedCardInfo(
cardId = "all-true",
isScanned = true,
isActivationStarted = true,
isActivationFinished = true,
hasBackupError = true,
),
UsedCardInfo(cardId = "activation-in-progress", isScanned = true, isActivationStarted = true),
UsedCardInfo(cardId = "backup-error", hasBackupError = true),
)
}

View file

@ -30,4 +30,8 @@ dependencies {
implementation(projects.domain.card)
implementation(projects.domain.models)
// region Tests
testImplementation(projects.test.core)
// end
}

View file

@ -32,33 +32,9 @@ internal class DefaultCardRepository(
appPreferencesStore.editUsedCards(cardId) { it.copy(isActivationStarted = true) }
}
override suspend fun finishCardActivation(cardId: String) {
override suspend fun finishCardActivation(cardId: String, hasBackupError: Boolean) {
appPreferencesStore.editUsedCards(cardId) {
it.copy(isActivationStarted = true, isActivationFinished = true)
}
}
override suspend fun finishCardsActivation(cardIds: List<String>) {
appPreferencesStore.editData { mutablePreferences ->
val usedCards = mutablePreferences.getUsedCards()
val newCards = cardIds.mapNotNull { newCardId ->
if (usedCards.none { it.cardId == newCardId }) {
createDefaultUsedCardInfo(cardId = newCardId)
} else {
null
}
}
val updatedUsedCards = (usedCards + newCards).map { card ->
if (cardIds.contains(card.cardId)) {
card.copy(isActivationStarted = true, isActivationFinished = true)
} else {
card
}
}
mutablePreferences.setObjectList(key = PreferencesKeys.USED_CARDS_INFO_KEY, value = updatedUsedCards)
it.copy(isActivationStarted = true, isActivationFinished = true, hasBackupError = hasBackupError)
}
}
@ -76,6 +52,10 @@ internal class DefaultCardRepository(
return card.isActivationStarted && !card.isActivationFinished
}
override suspend fun hasBackupError(cardId: String): Boolean {
return getUsedCardSync(cardId)?.hasBackupError == true
}
override suspend fun isTangemTOSAccepted(): Boolean {
return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.IS_TANGEM_TOS_ACCEPTED_KEY, default = false)
}

View file

@ -0,0 +1,287 @@
package com.tangem.data.card
import androidx.datastore.preferences.core.emptyPreferences
import com.google.common.truth.Truth.assertThat
import com.squareup.moshi.Moshi
import com.tangem.datasource.local.card.UsedCardInfo
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectListSync
import com.tangem.datasource.local.preferences.utils.storeObjectList
import com.tangem.test.core.ProvideTestModels
import com.tangem.test.core.datastore.MockStateDataStore
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
/**
* Tests for [DefaultCardRepository].
*
* Uses a real [AppPreferencesStore] backed by an in-memory [MockStateDataStore] and a real [Moshi]
* instance, so the JSON round-trip through preferences is exercised end-to-end.
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class DefaultCardRepositoryTest {
// Only the in-memory store's content is mutable, so it is the single thing reset per test.
private val dataStore = MockStateDataStore(default = emptyPreferences())
private val appPreferencesStore = AppPreferencesStore(
moshi = Moshi.Builder().build(),
dispatchers = TestingCoroutineDispatcherProvider(),
preferencesDataStore = dataStore,
)
private val repository = DefaultCardRepository(appPreferencesStore)
@BeforeEach
fun resetStore() {
runBlocking { dataStore.updateData { emptyPreferences() } }
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class WasCardScanned {
@Test
fun `GIVEN card present WHEN wasCardScanned THEN emits true`() = runTest {
// Arrange
seedCards(UsedCardInfo(cardId = CARD_ID))
// Act
val result = repository.wasCardScanned(CARD_ID).first()
// Assert
assertThat(result).isTrue()
}
@Test
fun `GIVEN card absent WHEN wasCardScanned THEN emits false`() = runTest {
// Act
val result = repository.wasCardScanned(CARD_ID).first()
// Assert
assertThat(result).isFalse()
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class SetCardWasScanned {
@Test
fun `GIVEN empty store WHEN setCardWasScanned THEN creates entry with isScanned true`() = runTest {
// Act
repository.setCardWasScanned(CARD_ID)
// Assert
assertThat(storedCards()).containsExactly(UsedCardInfo(cardId = CARD_ID, isScanned = true))
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class StartCardActivation {
@Test
fun `GIVEN empty store WHEN startCardActivation THEN creates entry with isActivationStarted true`() = runTest {
// Act
repository.startCardActivation(CARD_ID)
// Assert
assertThat(storedCards()).containsExactly(UsedCardInfo(cardId = CARD_ID, isActivationStarted = true))
}
@Test
fun `GIVEN other cards present WHEN editing one card THEN others are preserved`() = runTest {
// Arrange
val other = UsedCardInfo(cardId = OTHER_CARD_ID, isScanned = true)
seedCards(other)
// Act
repository.startCardActivation(CARD_ID)
// Assert
assertThat(storedCards()).containsExactly(
other,
UsedCardInfo(cardId = CARD_ID, isActivationStarted = true),
)
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class FinishCardActivation {
@Test
fun `GIVEN empty store WHEN finishCardActivation with backup error THEN entry marked finished with error`() =
runTest {
// Act
repository.finishCardActivation(cardId = CARD_ID, hasBackupError = true)
// Assert
assertThat(storedCards()).containsExactly(
UsedCardInfo(
cardId = CARD_ID,
isActivationStarted = true,
isActivationFinished = true,
hasBackupError = true,
),
)
}
@Test
fun `GIVEN empty store WHEN finishCardActivation without backup error THEN entry marked finished without error`() =
runTest {
// Act
repository.finishCardActivation(cardId = CARD_ID, hasBackupError = false)
// Assert
assertThat(storedCards()).containsExactly(
UsedCardInfo(
cardId = CARD_ID,
isActivationStarted = true,
isActivationFinished = true,
hasBackupError = false,
),
)
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class IsActivationStarted {
@Test
fun `GIVEN activation started WHEN isActivationStarted THEN true`() = runTest {
// Arrange
seedCards(UsedCardInfo(cardId = CARD_ID, isActivationStarted = true))
// Act & Assert
assertThat(repository.isActivationStarted(CARD_ID)).isTrue()
}
@Test
fun `GIVEN card absent WHEN isActivationStarted THEN false`() = runTest {
assertThat(repository.isActivationStarted(CARD_ID)).isFalse()
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class IsActivationFinished {
@Test
fun `GIVEN activation finished WHEN isActivationFinished THEN true`() = runTest {
// Arrange
seedCards(UsedCardInfo(cardId = CARD_ID, isActivationFinished = true))
// Act & Assert
assertThat(repository.isActivationFinished(CARD_ID)).isTrue()
}
@Test
fun `GIVEN card absent WHEN isActivationFinished THEN false`() = runTest {
assertThat(repository.isActivationFinished(CARD_ID)).isFalse()
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class IsActivationInProgress {
@ParameterizedTest
@ProvideTestModels
fun isActivationInProgress(model: ActivationInProgressModel) = runTest {
// Arrange
model.stored?.let { seedCards(it) }
// Act
val result = repository.isActivationInProgress(CARD_ID)
// Assert
assertThat(result).isEqualTo(model.expected)
}
private fun provideTestModels() = listOf(
ActivationInProgressModel(stored = null, expected = false),
ActivationInProgressModel(
stored = UsedCardInfo(cardId = CARD_ID, isActivationStarted = false, isActivationFinished = false),
expected = false,
),
ActivationInProgressModel(
stored = UsedCardInfo(cardId = CARD_ID, isActivationStarted = true, isActivationFinished = false),
expected = true,
),
ActivationInProgressModel(
stored = UsedCardInfo(cardId = CARD_ID, isActivationStarted = true, isActivationFinished = true),
expected = false,
),
ActivationInProgressModel(
stored = UsedCardInfo(cardId = CARD_ID, isActivationStarted = false, isActivationFinished = true),
expected = false,
),
)
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class HasBackupError {
@Test
fun `GIVEN backup error WHEN hasBackupError THEN true`() = runTest {
// Arrange
seedCards(UsedCardInfo(cardId = CARD_ID, hasBackupError = true))
// Act & Assert
assertThat(repository.hasBackupError(CARD_ID)).isTrue()
}
@Test
fun `GIVEN no backup error WHEN hasBackupError THEN false`() = runTest {
// Arrange
seedCards(UsedCardInfo(cardId = CARD_ID, hasBackupError = false))
// Act & Assert
assertThat(repository.hasBackupError(CARD_ID)).isFalse()
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class TangemTos {
@Test
fun `GIVEN nothing stored WHEN isTangemTOSAccepted THEN false by default`() = runTest {
assertThat(repository.isTangemTOSAccepted()).isFalse()
}
@Test
fun `GIVEN TOS accepted WHEN isTangemTOSAccepted THEN true`() = runTest {
// Arrange
repository.acceptTangemTOS()
// Act & Assert
assertThat(repository.isTangemTOSAccepted()).isTrue()
}
}
private suspend fun seedCards(vararg cards: UsedCardInfo) {
appPreferencesStore.storeObjectList(key = PreferencesKeys.USED_CARDS_INFO_KEY, value = cards.toList())
}
private suspend fun storedCards(): List<UsedCardInfo> {
return appPreferencesStore.getObjectListSync(key = PreferencesKeys.USED_CARDS_INFO_KEY)
}
internal data class ActivationInProgressModel(val stored: UsedCardInfo?, val expected: Boolean)
private companion object {
const val CARD_ID = "card-1"
const val OTHER_CARD_ID = "card-2"
}
}

View file

@ -10,9 +10,7 @@ interface CardRepository {
suspend fun startCardActivation(cardId: String)
suspend fun finishCardActivation(cardId: String)
suspend fun finishCardsActivation(cardIds: List<String>)
suspend fun finishCardActivation(cardId: String, hasBackupError: Boolean = false)
@Throws
suspend fun isActivationStarted(cardId: String): Boolean
@ -23,6 +21,9 @@ interface CardRepository {
@Throws
suspend fun isActivationInProgress(cardId: String): Boolean
@Throws
suspend fun hasBackupError(cardId: String): Boolean
@Throws
suspend fun isTangemTOSAccepted(): Boolean

View file

@ -59,9 +59,7 @@ dependencies {
// end
// region Tests
testImplementation(deps.test.junit5)
testImplementation(deps.test.coroutine)
testImplementation(deps.test.truth)
testImplementation(deps.test.mockk)
testImplementation(projects.test.core)
testImplementation(projects.common.test)
// end
}

View file

@ -6,11 +6,9 @@
<ID>BooleanPropertyNaming:HotWalletPasswordRequester.kt$HotWalletPasswordRequester.AttemptRequest$val authMode: Boolean</ID>
<ID>BooleanPropertyNaming:SaveWalletUseCase.kt$SaveWalletUseCase$val newUserWallet = userWalletsListRepository.userWalletsSync().none { it.walletId == userWallet.walletId }</ID>
<ID>MaxChainedCallsOnSameLine:UserWalletExtensions.kt$wallets.orEmpty().first { it.curve == primaryCurve }.derivedKeys.keys.any { it == dp }</ID>
<ID>MultilineLambdaItParameter:ColdUserWalletBuilder.kt$ColdUserWalletBuilder${ UserWallet.Cold( walletId = it, name = generateWalletNameUseCase( card = card, productType = productType, isStartToCoin = cardTypesResolver.isStart2Coin(), ), cardsInWallet = backupCardsIds.plus(card.cardId), scanResponse = this, isMultiCurrency = cardTypesResolver.isMultiwalletAllowed(), hasBackupError = hasBackupError, ) }</ID>
<ID>MultilineLambdaItParameter:HotUserWalletBuilder.kt$HotUserWalletBuilder${ MobileWallet( publicKey = it.seedKey.publicKey, chainCode = it.seedKey.chainCode, curve = it.curve, derivedKeys = it.publicKeys, ) }</ID>
<ID>MultilineLambdaItParameter:HotUserWalletBuilder.kt$HotUserWalletBuilder${ val derivationPath = it.derivationPath(DerivationStyle.V3) ?: return@mapNotNull null if (it == Blockchain.Cardano) { val extendedDerivationPath = CardanoUtils.extendedDerivationPath(derivationPath) listOf(derivationPath, extendedDerivationPath) } else { listOf(derivationPath) } }</ID>
<ID>MultilineLambdaItParameter:UpdateWalletUseCase.kt$UpdateWalletUseCase${ when (it) { is SaveWalletError.DataError -&gt; DataError( IllegalStateException("Failed to update wallet: ${it.messageId}"), ) is SaveWalletError.WalletAlreadySaved -&gt; UpdateWalletError.NameAlreadyExists } }</ID>
<ID>NestedScopeFunctions:ColdUserWalletBuilder.kt$ColdUserWalletBuilder$let { UserWallet.Cold( walletId = it, name = generateWalletNameUseCase( card = card, productType = productType, isStartToCoin = cardTypesResolver.isStart2Coin(), ), cardsInWallet = backupCardsIds.plus(card.cardId), scanResponse = this, isMultiCurrency = cardTypesResolver.isMultiwalletAllowed(), hasBackupError = hasBackupError, ) }</ID>
<ID>NoNameShadowing:SaveWalletUseCase.kt$SaveWalletUseCase$userWallet</ID>
<ID>RedundantSuspendModifier:GetHotWalletContextualUnlockUseCase.kt$GetHotWalletContextualUnlockUseCase$suspend</ID>
<ID>SuspendFunSwallowedCancellation:RenameWalletUseCase.kt$RenameWalletUseCase$runCatching</ID>

View file

@ -1,6 +1,7 @@
package com.tangem.domain.wallets.builder
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
@ -11,6 +12,7 @@ import dagger.assisted.AssistedInject
class ColdUserWalletBuilder @AssistedInject constructor(
@Assisted private val scanResponse: ScanResponse,
private val generateWalletNameUseCase: GenerateWalletNameUseCase,
private val cardRepository: CardRepository,
) {
private var backupCardsIds: Set<String> = emptySet()
private var hasBackupError: Boolean = false
@ -32,25 +34,22 @@ class ColdUserWalletBuilder @AssistedInject constructor(
this.hasBackupError = hasBackupError
}
fun build(): UserWallet.Cold? {
return with(scanResponse) {
UserWalletIdBuilder.scanResponse(scanResponse)
.build()
?.let {
UserWallet.Cold(
walletId = it,
name = generateWalletNameUseCase(
card = card,
productType = productType,
isStartToCoin = cardTypesResolver.isStart2Coin(),
),
cardsInWallet = backupCardsIds.plus(card.cardId),
scanResponse = this,
isMultiCurrency = cardTypesResolver.isMultiwalletAllowed(),
hasBackupError = hasBackupError,
)
}
}
suspend fun build(): UserWallet.Cold? {
val walletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
?: return null
return UserWallet.Cold(
walletId = walletId,
name = generateWalletNameUseCase(
card = scanResponse.card,
productType = scanResponse.productType,
isStartToCoin = scanResponse.cardTypesResolver.isStart2Coin(),
),
cardsInWallet = backupCardsIds.plus(scanResponse.card.cardId),
scanResponse = scanResponse,
isMultiCurrency = scanResponse.cardTypesResolver.isMultiwalletAllowed(),
hasBackupError = hasBackupError || cardRepository.hasBackupError(scanResponse.card.cardId),
)
}
@AssistedFactory

View file

@ -0,0 +1,174 @@
package com.tangem.domain.wallets.builder
import com.google.common.truth.Truth.assertThat
import com.tangem.common.test.domain.card.MockScanResponseFactory
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.card.configs.MultiWalletCardConfig
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
import com.tangem.test.core.ProvideTestModels
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
import kotlinx.coroutines.test.runTest
/**
* Tests for [ColdUserWalletBuilder].
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class ColdUserWalletBuilderTest {
private val generateWalletNameUseCase: GenerateWalletNameUseCase = mockk()
private val cardRepository: CardRepository = mockk()
private val scanResponse = MockScanResponseFactory.create(
cardConfig = MultiWalletCardConfig,
derivedKeys = emptyMap(),
)
private val primaryCardId = scanResponse.card.cardId
private val expectedWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
@BeforeEach
fun setup() {
clearMocks(generateWalletNameUseCase, cardRepository)
every { generateWalletNameUseCase(any(), any(), any()) } returns WALLET_NAME
coEvery { cardRepository.hasBackupError(any()) } returns false
}
@Test
fun `GIVEN valid scan response WHEN build THEN returns cold wallet with expected fields`() = runTest {
// Act
val result = createBuilder().build()
// Assert
assertThat(result).isEqualTo(
UserWallet.Cold(
name = WALLET_NAME,
walletId = requireNotNull(expectedWalletId),
cardsInWallet = setOf(primaryCardId),
isMultiCurrency = scanResponse.cardTypesResolver.isMultiwalletAllowed(),
hasBackupError = false,
scanResponse = scanResponse,
),
)
}
@Test
fun `GIVEN backup card ids WHEN build THEN cards in wallet include primary and backup`() = runTest {
// Act
val result = createBuilder()
.backupCardsIds(setOf("backup-1", "backup-2"))
.build()
// Assert
assertThat(result?.cardsInWallet).containsExactly(primaryCardId, "backup-1", "backup-2")
}
@Test
fun `GIVEN null backup card ids WHEN build THEN cards in wallet contain only primary`() = runTest {
// Act
val result = createBuilder()
.backupCardsIds(null)
.build()
// Assert
assertThat(result?.cardsInWallet).containsExactly(primaryCardId)
}
@Test
fun `GIVEN scan response without wallets WHEN build THEN returns null`() = runTest {
// Arrange
val scanResponseWithoutWallets = scanResponse.copy(
card = scanResponse.card.copy(wallets = emptyList()),
)
// Act
val result = createBuilder(scanResponseWithoutWallets).build()
// Assert
assertThat(result).isNull()
}
@Test
fun `GIVEN scan response WHEN build THEN wallet name generated from resolver data`() = runTest {
// Act
createBuilder().build()
// Assert
verify(exactly = 1) {
generateWalletNameUseCase(
scanResponse.productType,
scanResponse.card,
scanResponse.cardTypesResolver.isStart2Coin(),
)
}
}
@ParameterizedTest
@ProvideTestModels
fun hasBackupError(model: HasBackupErrorModel) = runTest {
// Arrange
coEvery { cardRepository.hasBackupError(primaryCardId) } returns model.repositoryReturns
// Act
val result = createBuilder()
.hasBackupError(model.builderFlag)
.build()
// Assert
assertThat(result?.hasBackupError).isEqualTo(model.expected)
// The externally set flag short-circuits `||`: the repository is queried only when the flag is not set.
coVerify(exactly = model.expectedRepositoryCalls) { cardRepository.hasBackupError(primaryCardId) }
}
private fun createBuilder(scanResponse: ScanResponse = this.scanResponse) = ColdUserWalletBuilder(
scanResponse = scanResponse,
generateWalletNameUseCase = generateWalletNameUseCase,
cardRepository = cardRepository,
)
internal data class HasBackupErrorModel(
val builderFlag: Boolean,
val repositoryReturns: Boolean,
val expected: Boolean,
val expectedRepositoryCalls: Int,
)
private fun provideTestModels() = listOf(
// Flag not set externally -> repository is queried and is the source of truth.
HasBackupErrorModel(
builderFlag = false,
repositoryReturns = false,
expected = false,
expectedRepositoryCalls = 1,
),
HasBackupErrorModel(
builderFlag = false,
repositoryReturns = true,
expected = true,
expectedRepositoryCalls = 1,
),
// Flag set externally -> `||` short-circuits, repository is NOT queried regardless of its value.
HasBackupErrorModel(
builderFlag = true,
repositoryReturns = false,
expected = true,
expectedRepositoryCalls = 0,
),
HasBackupErrorModel(builderFlag = true, repositoryReturns = true, expected = true, expectedRepositoryCalls = 0),
)
private companion object {
const val WALLET_NAME = "Wallet"
}
}

View file

@ -79,7 +79,7 @@ internal class CreateWalletStartModelTest {
coEvery { appsFlyerStore.get() } returns null
coEvery { settingsRepository.shouldSaveAccessCodes() } returns false
every { coldUserWalletBuilderFactory.create(any()) } returns coldUserWalletBuilder
every { coldUserWalletBuilder.build() } returns testColdWallet
coEvery { coldUserWalletBuilder.build() } returns testColdWallet
every { onboardingV2FeatureToggles.isAddressSyncEnabled } returns false
coEvery {
scanCardProcessor.scan(
@ -356,7 +356,7 @@ internal class CreateWalletStartModelTest {
@Test
fun `GIVEN builder returns null WHEN proceedWithScanResponse THEN saveWalletUseCase not called`() = runTest {
every { coldUserWalletBuilder.build() } returns null
coEvery { coldUserWalletBuilder.build() } returns null
coEvery {
scanCardProcessor.scan(
analyticsSource = any(),

View file

@ -119,7 +119,7 @@ internal class UserWalletSaver @Inject constructor(
)
}
private fun Raise<Error>.createUserWallet(response: ScanResponse): UserWallet {
private suspend fun Raise<Error>.createUserWallet(response: ScanResponse): UserWallet {
val userWallet = coldUserWalletBuilderFactory.create(scanResponse = response).build()
return ensureNotNull(userWallet) { Error.Unknown }

View file

@ -292,7 +292,7 @@ internal class UserWalletSaverTest {
private fun mockBuilderReturns(userWallet: UserWallet.Cold?) {
val builder: ColdUserWalletBuilder = mockk {
every { build() } returns userWallet
coEvery { build() } returns userWallet
}
every { coldUserWalletBuilderFactory.create(scanResponse = any()) } returns builder
}

View file

@ -68,7 +68,7 @@ internal class Wallet1ChooseOptionModel @Inject constructor(
}
}
private fun createUserWallet(scanResponse: ScanResponse): UserWallet.Cold {
private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet.Cold {
return requireNotNull(
value = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build(),
lazyMessage = { "User wallet not created" },

View file

@ -334,13 +334,16 @@ internal class MultiWalletFinalizeModel @Inject constructor(
// to prevent showing finalize screen dialog on next app start
onboardingRepository.clearUnfinishedFinalizeOnboarding()
cardRepository.finishCardActivation(scanResponse.card.cardId)
cardRepository.finishCardActivation(
cardId = scanResponse.card.cardId,
hasBackupError = hasWalletBackupError,
)
backupServiceHolder.backupService.get()?.discardSavedBackup()
onEvent.emit(MultiWalletFinalizeComponent.Event.ThreeBackupCardsAdded)
}
}
private fun createUserWallet(scanResponse: ScanResponse): UserWallet.Cold {
private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet.Cold {
return requireNotNull(
value = coldUserWalletBuilderFactory.create(scanResponse = scanResponse)
.backupCardsIds(backupCardIds.toSet())

View file

@ -299,7 +299,7 @@ internal class MultiWalletSeedPhraseModel @Inject constructor(
}
}
private fun createUserWallet(scanResponse: ScanResponse): UserWallet.Cold {
private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet.Cold {
return requireNotNull(
value = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build(),
lazyMessage = { "User wallet not created" },

View file

@ -155,7 +155,7 @@ internal class MultiWalletUpgradeWalletModel @Inject constructor(
}
}
private fun createUserWallet(scanResponse: ScanResponse): UserWallet.Cold {
private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet.Cold {
return requireNotNull(
value = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build(),
lazyMessage = { "User wallet not created" },