Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-03 13:42:07 +03:00
commit d994b7840a
150 changed files with 971 additions and 4621 deletions

View file

@ -16,19 +16,14 @@ import com.tangem.domain.visa.model.VisaActivationRemoteState
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
import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager
import com.tangem.tap.domain.userWalletList.implementation.GeneralUserWalletsListManager
import com.tangem.tap.domain.userWalletList.implementation.RuntimeUserWalletsListManager
import com.tangem.tap.domain.userWalletList.repository.DefaultUserWalletsListRepository
import com.tangem.tap.domain.userWalletList.repository.DelegatedKeystoreManager
import com.tangem.tap.domain.userWalletList.repository.UserWalletEncryptionKeysRepository
import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysStoreDecorator
import com.tangem.tap.domain.userWalletList.repository.implementation.BiometricUserWalletsKeysRepository
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultSelectedUserWalletRepository
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsPublicInformationRepository
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository
@ -47,77 +42,6 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
internal object UserWalletsListManagerModule {
@Provides
@Singleton
@Deprecated("Use UserWalletsListRepository instead")
fun provideGeneralUserWalletsListManager(
@ApplicationContext applicationContext: Context,
appPreferencesStore: AppPreferencesStore,
dispatchers: CoroutineDispatcherProvider,
analyticsEventHandler: AnalyticsEventHandler,
): UserWalletsListManager {
return GeneralUserWalletsListManager(
runtimeUserWalletsListManager = RuntimeUserWalletsListManager(),
biometricUserWalletsListManager = createBiometricUserWalletsListManager(
applicationContext = applicationContext,
analyticsEventHandler = analyticsEventHandler,
dispatchers = dispatchers,
),
appPreferencesStore = appPreferencesStore,
dispatchers = dispatchers,
)
}
@Deprecated("Use UserWalletsListRepository instead")
private fun createBiometricUserWalletsListManager(
applicationContext: Context,
analyticsEventHandler: AnalyticsEventHandler,
dispatchers: CoroutineDispatcherProvider,
): UserWalletsListManager {
val moshi = buildMoshi()
val secureStorage = buildSecureStorage(applicationContext = applicationContext)
val authenticatedStorage = AuthenticatedStorage(
secureStorage = UserWalletsKeysStoreDecorator(
featureStorage = secureStorage,
cardSdkStorageProvider = Provider { tangemSdkManager.secureStorage },
),
keystoreManager = DelegatedKeystoreManager(
keystoreManagerProvider = Provider { tangemSdkManager.keystoreManager },
),
)
val keysRepository = BiometricUserWalletsKeysRepository(
moshi = moshi,
secureStorage = secureStorage,
authenticatedStorage = authenticatedStorage,
analyticsEventHandler = analyticsEventHandler,
)
val publicInformationRepository = DefaultUserWalletsPublicInformationRepository(
moshi = moshi,
secureStorage = secureStorage,
)
val sensitiveInformationRepository = DefaultUserWalletsSensitiveInformationRepository(
moshi = moshi,
secureStorage = secureStorage,
)
val selectedUserWalletRepository = DefaultSelectedUserWalletRepository(
secureStorage = secureStorage,
dispatchers = dispatchers,
)
return BiometricUserWalletsListManager(
keysRepository = keysRepository,
publicInformationRepository = publicInformationRepository,
sensitiveInformationRepository = sensitiveInformationRepository,
selectedUserWalletRepository = selectedUserWalletRepository,
dispatcherProvider = dispatchers,
)
}
@Provides
@Singleton
fun provideUserWalletsListRepository(
@ -183,7 +107,7 @@ internal object UserWalletsListManagerModule {
)
}
fun buildMoshi(): Moshi {
private fun buildMoshi(): Moshi {
return Moshi.Builder()
.add(WalletDerivedKeysMapAdapter())
.add(ScanResponseDerivedKeysMapAdapter())
@ -200,7 +124,7 @@ internal object UserWalletsListManagerModule {
.build()
}
fun buildSecureStorage(@ApplicationContext applicationContext: Context): SecureStorage {
private fun buildSecureStorage(@ApplicationContext applicationContext: Context): SecureStorage {
return AndroidSecureStorage(
preferences = SecureStorage.createEncryptedSharedPreferences(
context = applicationContext,

View file

@ -1,434 +0,0 @@
package com.tangem.tap.domain.userWalletList.implementation
import com.tangem.common.*
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.wallets.legacy.UserWalletsListError
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
import com.tangem.tap.domain.userWalletList.repository.SelectedUserWalletRepository
import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysRepository
import com.tangem.tap.domain.userWalletList.repository.UserWalletsPublicInformationRepository
import com.tangem.tap.domain.userWalletList.repository.UserWalletsSensitiveInformationRepository
import com.tangem.tap.domain.userWalletList.utils.encryptionKey
import com.tangem.tap.domain.userWalletList.utils.lockAll
import com.tangem.tap.domain.userWalletList.utils.toUserWallets
import com.tangem.tap.domain.userWalletList.utils.updateWith
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import timber.log.Timber
@Suppress("LargeClass")
@OptIn(ExperimentalCoroutinesApi::class)
internal class BiometricUserWalletsListManager(
private val keysRepository: UserWalletsKeysRepository,
private val publicInformationRepository: UserWalletsPublicInformationRepository,
private val sensitiveInformationRepository: UserWalletsSensitiveInformationRepository,
private val selectedUserWalletRepository: SelectedUserWalletRepository,
private val dispatcherProvider: CoroutineDispatcherProvider,
) : UserWalletsListManager.Lockable {
private val state = MutableStateFlow(State())
private var hasSavedWallets: Boolean? = null
private val savedWalletMutex = Mutex()
override val isLockable: Boolean = true
override val userWallets: Flow<List<UserWallet>>
get() = state
.mapLatest { it.userWallets }
.distinctUntilChanged()
override val userWalletsSync: List<UserWallet>
get() = state.value.userWallets
@Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features")
override val selectedUserWallet: Flow<UserWallet>
get() = state
.mapLatest { state ->
findSelectedUserWallet(state.userWallets)
}
.filterNotNull()
.distinctUntilChanged()
@Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features")
override val selectedUserWalletSync: UserWallet?
get() = findSelectedUserWallet()
override val lockedState: Flow<Boolean>
get() = state
.mapLatest { it.isLocked }
.distinctUntilChanged()
override val isLocked: Boolean
get() = state.value.isLocked
override val hasUserWallets: Boolean
get() {
return runBlocking {
// workaround to avoid calling hasSavedEncryptionKeys many times because of performance
savedWalletMutex.withLock {
Timber.i("Checking if user has saved wallets")
val hasSavedWalletsLocal = hasSavedWallets
if (hasSavedWalletsLocal == null || !hasSavedWalletsLocal) {
val hasKeys = keysRepository.hasSavedEncryptionKeys()
hasSavedWallets = hasKeys
hasKeys
} else {
Timber.i("User has saved wallets (from cache)")
true
}
}
}
}
override val walletsCount: Int
get() = state.value.userWallets.size
override val savedWalletsCount: Flow<Int>
get() = state
.mapLatest { walletsCount }
.distinctUntilChanged()
override suspend fun unlock(type: UnlockType): CompletionResult<UserWallet> {
return withContext(dispatcherProvider.io) {
unlockAndSetSelectedUserWallet(type)
.mapFailure { error ->
Timber.e(error, "Unable to unlock user wallets")
if (error is UserWalletsListError) {
error
} else {
UserWalletsListError.UnableToUnlockUserWallets(error)
}
}
.map { selectedUserWallet ->
if (selectedUserWallet == null || selectedUserWallet.isLocked) {
Timber.e("Unable to find selected user wallet")
throw UserWalletsListError.NoUserWalletSelected
} else {
selectedUserWallet
}
}
}
}
override fun lock() {
state.update { prevState ->
prevState.copy(
encryptionKeys = emptyList(),
userWallets = prevState.userWallets.lockAll(),
isLocked = true,
)
}
}
override suspend fun select(userWalletId: UserWalletId): CompletionResult<UserWallet> = catching {
if (state.value.selectedUserWalletId == userWalletId) {
return@catching requireNotNull(findSelectedUserWallet()) {
"Wallet is not found"
}
}
selectedUserWalletRepository.set(userWalletId)
val newState = state.updateAndGet { prevState ->
prevState.copy(
selectedUserWalletId = userWalletId,
)
}
newState.userWallets.first { it.walletId == userWalletId }
}
override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit> {
return withContext(dispatcherProvider.io) {
if (canOverride) {
saveInternal(userWallet, changeSelectedUserWallet = true, canOverridePublicInfo = false)
} else {
val isWalletSaved = state.value.userWallets
.any {
it.walletId == userWallet.walletId
}
if (isWalletSaved) {
CompletionResult.Failure(UserWalletsListError.WalletAlreadySaved)
} else {
saveInternal(userWallet, changeSelectedUserWallet = true, canOverridePublicInfo = false)
}
}
}
}
override suspend fun update(
userWalletId: UserWalletId,
update: suspend (UserWallet) -> UserWallet,
): CompletionResult<UserWallet> {
return withContext(dispatcherProvider.io) {
get(userWalletId)
.map { storedUserWallet ->
update(storedUserWallet)
}
.flatMap { updatedUserWallet ->
saveInternal(updatedUserWallet, changeSelectedUserWallet = false, canOverridePublicInfo = true)
}
.flatMap {
get(userWalletId)
}
}
}
override suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit> {
val idsToRemove = state.value.userWallets
.takeIf { it.isNotEmpty() }
?.filter { it.walletId in userWalletIds }
?.map { it.walletId }
if (idsToRemove.isNullOrEmpty()) {
return CompletionResult.Success(Unit)
}
if (idsToRemove.size == state.value.userWallets.size) {
return clear()
}
return withContext(dispatcherProvider.io) {
sensitiveInformationRepository.delete(idsToRemove)
.flatMap { publicInformationRepository.delete(idsToRemove) }
.map { keysRepository.delete(idsToRemove) }
.map {
state.update { prevState ->
val remainingWallets = prevState.userWallets.filter { it.walletId !in idsToRemove }
val isSelectedWalletDeleted = prevState.selectedUserWalletId in idsToRemove
val newSelectedUserWallet = findOrSetSelectedWallet(
prevSelectedWalletId = prevState.selectedUserWalletId,
prevSelectedWalletIndex = prevState.userWallets.indexOfFirst {
it.walletId == prevState.selectedUserWalletId
},
userWallets = remainingWallets,
ignorePrevSelectedWallet = isSelectedWalletDeleted,
)
prevState.copy(
encryptionKeys = prevState.encryptionKeys.filter { it.walletId !in idsToRemove },
userWallets = remainingWallets,
isLocked = remainingWallets.any { it.isLocked },
selectedUserWalletId = newSelectedUserWallet?.walletId,
)
}
}
}
}
override suspend fun clear(): CompletionResult<Unit> {
savedWalletMutex.withLock {
hasSavedWallets = null
}
return withContext(dispatcherProvider.io) {
sensitiveInformationRepository.clear()
.flatMap { publicInformationRepository.clear() }
.map {
keysRepository.clear()
selectedUserWalletRepository.set(null)
state.value = State()
}
}
}
override suspend fun get(userWalletId: UserWalletId): CompletionResult<UserWallet> {
return catching {
state.value.userWallets.first { it.walletId == userWalletId }
}
}
private suspend fun saveInternal(
userWallet: UserWallet,
changeSelectedUserWallet: Boolean,
canOverridePublicInfo: Boolean,
): CompletionResult<Unit> {
val encryptionKey = userWallet.encryptionKey
?.let { UserWalletEncryptionKey(userWallet.walletId, it) }
?: return CompletionResult.Success(Unit) // No encryption key, no need to save
return keysRepository.save(encryptionKey)
.flatMap { sensitiveInformationRepository.save(userWallet, encryptionKey = encryptionKey.encryptionKey) }
.flatMap { publicInformationRepository.save(userWallet, canOverridePublicInfo) }
.flatMap {
loadUserWallets(
encryptionKeys = state.value.encryptionKeys
.plus(encryptionKey)
.distinctBy(UserWalletEncryptionKey::walletId),
)
}
.doOnSuccess { loadedState ->
if (changeSelectedUserWallet) {
selectedUserWalletRepository.set(userWallet.walletId)
state.value = loadedState.copy(
selectedUserWalletId = userWallet.walletId,
)
} else {
state.value = loadedState
}
}
.map { /* Type erasing */ }
}
private suspend fun unlockAndSetSelectedUserWallet(type: UnlockType): CompletionResult<UserWallet?> {
return keysRepository.getAll()
.flatMap { encryptionKeys ->
loadUserWallets(
encryptionKeys = state.value.encryptionKeys
.plus(encryptionKeys)
.distinctBy(UserWalletEncryptionKey::walletId),
)
}
.map { loadedState ->
when (type) {
UnlockType.ALL -> {
if (loadedState.isLocked) {
Timber.e("Some user wallets remain locked")
state.value = loadedState
throw UserWalletsListError.NotAllUserWalletsUnlocked
} else {
val prevState = state.value
val selectedWallet = findOrSetSelectedWallet(
prevSelectedWalletId = prevState.selectedUserWalletId,
userWallets = loadedState.userWallets,
prevSelectedWalletIndex = prevState.userWallets.indexOfFirst {
it.walletId == prevState.selectedUserWalletId
},
)
state.value = loadedState.copy(selectedUserWalletId = selectedWallet?.walletId)
selectedWallet
}
}
UnlockType.ANY -> {
val prevState = state.value
val selectedWallet = findOrSetSelectedWallet(
prevSelectedWalletId = state.value.selectedUserWalletId,
prevSelectedWalletIndex = prevState.userWallets.indexOfFirst {
it.walletId == prevState.selectedUserWalletId
},
userWallets = loadedState.userWallets,
)
state.value = loadedState.copy(selectedUserWalletId = selectedWallet?.walletId)
selectedWallet
}
UnlockType.ALL_WITHOUT_SELECT -> {
state.value = loadedState
findSelectedUserWallet()
}
}
}
}
private suspend fun loadUserWallets(encryptionKeys: List<UserWalletEncryptionKey>): CompletionResult<State> {
return publicInformationRepository.getAll()
.map { it.toUserWallets() }
.flatMap { userWallets ->
sensitiveInformationRepository.getAll(encryptionKeys)
.map { walletIdToSensitiveInformation ->
userWallets.updateWith(walletIdToSensitiveInformation)
}
}
.map { userWallets ->
val prevState = state.value
if (userWallets.isNotEmpty()) {
val newUserWallets = (userWallets + prevState.userWallets)
.distinctBy(UserWallet::walletId)
prevState.copy(
userWallets = newUserWallets,
encryptionKeys = encryptionKeys,
isLocked = newUserWallets.any(UserWallet::isLocked),
)
} else {
prevState
}
}
}
private suspend fun findOrSetSelectedWallet(
prevSelectedWalletId: UserWalletId?,
prevSelectedWalletIndex: Int,
userWallets: List<UserWallet>,
ignorePrevSelectedWallet: Boolean = false,
): UserWallet? {
var possibleSelectedUserWallet: UserWallet? = null
if (!ignorePrevSelectedWallet) {
val selectedWalletId = prevSelectedWalletId ?: selectedUserWalletRepository.get()
possibleSelectedUserWallet = findSelectedUserWallet(userWallets, selectedWalletId)
}
if (possibleSelectedUserWallet == null || possibleSelectedUserWallet.isLocked) {
possibleSelectedUserWallet =
userWallets.findAvailableUserWallet(prevSelectedIndex = prevSelectedWalletIndex)
}
selectedUserWalletRepository.set(possibleSelectedUserWallet?.walletId)
return possibleSelectedUserWallet
}
/**
* Find the nearest available wallet that can be selected
*
* Example:
* Number with *n* is previous selected wallet with index [prevSelectedIndex].
*
* 1. [*1*, 2, 3, 4] => delete 1 => [2, 3, 4] => find and select => [*2*, 3, 4]
* 2. [1, *2*, 3, 4] => delete 2 => [1, 3, 4] => find and select => [1, *3*, 4]
* 3. [1, 2, *3*, 4] => delete 3 => [1, 2, 4] => find and select => [1, 2, *4*]
* 4. [1, 2, 3, *4*] => delete 4 => [1, 2, 3] => find and select => [1, 2, *3*]
*
* @receiver list of user wallets without deleted wallet
*/
private fun List<UserWallet>.findAvailableUserWallet(prevSelectedIndex: Int): UserWallet? {
if (prevSelectedIndex == 0) return firstOrNull { !it.isLocked } ?: firstOrNull()
if (prevSelectedIndex in indices && !this[prevSelectedIndex].isLocked) return this[prevSelectedIndex]
for (offset in 1..size) {
val rightIndex = prevSelectedIndex + offset
if (rightIndex in indices && !this[rightIndex].isLocked) return this[rightIndex]
val leftIndex = prevSelectedIndex - offset
if (leftIndex in indices && !this[leftIndex].isLocked) return this[leftIndex]
}
return lastOrNull()
}
private fun findSelectedUserWallet(
userWallets: List<UserWallet> = state.value.userWallets,
selectedUserWalletId: UserWalletId? = state.value.selectedUserWalletId,
): UserWallet? {
return userWallets.firstOrNull { it.walletId == selectedUserWalletId }
}
private data class State(
val encryptionKeys: List<UserWalletEncryptionKey> = emptyList(),
val userWallets: List<UserWallet> = emptyList(),
val selectedUserWalletId: UserWalletId? = null,
val isLocked: Boolean = true,
)
}

View file

@ -1,203 +0,0 @@
package com.tangem.tap.domain.userWalletList.implementation
import com.tangem.common.CompletionResult
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.get
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import timber.log.Timber
/**
* General implementation of [UserWalletsListManager] that helps to switch between Runtime and Biometric
* implementations.
*
* @property runtimeUserWalletsListManager runtime user wallets list manager
* @property biometricUserWalletsListManager biometric user wallets list manager
* @property appPreferencesStore app preferences store
* @property dispatchers coroutine dispatcher provider
*
[REDACTED_AUTHOR]
*/
@OptIn(ExperimentalCoroutinesApi::class)
internal class GeneralUserWalletsListManager(
private val runtimeUserWalletsListManager: UserWalletsListManager,
private val biometricUserWalletsListManager: UserWalletsListManager,
private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider,
) : UserWalletsListManager.Lockable {
private val applicationScope = CoroutineScope(dispatchers.io)
private val implementation: MutableStateFlow<UserWalletsListManager?> = MutableStateFlow(value = null)
private val requireImplementation: UserWalletsListManager
get() = requireNotNull(implementation.value) {
"UserWalletsListManager is not initialized"
}
init {
subscribeOnCurrentManager()
}
override val isLockable: Boolean
get() = requireImplementation.isLockable
override val userWallets: Flow<List<UserWallet>>
get() = implementation
.transformLatest { impl ->
if (impl != null) {
emitAll(impl.userWallets)
}
}
// To avoid returning empty flow to subscriber while implementation and userWallets are null
// Flow is called first time when implementation is null and then when its assigned with implementation
// that may have not user wallets (null or empty).
// As a result subscription occurs on empty flow, than will not change if user wallets are available
.filter { requireImplementation.hasUserWallets }
override val savedWalletsCount: Flow<Int>
get() = implementation
.transformLatest { impl ->
if (impl != null) {
emitAll(impl.savedWalletsCount)
}
}
override val userWalletsSync: List<UserWallet>
get() = requireImplementation.userWalletsSync
override val selectedUserWallet: Flow<UserWallet>
get() = implementation
.transformLatest { impl ->
if (impl != null) {
emitAll(impl.selectedUserWallet)
}
}
// To avoid returning empty flow to subscriber while implementation and userWallets are null
// Flow is called first time when implementation is null and then when its assigned with implementation
// that may have not user wallets (null or empty).
// As a result subscription occurs on empty flow, than will not change if user wallets are available
.filter { requireImplementation.hasUserWallets }
override val selectedUserWalletSync: UserWallet?
get() = requireImplementation.selectedUserWalletSync
override val hasUserWallets: Boolean
get() = requireImplementation.hasUserWallets
override val walletsCount: Int
get() = requireImplementation.walletsCount
override val lockedState: Flow<Boolean>
get() = implementation.transformLatest { impl ->
if (impl == null) return@transformLatest
if (impl is UserWalletsListManager.Lockable) {
emitAll(impl.lockedState)
} else {
error("RuntimeUserWalletsListManager is not lockable")
}
}
override val isLocked: Boolean
get() {
val impl = requireImplementation
return if (impl is UserWalletsListManager.Lockable) {
impl.isLocked
} else {
error("RuntimeUserWalletsListManager is not lockable")
}
}
override suspend fun select(userWalletId: UserWalletId): CompletionResult<UserWallet> {
return requireImplementation.select(userWalletId)
}
override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit> {
return requireImplementation.save(userWallet, canOverride)
}
override suspend fun update(
userWalletId: UserWalletId,
update: suspend (UserWallet) -> UserWallet,
): CompletionResult<UserWallet> {
return requireImplementation.update(userWalletId, update)
}
override suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit> {
return requireImplementation.delete(userWalletIds)
}
override suspend fun clear(): CompletionResult<Unit> {
return requireImplementation.clear()
}
override suspend fun get(userWalletId: UserWalletId): CompletionResult<UserWallet> {
return requireImplementation.get(userWalletId)
}
override suspend fun unlock(type: UserWalletsListManager.Lockable.UnlockType): CompletionResult<UserWallet> {
val implementation = requireImplementation
return if (implementation is UserWalletsListManager.Lockable) {
implementation.unlock(type)
} else {
error("RuntimeUserWalletsListManager is not lockable")
}
}
override fun lock() {
val implementation = requireImplementation
return if (implementation is UserWalletsListManager.Lockable) {
implementation.lock()
} else {
error("RuntimeUserWalletsListManager is not lockable")
}
}
private fun subscribeOnCurrentManager() {
appPreferencesStore.get(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false)
.distinctUntilChanged()
.onEach { shouldSaveUserWallets ->
val possibleManager = if (shouldSaveUserWallets) {
biometricUserWalletsListManager
} else {
runtimeUserWalletsListManager
}
if (possibleManager == implementation.value) {
Timber.e("Switch to the same manager ${possibleManager::class.simpleName.orEmpty()}")
}
Timber.i("Switch to ${possibleManager::class.simpleName.orEmpty()}")
val previousManager = implementation.value
implementation.value = copySelectedUserWallet(
sourceManager = previousManager,
destinationManager = possibleManager,
)
previousManager?.clear()
}
.flowOn(dispatchers.io)
.launchIn(applicationScope)
}
private suspend fun copySelectedUserWallet(
sourceManager: UserWalletsListManager?,
destinationManager: UserWalletsListManager,
): UserWalletsListManager {
sourceManager?.selectedUserWalletSync?.let { selectedWallet ->
destinationManager.save(selectedWallet, canOverride = true)
}
return destinationManager
}
}

View file

@ -1,115 +0,0 @@
package com.tangem.tap.domain.userWalletList.implementation
import com.tangem.common.CompletionResult
import com.tangem.common.catching
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.legacy.UserWalletsListError
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
@OptIn(ExperimentalCoroutinesApi::class)
internal class RuntimeUserWalletsListManager : UserWalletsListManager {
private val state = MutableStateFlow(State())
override val isLockable: Boolean = false
override val userWallets: Flow<List<UserWallet>>
get() = state
.mapLatest { listOfNotNull(it.userWallet) }
.distinctUntilChanged()
override val selectedUserWallet: Flow<UserWallet>
get() = state
.mapLatest { it.userWallet }
.filterNotNull()
.distinctUntilChanged()
override val userWalletsSync: List<UserWallet>
get() = listOfNotNull(state.value.userWallet)
override val selectedUserWalletSync: UserWallet?
get() = state.value.userWallet
override val hasUserWallets: Boolean
get() = state.value.userWallet != null
/**
* only 1 wallet stored in runtime implementation
*/
override val walletsCount: Int
get() = if (hasUserWallets) 1 else 0
override val savedWalletsCount: Flow<Int>
get() = state
.mapLatest { walletsCount }
.distinctUntilChanged()
override suspend fun select(userWalletId: UserWalletId): CompletionResult<UserWallet> = catching {
state.value.userWallet
?.takeIf { it.walletId == userWalletId }
?: walletNotFound()
}
override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit> {
return if (canOverride) {
saveInternal(userWallet)
} else {
val isWalletSaved = state.value.userWallet?.walletId == userWallet.walletId
if (isWalletSaved) {
CompletionResult.Failure(UserWalletsListError.WalletAlreadySaved)
} else {
saveInternal(userWallet)
}
}
}
override suspend fun update(
userWalletId: UserWalletId,
update: suspend (UserWallet) -> UserWallet,
): CompletionResult<UserWallet> = catching {
val wallet = state.value.userWallet
?.takeIf { it.walletId == userWalletId }
?: walletNotFound()
requireNotNull(
state.updateAndGet { prevState ->
prevState.copy(
userWallet = update(wallet),
)
}.userWallet,
) { "User wallet is null after update" }
}
override suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit> = clear()
override suspend fun clear(): CompletionResult<Unit> = catching {
state.update { prevState ->
prevState.copy(
userWallet = null,
)
}
}
override suspend fun get(userWalletId: UserWalletId): CompletionResult<UserWallet> = catching {
state.value.userWallet ?: walletNotFound()
}
private fun saveInternal(userWallet: UserWallet): CompletionResult<Unit> = catching {
state.update { prevState ->
prevState.copy(
userWallet = userWallet,
)
}
}
private fun walletNotFound(): Nothing {
throw NoSuchElementException("User wallet not found")
}
private data class State(
val userWallet: UserWallet? = null,
)
}

View file

@ -1,39 +0,0 @@
package com.tangem.tap.domain.userWalletList.repository.implementation
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.core.analytics.models.Basic
import com.tangem.domain.wallets.legacy.UserWalletsListError
import com.tangem.utils.converter.Converter
object BiometricFailReasonConverter : Converter<TangemError, Basic.BiometryFailed.BiometricFailReason> {
override fun convert(value: TangemError): Basic.BiometryFailed.BiometricFailReason {
// 1. Try handle TangemSdkError cases first if error has not been mapped
when (value) {
is TangemSdkError.AuthenticationCanceled ->
return Basic.BiometryFailed.BiometricFailReason.AuthenticationCancelled
is TangemSdkError.AuthenticationAlreadyInProgress ->
return Basic.BiometryFailed.BiometricFailReason.AuthenticationAlreadyInProgress
}
// 2. For other errors, check if they are of type UserWalletsListError
if (value !is UserWalletsListError) {
return Basic.BiometryFailed.BiometricFailReason.Other(value.customMessage)
}
// 3. Map UserWalletsListError to BiometricFailReason
return when (value) {
UserWalletsListError.AllKeysInvalidated ->
Basic.BiometryFailed.BiometricFailReason.AllKeysInvalidated
UserWalletsListError.BiometricsAuthenticationDisabled ->
Basic.BiometryFailed.BiometricFailReason.BiometricsAuthenticationDisabled
is UserWalletsListError.BiometricsAuthenticationLockout ->
if (value.isPermanent) {
Basic.BiometryFailed.BiometricFailReason.AuthenticationLockoutPermanent
} else {
Basic.BiometryFailed.BiometricFailReason.AuthenticationLockout
}
else -> Basic.BiometryFailed.BiometricFailReason.Other(value.customMessage)
}
}
}

View file

@ -1,207 +0,0 @@
package com.tangem.tap.domain.userWalletList.repository.implementation
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.tangem.common.*
import com.tangem.common.authentication.storage.AuthenticatedStorage
import com.tangem.common.core.TangemSdkError
import com.tangem.common.services.secure.SecureStorage
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.legacy.UserWalletsListError
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
internal class BiometricUserWalletsKeysRepository(
moshi: Moshi,
private val authenticatedStorage: AuthenticatedStorage,
private val secureStorage: SecureStorage,
private val analyticsEventHandler: AnalyticsEventHandler,
) : UserWalletsKeysRepository {
private val encryptionKeyAdapter: JsonAdapter<UserWalletEncryptionKey> = moshi.adapter(
UserWalletEncryptionKey::class.java,
)
private val userWalletsIdsListAdapter: JsonAdapter<List<UserWalletId>> = moshi.adapter(
Types.newParameterizedType(List::class.java, UserWalletId::class.java),
)
override suspend fun getAll(): CompletionResult<List<UserWalletEncryptionKey>> {
return withContext(Dispatchers.IO) {
getAllInternal()
.mapFailure { error ->
val mappedError = when (error) {
is TangemSdkError.AuthenticationLockout ->
UserWalletsListError.BiometricsAuthenticationLockout(isPermanent = false)
is TangemSdkError.AuthenticationPermanentLockout ->
UserWalletsListError.BiometricsAuthenticationLockout(isPermanent = true)
is TangemSdkError.KeystoreInvalidated ->
UserWalletsListError.AllKeysInvalidated
is TangemSdkError.AuthenticationUnavailable ->
UserWalletsListError.BiometricsAuthenticationDisabled
else -> error
}
analyticsEventHandler.send(
Basic.BiometryFailed(
source = AnalyticsParam.ScreensSources.SignIn,
reason = BiometricFailReasonConverter.convert(mappedError),
),
)
mappedError
}
}
}
override suspend fun save(encryptionKey: UserWalletEncryptionKey): CompletionResult<Unit> {
return withContext(Dispatchers.IO) {
storeEncryptionKey(encryptionKey)
}
}
override suspend fun delete(userWalletsIds: List<UserWalletId>) {
return withContext(Dispatchers.IO) {
userWalletsIds.forEach { userWalletId ->
deleteEncryptionKey(userWalletId)
}
deleteUserWalletsIds(userWalletsIds)
}
}
override suspend fun clear() {
return withContext(Dispatchers.IO) {
getUserWalletsIds()
.forEach { userWalletId ->
deleteEncryptionKey(userWalletId)
}
clearUserWalletsIds()
}
}
override fun hasSavedEncryptionKeys(): Boolean {
return runBlocking {
getUserWalletsIds().isNotEmpty()
}
}
private suspend fun getAllInternal(): CompletionResult<List<UserWalletEncryptionKey>> {
return catching {
val userWalletIds = getUserWalletsIds()
getEncryptionKeys(userWalletIds)
}
.doOnFailure { error ->
when (error) {
is TangemSdkError.KeystoreInvalidated -> {
getUserWalletsIds().forEach { userWalletId ->
deleteEncryptionKey(userWalletId)
}
}
else -> Unit
}
}
}
private suspend fun getEncryptionKeys(userWalletsIds: List<UserWalletId>): List<UserWalletEncryptionKey> {
val keys = userWalletsIds.map { userWalletId ->
StorageKey.UserWalletEncryptionKey(userWalletId).name
}
return authenticatedStorage.get(keys).mapNotNull { (_, encodedData) ->
encodedData.decodeToKey()
}
}
private suspend fun storeEncryptionKey(encryptionKey: UserWalletEncryptionKey): CompletionResult<Unit> {
return catching {
authenticatedStorage.store(
keyAlias = StorageKey.UserWalletEncryptionKey(encryptionKey.walletId).name,
data = encryptionKey.encode(),
)
}
.map { storeUserWalletId(encryptionKey.walletId) }
}
private fun deleteEncryptionKey(userWalletId: UserWalletId) {
authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name)
}
private suspend fun getUserWalletsIds(): List<UserWalletId> {
return withContext(Dispatchers.IO) {
secureStorage.get(StorageKey.UserWalletIds.name)
.decodeToUserWalletsIds()
}
}
private suspend fun storeUserWalletId(userWalletId: UserWalletId) {
val userWalletIds = (getUserWalletsIds() + userWalletId).distinct()
withContext(Dispatchers.IO) {
secureStorage.store(userWalletIds.encode(), StorageKey.UserWalletIds.name)
}
}
private suspend fun deleteUserWalletsIds(userWalletsIds: List<UserWalletId>) {
val remainingIds = getUserWalletsIds() - userWalletsIds.toSet()
withContext(Dispatchers.IO) {
secureStorage.store(remainingIds.encode(), StorageKey.UserWalletIds.name)
}
}
private suspend fun clearUserWalletsIds() {
withContext(Dispatchers.IO) {
secureStorage.delete(StorageKey.UserWalletIds.name)
}
}
private suspend fun UserWalletEncryptionKey.encode(): ByteArray {
return withContext(Dispatchers.Default) {
encryptionKeyAdapter.toJson(this@encode)
.encodeToByteArray(throwOnInvalidSequence = true)
}
}
private suspend fun ByteArray?.decodeToKey(): UserWalletEncryptionKey? {
return withContext(Dispatchers.Default) {
this@decodeToKey
?.decodeToString(throwOnInvalidSequence = true)
?.let(encryptionKeyAdapter::fromJson)
}
}
private suspend fun List<UserWalletId>.encode(): ByteArray {
return withContext(Dispatchers.Default) {
userWalletsIdsListAdapter.toJson(this@encode)
.encodeToByteArray(throwOnInvalidSequence = true)
}
}
private suspend fun ByteArray?.decodeToUserWalletsIds(): List<UserWalletId> {
return withContext(Dispatchers.Default) {
this@decodeToUserWalletsIds
?.decodeToString(throwOnInvalidSequence = true)
?.let(userWalletsIdsListAdapter::fromJson)
.orEmpty()
}
}
private sealed interface StorageKey {
val name: String
class UserWalletEncryptionKey(userWalletId: UserWalletId) : StorageKey {
override val name: String = "user_wallet_encryption_key_${userWalletId.stringValue}"
}
object UserWalletIds : StorageKey {
override val name: String = "user_wallets_ids_with_saved_keys"
}
}
}

View file

@ -104,8 +104,6 @@ internal fun List<UserWallet>.updateWith(
}
}
internal fun List<UserWallet>.lockAll(): List<UserWallet> = map(UserWallet::lock)
internal fun UserWallet.lock(): UserWallet = when (this) {
is UserWallet.Cold -> {
copy(