Updated on 2026-08-14
This commit is contained in:
parent
4fbaa219b1
commit
c48e7204eb
13 changed files with 104 additions and 130 deletions
|
|
@ -19,7 +19,7 @@ interface UserWalletsListManager {
|
|||
suspend fun selectWallet(userWalletId: UserWalletId): CompletionResult<UserWallet>
|
||||
|
||||
/**
|
||||
* Save user wallet
|
||||
* Save provided user wallet and set it as selected
|
||||
* @param userWallet [UserWallet] to save
|
||||
* @param canOverride If false, then terminate with [UserWalletListError.WalletAlreadySaved] when user tries to save an
|
||||
* already saved card
|
||||
|
|
@ -27,6 +27,17 @@ interface UserWalletsListManager {
|
|||
* */
|
||||
suspend fun save(userWallet: UserWallet, canOverride: Boolean = false): CompletionResult<Unit>
|
||||
|
||||
/**
|
||||
* Same as [save] but not change selected user wallet ID
|
||||
* and not terminate with [UserWalletListError.WalletAlreadySaved] if [UserWallet] already saved
|
||||
*
|
||||
* Can terminate with [NoSuchElementException] if unable to find [UserWallet] with provided [UserWalletId]
|
||||
* @param userWalletId update [UserWallet] with that [UserWalletId]
|
||||
* @param update lambda that receives stored [UserWallet] and returns updated [UserWallet]
|
||||
* @return [CompletionResult] of operation with updated [UserWallet]
|
||||
* */
|
||||
suspend fun update(userWalletId: UserWalletId, update: (UserWallet) -> UserWallet): CompletionResult<UserWallet>
|
||||
|
||||
suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit>
|
||||
suspend fun clear(): CompletionResult<Unit>
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import com.tangem.tap.domain.userWalletList.repository.SelectedUserWalletReposit
|
|||
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.toUserWallets
|
||||
import com.tangem.tap.domain.userWalletList.utils.updateWith
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
|
@ -84,7 +85,7 @@ internal class BiometricUserWalletsListManager(
|
|||
|
||||
override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit> {
|
||||
return if (canOverride) {
|
||||
saveInternal(userWallet)
|
||||
saveInternal(userWallet, changeSelectedUserWallet = true)
|
||||
} else {
|
||||
val isWalletSaved = state.value.userWallets
|
||||
.any {
|
||||
|
|
@ -94,11 +95,25 @@ internal class BiometricUserWalletsListManager(
|
|||
if (isWalletSaved) {
|
||||
CompletionResult.Failure(UserWalletListError.WalletAlreadySaved)
|
||||
} else {
|
||||
saveInternal(userWallet)
|
||||
saveInternal(userWallet, changeSelectedUserWallet = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun update(
|
||||
userWalletId: UserWalletId,
|
||||
update: (UserWallet) -> UserWallet,
|
||||
): CompletionResult<UserWallet> {
|
||||
return get(userWalletId)
|
||||
.map { storedUserWallet ->
|
||||
update(storedUserWallet)
|
||||
}
|
||||
.flatMap { updatedUserWallet ->
|
||||
saveInternal(updatedUserWallet, changeSelectedUserWallet = false)
|
||||
.map { updatedUserWallet }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit> {
|
||||
if (userWalletIds.isEmpty()) {
|
||||
return CompletionResult.Success(Unit)
|
||||
|
|
@ -111,9 +126,12 @@ internal class BiometricUserWalletsListManager(
|
|||
.flatMap { keysRepository.delete(userWalletIds) }
|
||||
.map {
|
||||
state.update { prevState ->
|
||||
val newUserWallets = prevState.userWallets.filter { it.walletId !in userWalletIds }
|
||||
|
||||
prevState.copy(
|
||||
encryptionKeys = prevState.encryptionKeys.filter { it.walletId !in userWalletIds },
|
||||
userWallets = prevState.userWallets.filter { it.walletId !in userWalletIds },
|
||||
userWallets = newUserWallets,
|
||||
isLocked = newUserWallets.any { it.isLocked },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -135,27 +153,23 @@ internal class BiometricUserWalletsListManager(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun saveInternal(userWallet: UserWallet): CompletionResult<Unit> {
|
||||
val newEncryptionKeys = state.value.encryptionKeys
|
||||
.plus(UserWalletEncryptionKey(userWallet))
|
||||
.distinctBy { it.walletId }
|
||||
|
||||
return keysRepository.store(newEncryptionKeys)
|
||||
.doOnSuccess {
|
||||
state.update { prevState ->
|
||||
prevState.copy(
|
||||
encryptionKeys = newEncryptionKeys,
|
||||
selectedUserWalletId = userWallet.walletId,
|
||||
)
|
||||
}
|
||||
}
|
||||
private suspend fun saveInternal(
|
||||
userWallet: UserWallet,
|
||||
changeSelectedUserWallet: Boolean,
|
||||
): CompletionResult<Unit> {
|
||||
return saveEncryptionKeyIfNotNull(userWallet)
|
||||
.flatMap { sensitiveInformationRepository.save(userWallet, encryptionKey = it) }
|
||||
.flatMap { publicInformationRepository.save(userWallet) }
|
||||
.flatMap { sensitiveInformationRepository.save(userWallet) }
|
||||
.map { selectedUserWalletRepository.set(userWallet.walletId) }
|
||||
.flatMap { loadModels() }
|
||||
.doOnSuccess {
|
||||
state.update { prevState ->
|
||||
prevState.copy(
|
||||
selectedUserWalletId = if (changeSelectedUserWallet) {
|
||||
userWallet.walletId
|
||||
} else {
|
||||
prevState.selectedUserWalletId
|
||||
},
|
||||
isLocked = prevState.userWallets.any { it.isLocked },
|
||||
)
|
||||
}
|
||||
|
|
@ -181,6 +195,27 @@ internal class BiometricUserWalletsListManager(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun saveEncryptionKeyIfNotNull(userWallet: UserWallet): CompletionResult<ByteArray?> {
|
||||
val encryptionKey = userWallet.scanResponse.card.encryptionKey
|
||||
?.let { UserWalletEncryptionKey(userWallet.walletId, it) }
|
||||
|
||||
return if (encryptionKey != null) {
|
||||
keysRepository.save(encryptionKey)
|
||||
.doOnSuccess {
|
||||
state.update { prevState ->
|
||||
prevState.copy(
|
||||
encryptionKeys = prevState.encryptionKeys
|
||||
.plus(encryptionKey)
|
||||
.distinctBy { it.walletId },
|
||||
)
|
||||
}
|
||||
}
|
||||
.map { encryptionKey.encryptionKey }
|
||||
} else {
|
||||
CompletionResult.Success(data = null)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadModels(): CompletionResult<Unit> {
|
||||
return getSavedUserWallets()
|
||||
.map { userWallets ->
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
package com.tangem.tap.domain.userWalletList.implementation
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.catching
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
class DummyUserWalletsListManager : UserWalletsListManager {
|
||||
override val userWallets: Flow<List<UserWallet>>
|
||||
get() = flowOf(emptyList())
|
||||
override val selectedUserWallet: Flow<UserWallet>
|
||||
get() = flowOf()
|
||||
override val selectedUserWalletSync: UserWallet?
|
||||
get() = null
|
||||
override val isLocked: Flow<Boolean>
|
||||
get() = flowOf(true)
|
||||
override val isLockedSync: Boolean
|
||||
get() = true
|
||||
override val hasSavedUserWallets: Boolean
|
||||
get() = false
|
||||
|
||||
override suspend fun unlockWithBiometry(): CompletionResult<UserWallet?> {
|
||||
return CompletionResult.Success(null)
|
||||
}
|
||||
|
||||
override fun lock() {
|
||||
/* no-op */
|
||||
}
|
||||
|
||||
override suspend fun selectWallet(userWalletId: UserWalletId): CompletionResult<UserWallet> {
|
||||
return catching {
|
||||
error("Not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit> {
|
||||
return CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit> {
|
||||
return CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun clear(): CompletionResult<Unit> {
|
||||
return CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun get(userWalletId: UserWalletId): CompletionResult<UserWallet> {
|
||||
return catching {
|
||||
error("Not implemented")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,19 +2,12 @@ package com.tangem.tap.domain.userWalletList.model
|
|||
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.userWalletList.utils.encryptionKey
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class UserWalletEncryptionKey(
|
||||
val walletId: UserWalletId,
|
||||
val encryptionKey: ByteArray,
|
||||
) {
|
||||
constructor(userWallet: UserWallet) : this(
|
||||
walletId = userWallet.walletId,
|
||||
encryptionKey = userWallet.scanResponse.card.encryptionKey,
|
||||
)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is UserWalletEncryptionKey) return false
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ internal interface UserWalletsKeysRepository {
|
|||
suspend fun getAll(): CompletionResult<List<UserWalletEncryptionKey>>
|
||||
|
||||
/**
|
||||
* Store the encryption keys for user wallets. Biometric authentication not required
|
||||
* @param encryptionKeys List of encryption keys for user wallets
|
||||
* Save the encryption key for user wallet. Biometric authentication not required
|
||||
* @param encryptionKey [UserWalletEncryptionKey] to save
|
||||
* @return [CompletionResult] of operation
|
||||
* */
|
||||
suspend fun store(encryptionKeys: List<UserWalletEncryptionKey>): CompletionResult<Unit>
|
||||
suspend fun save(encryptionKey: UserWalletEncryptionKey): CompletionResult<Unit>
|
||||
|
||||
/**
|
||||
* Delete encryption keys for user wallets. Biometric authentication not required
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
|
|||
import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation
|
||||
|
||||
internal interface UserWalletsSensitiveInformationRepository {
|
||||
suspend fun save(userWallet: UserWallet): CompletionResult<Unit>
|
||||
suspend fun save(userWallet: UserWallet, encryptionKey: ByteArray?): CompletionResult<Unit>
|
||||
suspend fun getAll(
|
||||
encryptionKeys: List<UserWalletEncryptionKey>,
|
||||
): CompletionResult<Map<UserWalletId, UserWalletSensitiveInformation>>
|
||||
|
|
|
|||
|
|
@ -45,10 +45,9 @@ internal class BiometricUserWalletsKeysRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun store(encryptionKeys: List<UserWalletEncryptionKey>): CompletionResult<Unit> {
|
||||
override suspend fun save(encryptionKey: UserWalletEncryptionKey): CompletionResult<Unit> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
encryptionKeys.map { storeEncryptionKey(it) }
|
||||
.fold()
|
||||
storeEncryptionKey(encryptionKey)
|
||||
.mapFailure { error ->
|
||||
UserWalletListError.SaveEncryptionKeysError(error.cause ?: error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,11 +38,12 @@ internal class DefaultUserWalletsSensitiveInformationRepository(
|
|||
Cipher.getInstance("$algorithm/$blockMode/$encryptionPadding")
|
||||
}
|
||||
|
||||
override suspend fun save(userWallet: UserWallet): CompletionResult<Unit> {
|
||||
override suspend fun save(userWallet: UserWallet, encryptionKey: ByteArray?): CompletionResult<Unit> {
|
||||
if (encryptionKey == null) return CompletionResult.Success(Unit) // Encryption key is null, do nothing
|
||||
return catching {
|
||||
val encryptedSensitiveInformation = userWallet.sensitiveInformation
|
||||
.encode()
|
||||
.encryptAndStoreIv(userWallet.walletId.stringValue, userWallet.scanResponse.card.encryptionKey)
|
||||
.encryptAndStoreIv(userWallet.walletId.stringValue, encryptionKey)
|
||||
|
||||
getAllEncrypted().toMutableMap()
|
||||
.apply { set(userWallet.walletId.stringValue, encryptedSensitiveInformation) }
|
||||
|
|
|
|||
|
|
@ -4,15 +4,8 @@ import com.tangem.common.extensions.calculateSha256
|
|||
import com.tangem.domain.common.CardDTO
|
||||
import com.tangem.domain.common.extensions.calculateHmacSha256
|
||||
|
||||
internal val CardDTO.encryptionKey: ByteArray
|
||||
@Throws(IllegalArgumentException::class)
|
||||
get() {
|
||||
val walletPublicKey = requireNotNull(findPublicKey(wallets)) {
|
||||
"Wallet public key must not be null"
|
||||
}
|
||||
|
||||
return calculateEncryptionKey(walletPublicKey)
|
||||
}
|
||||
internal val CardDTO.encryptionKey: ByteArray?
|
||||
get() = findPublicKey(wallets)?.let { calculateEncryptionKey(it) }
|
||||
|
||||
private fun calculateEncryptionKey(publicKey: ByteArray): ByteArray {
|
||||
val message = MESSAGE_FOR_ENCRYPTION_KEY.toByteArray()
|
||||
|
|
|
|||
|
|
@ -281,13 +281,16 @@ class TokensMiddleware {
|
|||
) {
|
||||
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
|
||||
if (selectedUserWallet != null) {
|
||||
val updatedUserWallet = selectedUserWallet.copy(
|
||||
scanResponse = scanResponse,
|
||||
)
|
||||
|
||||
scope.launch {
|
||||
userWalletsListManager.save(updatedUserWallet, canOverride = true)
|
||||
.flatMap {
|
||||
userWalletsListManager.update(
|
||||
userWalletId = selectedUserWallet.walletId,
|
||||
update = { userWallet ->
|
||||
userWallet.copy(
|
||||
scanResponse = scanResponse,
|
||||
)
|
||||
},
|
||||
)
|
||||
.flatMap { updatedUserWallet ->
|
||||
walletCurrenciesManager.addCurrencies(
|
||||
userWallet = updatedUserWallet,
|
||||
currenciesToAdd = currencyList,
|
||||
|
|
|
|||
|
|
@ -179,21 +179,24 @@ class MultiWalletMiddleware {
|
|||
}
|
||||
|
||||
private fun scanAndUpdateCard(
|
||||
selectedWallet: UserWallet,
|
||||
selectedUserWallet: UserWallet,
|
||||
state: WalletState?,
|
||||
) = scope.launch {
|
||||
Analytics.send(MainScreen.CardWasScanned())
|
||||
ScanCardProcessor.scan(
|
||||
cardId = selectedWallet.cardId,
|
||||
cardId = selectedUserWallet.cardId,
|
||||
additionalBlockchainsToDerive = state?.missingDerivations?.map { it.blockchain },
|
||||
) { scanResponse ->
|
||||
val userWallet = selectedWallet.copy(
|
||||
scanResponse = scanResponse,
|
||||
userWalletsListManager.update(
|
||||
userWalletId = selectedUserWallet.walletId,
|
||||
update = { userWallet ->
|
||||
userWallet.copy(
|
||||
scanResponse = scanResponse,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
userWalletsListManager.save(userWallet, canOverride = true)
|
||||
.doOnSuccess {
|
||||
store.state.globalState.tapWalletManager.loadData(userWallet, refresh = true)
|
||||
.doOnSuccess { updatedUserWallet ->
|
||||
store.state.globalState.tapWalletManager.loadData(updatedUserWallet, refresh = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -239,9 +239,7 @@ internal class WalletSelectorMiddleware {
|
|||
Analytics.send(MyWallets.Button.EditWalletTapped)
|
||||
|
||||
scope.launch {
|
||||
userWalletsListManager.get(userWalletId)
|
||||
.map { it.copy(name = newName) }
|
||||
.flatMap { userWalletsListManager.save(it, canOverride = true) }
|
||||
userWalletsListManager.update(userWalletId) { it.copy(name = newName) }
|
||||
.doOnFailure { error ->
|
||||
store.dispatchOnMain(WalletSelectorAction.HandleError(error))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
|
|||
|
||||
fun walletClicked(userWalletId: UserWalletId) = with(state.value) {
|
||||
when {
|
||||
editingUserWalletsIds.isNotEmpty() && isWalletLocked(userWalletId, this) -> Unit
|
||||
editingUserWalletsIds.isNotEmpty() && !editingUserWalletsIds.contains(userWalletId) -> {
|
||||
editWallet(userWalletId)
|
||||
}
|
||||
|
|
@ -52,7 +51,7 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
|
|||
}
|
||||
|
||||
fun walletLongClicked(userWalletId: UserWalletId) = with(state.value) {
|
||||
if (!isWalletLocked(userWalletId, this) && editingUserWalletsIds.isEmpty()) {
|
||||
if (editingUserWalletsIds.isEmpty()) {
|
||||
editWallet(userWalletId)
|
||||
}
|
||||
}
|
||||
|
|
@ -67,15 +66,15 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
|
|||
|
||||
fun renameWallet() = with(state.value) {
|
||||
if (editingUserWalletsIds.isNotEmpty() && renameWalletDialog == null) {
|
||||
val editedWalletId = editingUserWalletsIds.first()
|
||||
val editedWallet = (multiCurrencyWallets + singleCurrencyWallets)
|
||||
.find { it.id == editedWalletId }
|
||||
val editedUserWalletId = editingUserWalletsIds.first()
|
||||
val editedUserWallet = (multiCurrencyWallets + singleCurrencyWallets)
|
||||
.find { it.id == editedUserWalletId }
|
||||
|
||||
if (editedWallet != null) {
|
||||
if (editedUserWallet != null) {
|
||||
val dialog = RenameWalletDialog(
|
||||
currentName = editedWallet.name,
|
||||
currentName = editedUserWallet.name,
|
||||
onApply = { newName ->
|
||||
store.dispatch(WalletSelectorAction.RenameWallet(editedWalletId, newName))
|
||||
store.dispatch(WalletSelectorAction.RenameWallet(editedUserWalletId, newName))
|
||||
stateInternal.update { prevState ->
|
||||
prevState.copy(
|
||||
renameWalletDialog = null,
|
||||
|
|
@ -137,11 +136,6 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
|
|||
}
|
||||
}
|
||||
|
||||
private fun isWalletLocked(userWalletId: UserWalletId, state: WalletSelectorScreenState): Boolean = with(state) {
|
||||
multiCurrencyWallets.find { it.id == userWalletId }?.isLocked
|
||||
?: singleCurrencyWallets.find { it.id == userWalletId }?.isLocked ?: isLocked
|
||||
}
|
||||
|
||||
private fun subscribeToStoreChanges() {
|
||||
store.subscribe(this) { appState ->
|
||||
appState.skip { old, new -> old.walletSelectorState == new.walletSelectorState }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue