Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-24 14:00:32 +04:00
parent 4fa5989289
commit 2d0bc9d9e6
12 changed files with 141 additions and 74 deletions

View file

@ -1,12 +1,18 @@
package com.tangem.tap.di.data
import android.content.Context
import com.tangem.data.card.sdk.CardSdkOwner
import com.tangem.data.card.sdk.CardSdkProvider
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.operations.attestation.CardArtworksProvider
import com.tangem.tap.data.DefaultCardSdkProvider
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import java.io.File
import javax.inject.Singleton
@Module
@ -20,4 +26,22 @@ internal interface CardSdkModule {
@Binds
@Singleton
fun providerCardSdkLifecycleObserver(defaultCardSdkProvider: DefaultCardSdkProvider): CardSdkOwner
companion object {
@Provides
@Singleton
fun provideCardArtworksProvider(
sdkRepository: CardSdkConfigRepository,
@ApplicationContext context: Context,
): CardArtworksProvider {
return CardArtworksProvider(
tangemApiBaseUrlProvider = { sdkRepository.sdk.config.tangemApiBaseUrl },
artworksDirectory = File(
context.getExternalFilesDir(null) ?: context.filesDir,
"card_artworks",
).apply { mkdirs() },
)
}
}
}

View file

@ -11,7 +11,6 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.derivations.DerivationsRepository
import com.tangem.domain.wallets.usecase.*
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.card.DefaultDeleteSavedAccessCodesUseCase
import com.tangem.tap.domain.card.DefaultResetCardUseCase
import dagger.Module
import dagger.Provides
@ -56,7 +55,7 @@ internal object CardDomainModule {
@Provides
@Singleton
fun provideDeleteSavedAccessCodesUseCase(tangemSdkManager: TangemSdkManager): DeleteSavedAccessCodesUseCase {
return DefaultDeleteSavedAccessCodesUseCase(tangemSdkManager)
return DeleteSavedAccessCodesUseCase(tangemSdkManager)
}
@Provides

View file

@ -1,22 +0,0 @@
package com.tangem.tap.domain.card
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.domain.card.DeleteSavedAccessCodesUseCase
import com.tangem.sdk.api.TangemSdkManager
internal class DefaultDeleteSavedAccessCodesUseCase(
private val tangemSdkManager: TangemSdkManager,
) : DeleteSavedAccessCodesUseCase {
override suspend fun invoke(cardId: String): Either<Throwable, Unit> {
tangemSdkManager.deleteSavedUserCodes(setOf(cardId))
.doOnFailure { return it.left() }
.doOnSuccess { return Unit.right() }
return Unit.right()
}
}

View file

@ -36,9 +36,9 @@ import com.tangem.tap.domain.userWalletList.utils.*
import com.tangem.utils.Provider
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.coroutines.runSuspendCatching
import dagger.Lazy
import com.tangem.utils.extensions.addOrReplace
import com.tangem.utils.extensions.indexOfFirstOrNull
import dagger.Lazy
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
@ -96,8 +96,8 @@ internal class DefaultUserWalletsListRepository(
?: loadedWallets.firstOrNull()?.also {
selectedUserWalletRepository.set(it.walletId)
}
userWallets.value = loadedWallets
setSelectedUserWallet(initialSelection)
userWallets.value = loadedWallets
}
}
}
@ -235,7 +235,6 @@ internal class DefaultUserWalletsListRepository(
)
}
userWallets.value = updatedWallets
if (currentSelected != null) {
if (newSelected == null) {
onAllWalletsDeleted()
@ -243,6 +242,7 @@ internal class DefaultUserWalletsListRepository(
selectedUserWalletRepository.set(newSelected?.walletId)
setSelectedUserWallet(newSelected)
}
userWallets.value = updatedWallets
}
@Suppress("CyclomaticComplexMethod", "LongMethod")

View file

@ -197,6 +197,9 @@ internal class ResetCardModel @Inject constructor(
checkRemainingBackupCards()
}
.onLeft {
TangemLogger.e("Failed to reset card: $it")
}
}
}

View file

@ -8,6 +8,10 @@ android {
namespace = "com.tangem.domain.card"
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
dependencies {
implementation(projects.core.analytics.models)
implementation(projects.core.error)
@ -25,6 +29,7 @@ dependencies {
implementation(projects.domain.visa.models)
implementation(projects.core.utils)
implementation(projects.libs.tangemSdkApi)
implementation(tangemDeps.card.core)
implementation(tangemDeps.blockchain) {
@ -32,9 +37,8 @@ dependencies {
}
/** Testing libraries */
testImplementation(deps.test.junit5)
testRuntimeOnly(deps.test.junit5.engine)
testImplementation(deps.test.mockk)
testImplementation(deps.test.truth)
testRuntimeOnly(deps.test.junit5.vintage.engine)
testImplementation(projects.common.test)
testImplementation(projects.test.core)
}

View file

@ -1,8 +1,33 @@
package com.tangem.domain.card
import arrow.core.Either
import arrow.core.raise.either
import com.tangem.common.doOnFailure
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.utils.logging.TangemLogger
interface DeleteSavedAccessCodesUseCase {
/**
* Removes saved user codes (access code and/or passcode) for a physical Tangem card
* from the device's secure storage.
*
* Typically invoked after a successful factory reset of the card, so that stale codes
* for an already-wiped card are not left on the device.
*
* @property tangemSdkManager Card SDK wrapper that performs the code removal operation
*/
class DeleteSavedAccessCodesUseCase(
private val tangemSdkManager: TangemSdkManager,
) {
suspend operator fun invoke(cardId: String): Either<Throwable, Unit>
/**
* @param cardId identifier of the card whose saved codes must be removed
* @return [Unit] on success; a Card SDK error (as [Throwable]) if removal failed
*/
suspend operator fun invoke(cardId: String): Either<Throwable, Unit> = either {
tangemSdkManager.deleteSavedUserCodes(cardsIds = setOf(cardId))
.doOnFailure { error ->
TangemLogger.e("Failed to delete saved access codes for card with id: $cardId", error)
raise(error)
}
}
}

View file

@ -0,0 +1,69 @@
package com.tangem.domain.card
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.test.core.assertEitherLeft
import com.tangem.test.core.assertEitherRight
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class DeleteSavedAccessCodesUseCaseTest {
private val tangemSdkManager = mockk<TangemSdkManager>()
private lateinit var useCase: DeleteSavedAccessCodesUseCase
@BeforeEach
fun setup() {
clearMocks(tangemSdkManager)
useCase = DeleteSavedAccessCodesUseCase(tangemSdkManager = tangemSdkManager)
}
@Test
fun `returns Right Unit when sdk deletes codes successfully`() = runTest {
// Arrange
val cardId = "AA00000000000001"
coEvery { tangemSdkManager.deleteSavedUserCodes(setOf(cardId)) } returns CompletionResult.Success(Unit)
// Act
val actual = useCase(cardId = cardId)
// Assert
assertEitherRight(actual)
}
@Test
fun `returns Left with sdk error when sdk fails`() = runTest {
// Arrange
val cardId = "AA00000000000002"
val sdkError = TangemSdkError.ExceptionError(RuntimeException("boom"))
coEvery { tangemSdkManager.deleteSavedUserCodes(setOf(cardId)) } returns CompletionResult.Failure(sdkError)
// Act
val actual = useCase(cardId = cardId)
// Assert
assertEitherLeft(actual, sdkError)
}
@Test
fun `passes exactly the given cardId as a singleton set to sdk`() = runTest {
// Arrange
val cardId = "AA00000000000003"
coEvery { tangemSdkManager.deleteSavedUserCodes(any()) } returns CompletionResult.Success(Unit)
// Act
useCase(cardId = cardId)
// Assert
coVerify(exactly = 1) { tangemSdkManager.deleteSavedUserCodes(setOf(cardId)) }
}
}

View file

@ -4,6 +4,7 @@ import arrow.core.Either
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.error.DeleteWalletError
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.logging.TangemLogger
/**
* Use case for deleting user wallet
@ -24,8 +25,10 @@ class DeleteWalletUseCase(
* @return [Either] with [com.tangem.domain.common.wallets.error.DeleteWalletError] or [Boolean] which indicates that there are still saved wallets.
* */
suspend operator fun invoke(userWalletId: UserWalletId): Either<DeleteWalletError, Boolean> {
return userWalletsListRepository.delete(userWalletIds = listOf(userWalletId)).map {
userWalletsListRepository.selectedUserWallet.value != null
return userWalletsListRepository.delete(userWalletIds = listOf(userWalletId))
.map { userWalletsListRepository.selectedUserWallet.value != null }
.onLeft {
TangemLogger.e("Failed to delete wallet with id ${userWalletId.value}: $it")
}
}
}

View file

@ -228,6 +228,7 @@ test-espresso-intents = { module = "androidx.test.espresso:espresso-intents", ve
test-junit = { module = "junit:junit", version.ref = "junit" }
test-junit5 = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit5" }
test-junit5-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit5" }
test-junit5-vintage-engine = { module = "org.junit.vintage:junit-vintage-engine", version.ref = "junit5" }
test-junit-android = { module = "androidx.test.ext:junit", version.ref = "junitAndroidExt" }
test-truth = { module = "com.google.truth:truth", version.ref = "truth" }
test-mockk = { module = "io.mockk:mockk", version.ref = "mockk" }

View file

@ -2,7 +2,6 @@ plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.kotlin.serialization)
alias(deps.plugins.hilt.android)
id("configuration")
}
@ -12,11 +11,7 @@ android {
}
dependencies {
implementation(projects.common)
implementation(projects.domain.models)
implementation(projects.domain.card)
implementation(projects.domain.legacy)
implementation(projects.domain.wallets.models)
implementation(projects.domain.visa.models)
implementation(projects.core.configToggles)
@ -28,8 +23,6 @@ dependencies {
exclude(module = "joda-time")
}
/** Other libraries */
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)

View file

@ -1,32 +0,0 @@
package com.tangem.sdk.api.di
import android.content.Context
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.operations.attestation.CardArtworksProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import java.io.File
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object CardSdkModule {
@Provides
@Singleton
fun provideCardArtworksProvider(
sdkRepository: CardSdkConfigRepository,
@ApplicationContext context: Context,
): CardArtworksProvider {
return CardArtworksProvider(
tangemApiBaseUrlProvider = { sdkRepository.sdk.config.tangemApiBaseUrl },
artworksDirectory = File(
context.getExternalFilesDir(null) ?: context.filesDir,
"card_artworks",
).apply { mkdirs() },
)
}
}