Updated on 2026-08-14

This commit is contained in:
Tangem 2024-05-17 18:53:42 +03:00
parent 9868dd2bc2
commit a3295ea181
12 changed files with 123 additions and 107 deletions

View file

@ -41,7 +41,6 @@ import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.features.managetokens.navigation.ManageTokensUi
@ -440,10 +439,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) {
val canSaveWallets = runCatching { userWalletsListManager.asLockable()?.isLockedSync }
.fold(onSuccess = { true }, onFailure = { false })
if (canSaveWallets && userWalletsListManager.hasUserWallets) {
if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) {
store.dispatch(
NavigationAction.NavigateTo(
screen = AppScreen.Welcome,

View file

@ -12,6 +12,7 @@ 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 kotlinx.coroutines.ExperimentalCoroutinesApi
@ -27,6 +28,8 @@ internal class BiometricUserWalletsListManager(
) : UserWalletsListManager.Lockable {
private val state = MutableStateFlow(State())
override val isLockable: Boolean = true
override val userWallets: Flow<List<UserWallet>>
get() = state
.mapLatest { it.userWallets }
@ -78,11 +81,13 @@ internal class BiometricUserWalletsListManager(
}
override fun lock() {
state.update { State() }
}
override fun isLockable(): Boolean {
return true
state.update { prevState ->
prevState.copy(
encryptionKeys = emptyList(),
userWallets = prevState.userWallets.lockAll(),
isLocked = true,
)
}
}
override suspend fun select(userWalletId: UserWalletId): CompletionResult<UserWallet> = catching {

View file

@ -33,31 +33,49 @@ internal class GeneralUserWalletsListManager(
) : UserWalletsListManager.Lockable {
private val applicationScope = CoroutineScope(dispatchers.io)
private val implementation = MutableStateFlow(runtimeUserWalletsListManager)
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.flatMapLatest { it.userWallets }
get() = implementation.transformLatest { impl ->
if (impl != null && impl.hasUserWallets) {
emitAll(impl.userWallets)
}
}
override val selectedUserWallet: Flow<UserWallet>
get() = implementation.flatMapLatest { it.selectedUserWallet }
get() = implementation.transformLatest { impl ->
if (impl != null && impl.hasUserWallets) {
emitAll(impl.selectedUserWallet)
}
}
override val selectedUserWalletSync: UserWallet?
get() = implementation.value.selectedUserWalletSync
get() = requireImplementation.selectedUserWalletSync
override val hasUserWallets: Boolean
get() = implementation.value.hasUserWallets
get() = requireImplementation.hasUserWallets
override val walletsCount: Int
get() = implementation.value.walletsCount
get() = requireImplementation.walletsCount
override val isLocked: Flow<Boolean>
get() = implementation.flatMapLatest {
if (it is UserWalletsListManager.Lockable) {
it.isLocked
get() = implementation.transformLatest { impl ->
if (impl == null) return@transformLatest
if (impl is UserWalletsListManager.Lockable) {
emitAll(impl.isLocked)
} else {
error("RuntimeUserWalletsListManager is not lockable")
}
@ -65,43 +83,45 @@ internal class GeneralUserWalletsListManager(
override val isLockedSync: Boolean
get() {
val implementation = implementation.value
return if (implementation is UserWalletsListManager.Lockable) {
implementation.isLockedSync
val impl = requireImplementation
return if (impl is UserWalletsListManager.Lockable) {
impl.isLockedSync
} else {
error("RuntimeUserWalletsListManager is not lockable")
}
}
override suspend fun select(userWalletId: UserWalletId): CompletionResult<UserWallet> {
return implementation.value.select(userWalletId)
return requireImplementation.select(userWalletId)
}
override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit> {
return implementation.value.save(userWallet, canOverride)
return requireImplementation.save(userWallet, canOverride)
}
override suspend fun update(
userWalletId: UserWalletId,
update: suspend (UserWallet) -> UserWallet,
): CompletionResult<UserWallet> {
return implementation.value.update(userWalletId, update)
return requireImplementation.update(userWalletId, update)
}
override suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit> {
return implementation.value.delete(userWalletIds)
return requireImplementation.delete(userWalletIds)
}
override suspend fun clear(): CompletionResult<Unit> {
return implementation.value.clear()
return requireImplementation.clear()
}
override suspend fun get(userWalletId: UserWalletId): CompletionResult<UserWallet> {
return implementation.value.get(userWalletId)
return requireImplementation.get(userWalletId)
}
override suspend fun unlock(type: UserWalletsListManager.Lockable.UnlockType): CompletionResult<UserWallet> {
val implementation = implementation.value
val implementation = requireImplementation
return if (implementation is UserWalletsListManager.Lockable) {
implementation.unlock(type)
} else {
@ -110,7 +130,8 @@ internal class GeneralUserWalletsListManager(
}
override fun lock() {
val implementation = implementation.value
val implementation = requireImplementation
return if (implementation is UserWalletsListManager.Lockable) {
implementation.lock()
} else {
@ -118,10 +139,6 @@ internal class GeneralUserWalletsListManager(
}
}
override fun isLockable(): Boolean {
return implementation.value.isLockable()
}
private fun subscribeOnCurrentManager() {
appPreferencesStore.get(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false)
.distinctUntilChanged()
@ -144,17 +161,17 @@ internal class GeneralUserWalletsListManager(
destinationManager = possibleManager,
)
previousManager.clear()
previousManager?.clear()
}
.flowOn(dispatchers.io)
.launchIn(applicationScope)
}
private suspend fun copySelectedUserWallet(
sourceManager: UserWalletsListManager,
sourceManager: UserWalletsListManager?,
destinationManager: UserWalletsListManager,
): UserWalletsListManager {
sourceManager.selectedUserWalletSync?.let { selectedWallet ->
sourceManager?.selectedUserWalletSync?.let { selectedWallet ->
destinationManager.save(selectedWallet, canOverride = true)
}

View file

@ -13,6 +13,8 @@ import kotlinx.coroutines.flow.*
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) }
@ -34,7 +36,7 @@ internal class RuntimeUserWalletsListManager : UserWalletsListManager {
* only 1 wallet stored in runtime implementation
*/
override val walletsCount: Int
get() = 1
get() = if (hasUserWallets) 1 else 0
override suspend fun select(userWalletId: UserWalletId): CompletionResult<UserWallet> = catching {
state.value.userWallet
@ -85,10 +87,6 @@ internal class RuntimeUserWalletsListManager : UserWalletsListManager {
state.value.userWallet ?: walletNotFound()
}
override fun isLockable(): Boolean {
return false
}
private fun saveInternal(userWallet: UserWallet): CompletionResult<Unit> = catching {
state.update { prevState ->
prevState.copy(

View file

@ -61,4 +61,14 @@ internal fun List<UserWallet>.updateWith(
?: wallet
}
}
}
}
internal fun List<UserWallet>.lockAll(): List<UserWallet> = map(UserWallet::lock)
internal fun UserWallet.lock(): UserWallet = copy(
scanResponse = scanResponse.copy(
card = scanResponse.card.copy(
wallets = emptyList(),
),
),
)

View file

@ -25,7 +25,7 @@
},
{
"name": "TOKEN_LIST_LCE_ENABLED",
"version": "5.10.0"
"version": "5.11.0"
},
{
"name": "CARDANO_TOKENS_SUPPORT_ENABLED",

View file

@ -7,6 +7,11 @@ import kotlinx.coroutines.flow.Flow
interface UserWalletsListManager {
/**
* Indicates that the [UserWalletsListManager] is [UserWalletsListManager.Lockable]
* */
val isLockable: Boolean
/** [Flow] with all saved [UserWallet]s updates */
val userWallets: Flow<List<UserWallet>>
@ -84,11 +89,6 @@ interface UserWalletsListManager {
*/
suspend fun get(userWalletId: UserWalletId): CompletionResult<UserWallet>
/**
* Indicates that the [UserWalletsListManager] supports [UserWalletsListManager.Lockable]
* */
fun isLockable(): Boolean
interface Lockable : UserWalletsListManager {
/**

View file

@ -49,7 +49,7 @@ suspend fun UserWalletsListManager.unlockIfLockable(type: UnlockType = UnlockTyp
* [UserWalletsListManager.Lockable] otherwise
* */
fun UserWalletsListManager.asLockable(): UserWalletsListManager.Lockable? {
if (this.isLockable()) {
if (this.isLockable) {
return this as? UserWalletsListManager.Lockable
}
return null

View file

@ -1,17 +1,14 @@
package com.tangem.domain.wallets.usecase
import arrow.core.Either
import arrow.core.left
import arrow.core.raise.either
import arrow.core.right
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.DeleteWalletError
import com.tangem.domain.wallets.models.UserWalletId
/**
* Use case for updating user wallet
* Use case for deleting user wallet
*
* @property userWalletsListManager user wallets list manager
*
@ -19,13 +16,21 @@ import com.tangem.domain.wallets.models.UserWalletId
*/
class DeleteWalletUseCase(private val userWalletsListManager: UserWalletsListManager) {
suspend operator fun invoke(userWalletId: UserWalletId): Either<DeleteWalletError, Unit> {
/**
* Deletes user wallet with provided ID.
*
* @param userWalletId ID of user wallet to be deleted.
*
* @return [Either] with [DeleteWalletError] or [Boolean] which indicates that there are still saved wallets.
* */
suspend operator fun invoke(userWalletId: UserWalletId): Either<DeleteWalletError, Boolean> {
return either {
userWalletsListManager.delete(userWalletIds = listOf(userWalletId))
.doOnSuccess { return Unit.right() }
.doOnFailure { return DeleteWalletError.UnableToDelete.left() }
.doOnFailure {
raise(DeleteWalletError.UnableToDelete)
}
return Unit.right()
userWalletsListManager.hasUserWallets
}
}
}

View file

@ -5,7 +5,6 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.navigation.AppScreen
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.settings.CanUseBiometryUseCase
import com.tangem.domain.settings.IsWalletsScrollPreviewEnabled
@ -32,10 +31,11 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@ -197,9 +197,9 @@ internal class WalletViewModel @Inject constructor(
is WalletsUpdateActionResolver.Action.UpdateWalletName -> {
stateHolder.update(transformer = RenameWalletTransformer(action.selectedWalletId, action.name))
}
is WalletsUpdateActionResolver.Action.NoAccessibleWallets -> closeScreen(screen = AppScreen.Welcome)
is WalletsUpdateActionResolver.Action.NoWallets -> closeScreen(screen = AppScreen.Home)
is WalletsUpdateActionResolver.Action.Unknown -> Unit
is WalletsUpdateActionResolver.Action.Unknown -> {
Timber.w("Unable to perfom action: $action")
}
}
}
@ -313,13 +313,6 @@ internal class WalletViewModel @Inject constructor(
)
}
private fun closeScreen(screen: AppScreen) {
if (!screenLifecycleProvider.isBackgroundState.value) {
stateHolder.clear()
router.popBackStack(screen = screen)
}
}
private fun scrollToWallet(index: Int, onConsume: () -> Unit = {}) {
stateHolder.update(
ScrollToWalletTransformer(

View file

@ -1,5 +1,6 @@
package com.tangem.feature.wallet.presentation.wallet.viewmodels
import arrow.core.getOrElse
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
@ -23,16 +24,14 @@ internal class WalletsUpdateActionResolver @Inject constructor(
) {
fun resolve(wallets: List<UserWallet>, currentState: WalletScreenState): Action {
val selectedWallet = wallets.getSelectedWallet()
val selectedWallet = getSelectedWalletSyncUseCase().getOrElse {
error("Unable to find selected wallet: $it")
}
val action = if (selectedWallet == null) {
createNoSelectedWalletAction(wallets)
val action = if (isFirstInitialization(currentState)) {
createInitializeWalletsAction(wallets, selectedWallet)
} else {
if (isFirstInitialization(currentState)) {
createInitializeWalletsAction(wallets, selectedWallet)
} else {
getUpdateContentAction(currentState, wallets, selectedWallet)
}
getUpdateContentAction(currentState, wallets, selectedWallet)
}
Timber.d("Resolved action: $action")
@ -40,22 +39,6 @@ internal class WalletsUpdateActionResolver @Inject constructor(
return action
}
private fun List<UserWallet>.getSelectedWallet(): UserWallet? {
return when {
isEmpty() -> null
size == 1 -> if (first().isLocked) null else first()
else -> getSelectedWalletSyncUseCase().fold(ifLeft = { null }, ifRight = { it })
}
}
private fun createNoSelectedWalletAction(wallets: List<UserWallet>): Action {
return when {
wallets.isEmpty() -> Action.NoWallets
wallets.all(UserWallet::isLocked) -> Action.NoAccessibleWallets
else -> Action.Unknown
}
}
private fun isFirstInitialization(state: WalletScreenState): Boolean {
return state.selectedWalletIndex == NOT_INITIALIZED_WALLET_INDEX
}
@ -289,10 +272,6 @@ internal class WalletsUpdateActionResolver @Inject constructor(
}
}
data object NoAccessibleWallets : Action()
data object NoWallets : Action()
data object Unknown : Action()
}
}

View file

@ -1,7 +1,11 @@
package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents
import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.card.DeleteSavedAccessCodesUseCase
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.ReduxNavController
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.DeleteWalletUseCase
@ -43,6 +47,7 @@ internal class WalletCardClickIntentsImplementor @Inject constructor(
private val deleteSavedAccessCodesUseCase: DeleteSavedAccessCodesUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val reduxStateHolder: ReduxStateHolder,
private val reduxNavController: ReduxNavController,
private val dispatchers: CoroutineDispatcherProvider,
) : BaseWalletClickIntents(), WalletCardClickIntents {
@ -81,18 +86,26 @@ internal class WalletCardClickIntentsImplementor @Inject constructor(
viewModelScope.launch(dispatchers.main) {
walletScreenContentLoader.cancel(userWalletId)
val deletedUserWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: return@launch
val walletToDelete = getUserWalletUseCase(userWalletId).getOrNull() ?: return@launch
val hasUserWallets = deleteWalletUseCase(userWalletId).getOrElse {
Timber.e("Unable to delete user wallet: $it")
return@launch
}
deleteSavedAccessCodesUseCase(cardId = deletedUserWallet.cardId)
.onLeft { Timber.e(it.toString()) }
deleteSavedAccessCodesUseCase(cardId = walletToDelete.cardId).onLeft {
Timber.e("Unable to delete user wallet access code: $it")
}
deleteWalletUseCase(userWalletId)
.onRight {
getSelectedWalletSyncUseCase().getOrNull()?.let {
reduxStateHolder.onUserWalletSelected(it)
}
if (hasUserWallets) {
val selectedWallet = getSelectedWalletSyncUseCase().getOrElse {
error("Unable to find selected wallet: $it")
}
.onLeft { Timber.e(it.toString()) }
reduxStateHolder.onUserWalletSelected(selectedWallet)
} else {
stateHolder.clear()
reduxNavController.navigate(NavigationAction.PopBackTo(AppScreen.Home))
}
}
}
}