Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-23 17:53:30 +03:00
parent 324ae269fe
commit 54833d8fc6
14 changed files with 166 additions and 111 deletions

View file

@ -11,8 +11,8 @@ import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.core.UserCodeRequestPolicy
import com.tangem.domain.card.ResetCardUseCase
import com.tangem.domain.card.ResetCardUserCodeParams
import com.tangem.domain.card.models.ResetCardError
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.domain.sdk.TangemSdkManager
@ -20,24 +20,25 @@ internal class DefaultResetCardUseCase(
private val tangemSdkManager: TangemSdkManager,
) : ResetCardUseCase {
override suspend fun invoke(card: CardDTO): Either<ResetCardError, Unit> = resourceScope {
either {
withUserCodeRequestPolicy(card)
override suspend fun invoke(cardId: String, params: ResetCardUserCodeParams): Either<ResetCardError, Boolean> =
resourceScope {
either {
withUserCodeRequestPolicy(params)
tangemSdkManager.resetToFactorySettings(
cardId = card.cardId,
allowsRequestAccessCodeFromRepository = true,
).bind(raise = this)
tangemSdkManager.resetToFactorySettings(
cardId = cardId,
allowsRequestAccessCodeFromRepository = true,
).bind(raise = this)
}
}
}
override suspend fun invoke(
cardNumber: Int,
card: CardDTO,
params: ResetCardUserCodeParams,
userWalletId: UserWalletId,
): Either<ResetCardError, Unit> = resourceScope {
): Either<ResetCardError, Boolean> = resourceScope {
either {
withUserCodeRequestPolicy(card)
withUserCodeRequestPolicy(params)
tangemSdkManager.resetBackupCard(
cardNumber = cardNumber,
@ -46,11 +47,11 @@ internal class DefaultResetCardUseCase(
}
}
private suspend fun ResourceScope.withUserCodeRequestPolicy(card: CardDTO) {
private suspend fun ResourceScope.withUserCodeRequestPolicy(params: ResetCardUserCodeParams) {
install(
acquire = {
val policyBeforeReset = tangemSdkManager.userCodeRequestPolicy
requestMandatoryAccessCodeEntry(card)
requestMandatoryAccessCodeEntry(params)
policyBeforeReset
},
@ -60,10 +61,10 @@ internal class DefaultResetCardUseCase(
)
}
private fun requestMandatoryAccessCodeEntry(card: CardDTO) {
val type = if (card.isAccessCodeSet) {
private fun requestMandatoryAccessCodeEntry(params: ResetCardUserCodeParams) {
val type = if (params.isAccessCodeSet) {
UserCodeType.AccessCode
} else if (card.isPasscodeSet == true) {
} else if (params.isPasscodeSet == true) {
UserCodeType.Passcode
} else {
null
@ -74,15 +75,14 @@ internal class DefaultResetCardUseCase(
}
}
private fun CompletionResult<*>.bind(raise: Raise<ResetCardError>) {
private fun CompletionResult<Boolean>.bind(raise: Raise<ResetCardError>): Boolean {
return when (this) {
is CompletionResult.Failure -> {
val domainError = error.mapToDomainError()
raise.raise(domainError)
}
is CompletionResult.Success -> { /* no-op */
}
is CompletionResult.Success -> data
}
}

View file

@ -70,9 +70,9 @@ interface TangemSdkManager {
suspend fun resetToFactorySettings(
cardId: String,
allowsRequestAccessCodeFromRepository: Boolean,
): CompletionResult<CardDTO>
): CompletionResult<Boolean>
suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult<Unit>
suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult<Boolean>
suspend fun saveAccessCode(accessCode: String, cardsIds: Set<String>): CompletionResult<Unit>

View file

@ -202,7 +202,7 @@ class DefaultTangemSdkManager(
override suspend fun resetToFactorySettings(
cardId: String,
allowsRequestAccessCodeFromRepository: Boolean,
): CompletionResult<CardDTO> {
): CompletionResult<Boolean> {
return runTaskAsyncReturnOnMain(
runnable = ResetToFactorySettingsTask(
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
@ -210,10 +210,9 @@ class DefaultTangemSdkManager(
cardId = cardId,
initialMessage = Message(resources.getString(R.string.card_settings_reset_card_to_factory)),
)
.map { CardDTO(it) }
}
override suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult<Unit> {
override suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult<Boolean> {
return runTaskAsyncReturnOnMain(
runnable = ResetBackupCardTask(userWalletId),
initialMessage = Message(

View file

@ -87,12 +87,12 @@ class MockTangemSdkManager(
override suspend fun resetToFactorySettings(
cardId: String,
allowsRequestAccessCodeFromRepository: Boolean,
): CompletionResult<CardDTO> {
return MockProvider.getCardDto()
): CompletionResult<Boolean> {
return CompletionResult.Success(true)
}
override suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult<Unit> {
return CompletionResult.Success(Unit)
override suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult<Boolean> {
return CompletionResult.Success(true)
}
override suspend fun saveAccessCode(accessCode: String, cardsIds: Set<String>): CompletionResult<Unit> {

View file

@ -19,11 +19,11 @@ import com.tangem.tap.domain.tasks.UserWalletIdPreflightReadFilter
*/
internal class ResetBackupCardTask(
private val userWalletId: UserWalletId,
) : CardSessionRunnable<Unit> {
) : CardSessionRunnable<Boolean> {
override val allowsRequestAccessCodeFromRepository: Boolean = false
override fun run(session: CardSession, callback: CompletionCallback<Unit>) {
override fun run(session: CardSession, callback: CompletionCallback<Boolean>) {
PreflightReadTask(
readMode = PreflightReadMode.FullCardRead,
filter = UserWalletIdPreflightReadFilter(expectedUserWalletId = userWalletId),
@ -35,10 +35,10 @@ internal class ResetBackupCardTask(
}
}
private fun resetCard(session: CardSession, callback: CompletionCallback<Unit>) {
private fun resetCard(session: CardSession, callback: CompletionCallback<Boolean>) {
ResetToFactorySettingsTask(allowsRequestAccessCodeFromRepository).run(session) { result ->
when (result) {
is CompletionResult.Success -> callback(CompletionResult.Success(Unit))
is CompletionResult.Success -> callback(CompletionResult.Success(result.data))
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}

View file

@ -10,13 +10,15 @@ import com.tangem.operations.wallet.PurgeWalletCommand
class ResetToFactorySettingsTask(
override val allowsRequestAccessCodeFromRepository: Boolean,
) : CardSessionRunnable<Card> {
) : CardSessionRunnable<Boolean> {
override fun run(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
private var isResetCompleted = false
override fun run(session: CardSession, callback: (result: CompletionResult<Boolean>) -> Unit) {
deleteWallets(session, callback)
}
private fun deleteWallets(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
private fun deleteWallets(session: CardSession, callback: (result: CompletionResult<Boolean>) -> Unit) {
val wallet = session.environment.card?.wallets?.lastOrNull().guard {
resetBackup(session, callback)
return
@ -25,6 +27,7 @@ class ResetToFactorySettingsTask(
PurgeWalletCommand(wallet.publicKey).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
isResetCompleted = true
deleteWallets(session, callback)
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
@ -32,18 +35,18 @@ class ResetToFactorySettingsTask(
}
}
private fun resetBackup(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
private fun resetBackup(session: CardSession, callback: (result: CompletionResult<Boolean>) -> Unit) {
if (session.environment.card?.backupStatus == null ||
session.environment.card?.backupStatus == Card.BackupStatus.NoBackup
) {
callback(CompletionResult.Success(session.environment.card!!))
callback(CompletionResult.Success(isResetCompleted))
return
}
ResetBackupCommand().run(session) { result ->
when (result) {
is CompletionResult.Success -> {
callback(CompletionResult.Success(session.environment.card!!))
callback(CompletionResult.Success(true))
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}

View file

@ -4,7 +4,6 @@ import android.os.Bundle
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import arrow.core.getOrElse
import com.tangem.common.CompletionResult
import com.tangem.common.doOnSuccess
import com.tangem.common.routing.AppRoute
@ -14,10 +13,9 @@ import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchDialogShow
@ -38,7 +36,6 @@ import javax.inject.Inject
@HiltViewModel
internal class CardSettingsViewModel @Inject constructor(
private val scanCardProcessor: ScanCardProcessor,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val tangemSdkManager: TangemSdkManager,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
@ -47,6 +44,8 @@ internal class CardSettingsViewModel @Inject constructor(
?.unbundle(UserWalletId.serializer())
?: error("User wallet ID is required for CardSettingsViewModel")
private val scannedScanResponse = MutableStateFlow<ScanResponse?>(value = null)
val screenState: MutableStateFlow<CardSettingsScreenState> = MutableStateFlow(getInitialState())
private fun getInitialState() = CardSettingsScreenState(
@ -58,13 +57,11 @@ internal class CardSettingsViewModel @Inject constructor(
private fun scanCard() = viewModelScope.launch {
scanCardProcessor.scan(allowsRequestAccessCodeFromRepository = true)
.doOnSuccess { scanResponse ->
scannedScanResponse.value = scanResponse
val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
val isCorrectUserWalletScanned = scannedUserWalletId == userWalletId
if (isCorrectUserWalletScanned) {
val userWallet = getUserWallet()
updateCardDetails(userWallet)
if (userWalletId == scannedUserWalletId || scannedUserWalletId == null) {
updateCardDetails(scanResponse)
} else {
store.dispatchDialogShow(
AppDialog.SimpleOkDialogRes(
@ -76,9 +73,9 @@ internal class CardSettingsViewModel @Inject constructor(
}
}
private fun updateCardDetails(userWallet: UserWallet) {
val card = userWallet.scanResponse.card
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
private fun updateCardDetails(scanResponse: ScanResponse) {
val card = scanResponse.card
val cardTypesResolver = scanResponse.cardTypesResolver
val cardId = cardTypesResolver.getCardId()
val currentSecurityOption = getCurrentSecurityOption(card)
@ -112,7 +109,7 @@ internal class CardSettingsViewModel @Inject constructor(
if (isResetCardAllowed) {
CardInfo.ResetToFactorySettings(
description = getResetToFactoryDescription(card, cardTypesResolver),
description = getResetToFactoryDescription(card.backupStatus, cardTypesResolver),
).let(::add)
}
}
@ -129,9 +126,21 @@ internal class CardSettingsViewModel @Inject constructor(
changeAccessCode()
}
is CardInfo.ResetToFactorySettings -> {
val card = requireNotNull(scannedScanResponse.value) {
"Impossible to reset card if ScanResponse is null"
}.card
Analytics.send(Settings.CardSettings.ButtonFactoryReset())
store.dispatchNavigationAction {
push(route = AppRoute.ResetToFactory(userWalletId))
push(
route = AppRoute.ResetToFactory(
userWalletId = userWalletId,
cardSpecificInfo = AppRoute.ResetToFactory.CardSpecificInfo(
cardId = card.cardId,
backupStatus = card.backupStatus,
),
),
)
}
}
is CardInfo.SecurityMode -> {
@ -150,9 +159,9 @@ internal class CardSettingsViewModel @Inject constructor(
}
private fun changeAccessCode() = viewModelScope.launch {
val card = getUserWallet().scanResponse.card
val scanResponse = requireNotNull(scannedScanResponse.value) { "Scan response is null" }
when (val result = tangemSdkManager.setAccessCode(card.cardId)) {
when (val result = tangemSdkManager.setAccessCode(scanResponse.card.cardId)) {
is CompletionResult.Success -> Analytics.send(Settings.CardSettings.UserCodeChanged())
is CompletionResult.Failure -> {
Timber.e("Failed to change access code: ${result.error}")
@ -160,12 +169,6 @@ internal class CardSettingsViewModel @Inject constructor(
}
}
private fun getUserWallet(): UserWallet {
return getUserWalletUseCase(userWalletId).getOrElse {
error("Failed to get user wallet $userWalletId: $it")
}
}
private fun isResetToFactoryAllowedByCard(card: CardDTO, cardTypesResolver: CardTypesResolver): Boolean {
val hasPermanentWallet = card.wallets.any { it.settings.isPermanent }
val isNotAllowed = hasPermanentWallet || cardTypesResolver.isStart2Coin()

View file

@ -5,8 +5,11 @@ import com.tangem.domain.models.scan.CardDTO
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.wallet.R
internal fun getResetToFactoryDescription(card: CardDTO, typesResolver: CardTypesResolver): TextReference {
return if (card.backupStatus?.isActive != true || typesResolver.isTangemTwins()) {
internal fun getResetToFactoryDescription(
backupStatus: CardDTO.BackupStatus?,
typesResolver: CardTypesResolver,
): TextReference {
return if (backupStatus?.isActive != true || typesResolver.isTangemTwins()) {
TextReference.Res(R.string.reset_card_without_backup_to_factory_message)
} else {
TextReference.Res(R.string.reset_card_with_backup_to_factory_message)

View file

@ -11,12 +11,11 @@ import com.tangem.common.routing.utils.popTo
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.card.DeleteSavedAccessCodesUseCase
import com.tangem.domain.card.ResetCardUseCase
import com.tangem.domain.card.ResetCardUserCodeParams
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.DeleteWalletUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
@ -39,7 +38,7 @@ import javax.inject.Inject
@Suppress("LongParameterList")
@HiltViewModel
internal class ResetCardViewModel @Inject constructor(
private val getUserWalletUseCase: GetUserWalletUseCase,
getUserWalletUseCase: GetUserWalletUseCase,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val resetCardUseCase: ResetCardUseCase,
private val deleteSavedAccessCodesUseCase: DeleteSavedAccessCodesUseCase,
@ -49,9 +48,26 @@ internal class ResetCardViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
) : ViewModel() {
private val userWalletId = savedStateHandle.get<Bundle>(AppRoute.ResetToFactory.USER_WALLET_ID)
// region Card-set specific data. All cards from single set have the same userWalletId and cardTypesResolver
private val currentUserWalletId = savedStateHandle.get<Bundle>(AppRoute.ResetToFactory.USER_WALLET_ID)
?.unbundle(UserWalletId.serializer())
?: error("User wallet ID must be provided for ResetCardViewModel")
?: error("UserWalletId must be provided for ResetCardViewModel")
// Use only for card-specific data
private val userWallet = getUserWalletUseCase(userWalletId = currentUserWalletId)
.getOrElse { error("Failed to get user wallet: $it") }
private val currentCardTypesResolver = userWallet.cardTypesResolver
private val currentUserCodeParams = ResetCardUserCodeParams(
isAccessCodeSet = userWallet.scanResponse.card.isAccessCodeSet,
isPasscodeSet = userWallet.scanResponse.card.isPasscodeSet,
)
// endregion
// region Data of card that was scanned on CardSettings
private val primaryCardId: String
private val primaryBackupStatus: CardDTO.BackupStatus?
// endregion
// TODO: move logic to separate domain entity
private var resetBackupCardCount = 0
@ -60,29 +76,33 @@ internal class ResetCardViewModel @Inject constructor(
value = getInitialState(),
)
private fun getInitialState(): ResetCardScreenState {
val userWallet = getUserWalletUseCase(userWalletId).getOrElse {
error("Failed to get user wallet $userWalletId: $it")
}
val card = userWallet.scanResponse.card
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
val descriptionText = getResetToFactoryDescription(card, cardTypesResolver)
val isTangemWallet = cardTypesResolver.isTangemWallet() || cardTypesResolver.isWallet2()
val showResetPasswordButton = isTangemWallet && card.backupStatus is CardDTO.BackupStatus.Active
init {
val cardSpecificInfo = savedStateHandle.get<Bundle>(AppRoute.ResetToFactory.CARD_SPECIFIC_DATA)
?.unbundle(AppRoute.ResetToFactory.CardSpecificInfo.serializer())
?: error("CardSpecificData must be provided for ResetCardViewModel")
primaryCardId = cardSpecificInfo.cardId
primaryBackupStatus = cardSpecificInfo.backupStatus
}
private fun getInitialState(): ResetCardScreenState {
val shouldShowResetPasswordButton = shouldShowResetPasswordButton()
val warningsToShow = buildList {
add(ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS)
if (showResetPasswordButton) {
if (shouldShowResetPasswordButton) {
add(ResetCardScreenState.WarningsToReset.LOST_PASSWORD_RESTORE)
}
}
return ResetCardScreenState(
resetButtonEnabled = false,
descriptionText = descriptionText,
descriptionText = getResetToFactoryDescription(
backupStatus = primaryBackupStatus,
typesResolver = currentCardTypesResolver,
),
warningsToShow = warningsToShow,
showResetPasswordButton = showResetPasswordButton,
showResetPasswordButton = shouldShowResetPasswordButton,
acceptCondition1Checked = false,
acceptCondition2Checked = false,
onAcceptCondition1ToggleClick = ::toggleFirstCondition,
@ -92,6 +112,12 @@ internal class ResetCardViewModel @Inject constructor(
)
}
private fun shouldShowResetPasswordButton(): Boolean {
val isTangemWallet = currentCardTypesResolver.isTangemWallet() || currentCardTypesResolver.isWallet2()
return isTangemWallet && primaryBackupStatus is CardDTO.BackupStatus.Active
}
private fun toggleFirstCondition(isAccepted: Boolean) {
screenState.update { prevState ->
val resetButtonEnabled = if (prevState.showResetPasswordButton) {
@ -154,12 +180,9 @@ internal class ResetCardViewModel @Inject constructor(
private fun makeFullReset() {
viewModelScope.launch {
val userWallet = getUserWallet()
val scanResponse = userWallet.scanResponse
resetCardUseCase(card = scanResponse.card).onRight {
deleteSavedAccessCodesUseCase(scanResponse.card.cardId)
val hasUserWallets = deleteWalletUseCase(userWalletId).getOrElse {
resetCardUseCase(cardId = primaryCardId, params = currentUserCodeParams).onRight {
deleteSavedAccessCodesUseCase(cardId = primaryCardId)
val hasUserWallets = deleteWalletUseCase(userWalletId = currentUserWalletId).getOrElse {
Timber.e("Unable to delete user wallet: $it")
return@launch
}
@ -183,14 +206,15 @@ internal class ResetCardViewModel @Inject constructor(
dismissDialog()
viewModelScope.launch {
val userWallet = getUserWallet()
resetCardUseCase(
cardNumber = resetBackupCardCount + 1,
card = userWallet.scanResponse.card,
userWalletId = userWalletId,
params = currentUserCodeParams,
userWalletId = currentUserWalletId,
)
.onRight {
resetBackupCardCount++
.onRight { isResetCompleted ->
if (isResetCompleted) {
resetBackupCardCount++
}
delay(DELAY_SDK_DIALOG_CLOSE)
@ -213,7 +237,7 @@ internal class ResetCardViewModel @Inject constructor(
}
private fun checkRemainingBackupCards() {
val backupCardsCount = getUserWallet().scanResponse.getBackupCardsCount()
val backupCardsCount = getBackupCardsCount()
when {
backupCardsCount > resetBackupCardCount -> showDialog(ResetCardDialog.ContinueResetDialog)
@ -227,12 +251,6 @@ internal class ResetCardViewModel @Inject constructor(
}
}
private fun getUserWallet(): UserWallet {
return getUserWalletUseCase(userWalletId).getOrElse {
error("Failed to get user wallet $userWalletId: $it")
}
}
private fun dismissAndFinishFullReset() {
dismissDialog()
@ -249,7 +267,7 @@ internal class ResetCardViewModel @Inject constructor(
if (isLocked && userWalletsListManager.hasUserWallets) {
store.dispatchNavigationAction { popTo<AppRoute.Welcome>() }
} else {
store.dispatchNavigationAction { popTo<AppRoute.Home>() }
store.dispatchNavigationAction { replaceAll(AppRoute.Home) }
}
}
}
@ -262,10 +280,10 @@ internal class ResetCardViewModel @Inject constructor(
screenState.update { it.copy(dialog = null) }
}
private fun ScanResponse.getBackupCardsCount(): Int {
if (!cardTypesResolver.isMultiwalletAllowed()) return 0
private fun getBackupCardsCount(): Int {
if (!currentCardTypesResolver.isMultiwalletAllowed()) return 0
return when (val status = card.backupStatus) {
return when (val status = primaryBackupStatus) {
is CardDTO.BackupStatus.Active -> status.cardCount
is CardDTO.BackupStatus.CardLinked,
is CardDTO.BackupStatus.NoBackup,