Updated on 2026-08-14
This commit is contained in:
parent
02e44033c9
commit
9b506f8b25
6 changed files with 107 additions and 19 deletions
|
|
@ -15,6 +15,7 @@ import com.tangem.domain.visa.model.VisaCardActivationStatus
|
|||
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
|
||||
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.hot.sdk.TangemHotSdk
|
||||
import com.tangem.sdk.storage.AndroidSecureStorage
|
||||
import com.tangem.sdk.storage.AndroidSecureStorageV2
|
||||
import com.tangem.sdk.storage.createEncryptedSharedPreferences
|
||||
|
|
@ -122,6 +123,7 @@ internal object UserWalletsListManagerModule {
|
|||
passwordRequester: HotWalletPasswordRequester,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository,
|
||||
tangemHotSdk: TangemHotSdk,
|
||||
): UserWalletsListRepository {
|
||||
val moshi = buildMoshi()
|
||||
val secureStorage = buildSecureStorage(applicationContext = applicationContext)
|
||||
|
|
@ -168,6 +170,7 @@ internal object UserWalletsListManagerModule {
|
|||
appPreferencesStore = appPreferencesStore,
|
||||
savePersistentInformation = ProviderSuspend { true }, // Always save persistent information for now
|
||||
hotWalletAccessCodeAttemptsRepository = hotWalletAccessCodeAttemptsRepository,
|
||||
tangemHotSdk = tangemHotSdk,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import com.tangem.domain.wallets.R
|
|||
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
||||
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
|
||||
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
|
||||
import com.tangem.hot.sdk.TangemHotSdk
|
||||
import com.tangem.hot.sdk.model.HotWalletId
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
|
||||
|
|
@ -28,6 +29,7 @@ import com.tangem.tap.domain.userWalletList.utils.updateWith
|
|||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.ProviderSuspend
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import com.tangem.utils.extensions.addOrReplace
|
||||
import com.tangem.utils.extensions.indexOfFirstOrNull
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
|
@ -45,6 +47,7 @@ internal class DefaultUserWalletsListRepository(
|
|||
private val savePersistentInformation: ProviderSuspend<Boolean>,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository,
|
||||
private val tangemHotSdk: TangemHotSdk,
|
||||
) : UserWalletsListRepository {
|
||||
|
||||
override val userWallets = MutableStateFlow<List<UserWallet>?>(null)
|
||||
|
|
@ -120,14 +123,19 @@ internal class DefaultUserWalletsListRepository(
|
|||
}
|
||||
}
|
||||
|
||||
val oldUserWallet = userWalletsSync().find { it.walletId == userWallet.walletId }
|
||||
|
||||
if (oldUserWallet != null) {
|
||||
checkForUpgradeAndDeleteHotWalletIfNeeded(
|
||||
newUserWallet = userWallet,
|
||||
oldUserWallet = oldUserWallet,
|
||||
)
|
||||
}
|
||||
|
||||
// update the userWallets state and add if it doesn't exist
|
||||
updateWallets { currentWallets ->
|
||||
val wallets = currentWallets.orEmpty()
|
||||
if (wallets.any { it.walletId == userWallet.walletId }) {
|
||||
wallets.map { if (it.walletId == userWallet.walletId) userWallet else it }
|
||||
} else {
|
||||
wallets + userWallet
|
||||
}
|
||||
wallets.addOrReplace(userWallet) { it.walletId == userWallet.walletId }
|
||||
}
|
||||
|
||||
// update the selectedUserWallet state if it is the only wallet
|
||||
|
|
@ -194,6 +202,8 @@ internal class DefaultUserWalletsListRepository(
|
|||
|
||||
userWalletEncryptionKeysRepository.delete(userWalletIds)
|
||||
|
||||
removeHotWalletsFromSDK(userWalletIds)
|
||||
|
||||
userWallets.update { currentWallets ->
|
||||
val updatedWallets = currentWallets?.filter { userWalletIds.contains(it.walletId).not() }
|
||||
selectedUserWallet.update { currentSelected ->
|
||||
|
|
@ -288,7 +298,12 @@ internal class DefaultUserWalletsListRepository(
|
|||
}
|
||||
|
||||
override suspend fun unlockAllWallets(): Either<UnlockWalletError, Unit> = either {
|
||||
val userWallets = userWalletsSync()
|
||||
val userWallets = userWalletsSync().filter { it.isLocked }
|
||||
|
||||
if (userWallets.isEmpty()) {
|
||||
return@either
|
||||
}
|
||||
|
||||
val biometricKeys = runSuspendCatching {
|
||||
userWalletEncryptionKeysRepository.getAllBiometric()
|
||||
}.getOrElse {
|
||||
|
|
@ -352,6 +367,27 @@ internal class DefaultUserWalletsListRepository(
|
|||
return userWallets.any { it.walletId !in unsecuredWalletIds }
|
||||
}
|
||||
|
||||
private suspend fun checkForUpgradeAndDeleteHotWalletIfNeeded(
|
||||
newUserWallet: UserWallet,
|
||||
oldUserWallet: UserWallet,
|
||||
) {
|
||||
if (newUserWallet.walletId == oldUserWallet.walletId &&
|
||||
oldUserWallet is UserWallet.Hot && newUserWallet is UserWallet.Cold
|
||||
) {
|
||||
removeHotWalletsFromSDK(walletIds = listOf(oldUserWallet.walletId))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun removeHotWalletsFromSDK(walletIds: List<UserWalletId>) {
|
||||
val hotWalletsToDelete = userWalletsSync()
|
||||
.filterIsInstance<UserWallet.Hot>()
|
||||
.filter { walletIds.contains(it.walletId) }
|
||||
|
||||
hotWalletsToDelete.forEach {
|
||||
tangemHotSdk.delete(it.hotWalletId)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun requestPasswordRecursive(
|
||||
hotWalletId: HotWalletId,
|
||||
block: suspend (CharArray) -> UserWalletEncryptionKey?,
|
||||
|
|
|
|||
|
|
@ -76,6 +76,16 @@ fun UserWallet.requireColdWallet(): UserWallet.Cold {
|
|||
?: error("This user wallet is not a cold wallet")
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
fun UserWallet.requireHotWallet(): UserWallet.Hot {
|
||||
contract {
|
||||
returns() implies (this@requireHotWallet is UserWallet.Hot)
|
||||
}
|
||||
|
||||
return this as? UserWallet.Hot
|
||||
?: error("This user wallet is not a hot wallet")
|
||||
}
|
||||
|
||||
fun UserWallet.copy(name: String = this.name, walletId: UserWalletId = this.walletId): UserWallet = when (this) {
|
||||
is UserWallet.Cold -> this.copy(name = name, walletId = walletId)
|
||||
is UserWallet.Hot -> this.copy(name = name, walletId = walletId)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,14 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
import com.tangem.domain.models.wallet.isLocked
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
|
||||
/**
|
||||
* Use case for unlocking a wallet using non-biometric methods.
|
||||
* If the wallet is already unlocked, it does nothing.
|
||||
*
|
||||
* **Does not** return [UnlockWalletError.AlreadyUnlocked] as an error, since the wallet is already unlocked.
|
||||
*
|
||||
* @see UnlockWalletUseCase
|
||||
*/
|
||||
class NonBiometricUnlockWalletUseCase(
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val walletsRepository: WalletsRepository,
|
||||
|
|
|
|||
|
|
@ -6,13 +6,26 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository
|
|||
import com.tangem.domain.common.wallets.error.UnlockWalletError
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
/**
|
||||
* Use case for unlocking a wallet using biometric authentication.
|
||||
* If biometric unlock fails, it falls back to non-biometric unlock methods.
|
||||
*
|
||||
* ** Does not** return [UnlockWalletError.AlreadyUnlocked] as an error, since the wallet is already unlocked.
|
||||
*
|
||||
* @see NonBiometricUnlockWalletUseCase
|
||||
*/
|
||||
class UnlockWalletUseCase(
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val nonBiometricUnlockWalletUseCase: NonBiometricUnlockWalletUseCase,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(userWalletId: UserWalletId): Either<UnlockWalletError, Unit> = either {
|
||||
userWalletsListRepository.unlockAllWallets()
|
||||
.mapLeft { nonBiometricUnlockWalletUseCase(userWalletId).bind() }
|
||||
userWalletsListRepository.unlock(userWalletId, UserWalletsListRepository.UnlockMethod.Biometric)
|
||||
.mapLeft { error ->
|
||||
when (error) {
|
||||
UnlockWalletError.AlreadyUnlocked -> Unit
|
||||
else -> nonBiometricUnlockWalletUseCase(userWalletId).bind()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
package com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
|
|
@ -17,6 +19,7 @@ import com.tangem.domain.models.scan.ScanResponse
|
|||
import com.tangem.domain.models.scan.isRing
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.requireColdWallet
|
||||
import com.tangem.domain.models.wallet.requireHotWallet
|
||||
import com.tangem.domain.onboarding.repository.OnboardingRepository
|
||||
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
|
|
@ -33,6 +36,7 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.resetCardDia
|
|||
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState.FinalizeStage.*
|
||||
import com.tangem.features.onboarding.v2.util.ResetCardsComponent
|
||||
import com.tangem.operations.backup.BackupService
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
import com.tangem.sdk.api.BackupServiceHolder
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import com.tangem.utils.StringsSigns
|
||||
|
|
@ -45,7 +49,7 @@ import kotlinx.coroutines.flow.update
|
|||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class MultiWalletFinalizeModel @Inject constructor(
|
||||
|
|
@ -255,7 +259,7 @@ internal class MultiWalletFinalizeModel @Inject constructor(
|
|||
OnboardingMultiWalletComponent.Mode.Onboarding,
|
||||
OnboardingMultiWalletComponent.Mode.ContinueFinalize,
|
||||
-> {
|
||||
saveWalletUseCase(
|
||||
saveWalletUseCase.invoke(
|
||||
userWallet = userWalletCreated.copy(
|
||||
scanResponse = scanResponse.updateScanResponseAfterBackup(),
|
||||
),
|
||||
|
|
@ -271,7 +275,7 @@ internal class MultiWalletFinalizeModel @Inject constructor(
|
|||
}
|
||||
?: userWalletCreated
|
||||
|
||||
updateWalletUseCase(
|
||||
updateWalletUseCase.invoke(
|
||||
userWalletId = userWallet.walletId,
|
||||
update = {
|
||||
it.requireColdWallet().copy(
|
||||
|
|
@ -283,14 +287,20 @@ internal class MultiWalletFinalizeModel @Inject constructor(
|
|||
userWallet
|
||||
}
|
||||
is OnboardingMultiWalletComponent.Mode.UpgradeHotWallet -> {
|
||||
saveWalletUseCase(
|
||||
userWallet = userWalletCreated.copy(
|
||||
scanResponse = scanResponse.updateScanResponseAfterBackup(),
|
||||
),
|
||||
canOverride = true,
|
||||
)
|
||||
// TODO [REDACTED_TASK_KEY] remove hot wallet after upgrade
|
||||
userWalletCreated
|
||||
updateWalletUseCase.invoke(
|
||||
userWalletId = userWalletCreated.walletId,
|
||||
update = { hotUserWallet ->
|
||||
val wallet = hotUserWallet.requireHotWallet()
|
||||
userWalletCreated.copy(
|
||||
name = wallet.name,
|
||||
scanResponse = scanResponse
|
||||
.updateScanResponseAfterBackup()
|
||||
.updateWithHotWallet(wallet),
|
||||
)
|
||||
},
|
||||
).getOrElse {
|
||||
error("Failed to upgrade to cold wallet. Error: $it")
|
||||
}
|
||||
}
|
||||
}.requireColdWallet()
|
||||
|
||||
|
|
@ -350,6 +360,14 @@ internal class MultiWalletFinalizeModel @Inject constructor(
|
|||
return copy(card = card)
|
||||
}
|
||||
|
||||
private fun ScanResponse.updateWithHotWallet(hotWallet: UserWallet.Hot): ScanResponse {
|
||||
return copy(
|
||||
derivedKeys = derivedKeys + hotWallet.wallets?.associate {
|
||||
ByteArrayKey(it.publicKey) to ExtendedPublicKeysMap(it.derivedKeys)
|
||||
}.orEmpty(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleActivationError() {
|
||||
_uiState.update { st ->
|
||||
st.copy(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue