Updated on 2026-08-14

This commit is contained in:
Tangem 2023-03-02 11:24:02 +03:00
commit a09e571828
18 changed files with 240 additions and 62 deletions

View file

@ -6,6 +6,7 @@ import androidx.lifecycle.lifecycleScope
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.userWalletList.asLockable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@ -95,8 +96,9 @@ internal class LockUserWalletsTimer(
val startTime = System.currentTimeMillis()
delay(duration)
if (isActive) {
val userWalletsListManager = userWalletsListManagerSafe ?: return@launch
if (userWalletsListManager.hasSavedUserWallets) {
val userWalletsListManager = userWalletsListManagerSafe?.asLockable()
?: return@launch
if (userWalletsListManager.hasUserWallets) {
val currentTime = System.currentTimeMillis()
Timber.d(
"""

View file

@ -122,7 +122,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
intentHandler.handleIntent(intent, userWalletsListManager.hasSavedUserWallets)
intentHandler.handleIntent(intent, userWalletsListManager.hasUserWallets)
}
override fun onStart() {
@ -204,7 +204,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
private fun navigateToInitialScreen(intent: Intent?) {
if (userWalletsListManager.hasSavedUserWallets) {
if (userWalletsListManager.hasUserWallets) {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Welcome))
store.dispatchOnMain(WelcomeAction.HandleIntentIfNeeded(intent))
} else {

View file

@ -22,47 +22,19 @@ interface UserWalletsListManager {
val selectedUserWalletSync: UserWallet?
/**
* Indicates that all [UserWallet]s is unlocked
*
* @see [isLockedSync]
* @see [UserWallet.isLocked]
* Indicates that the [UserWalletsListManager] contains at least one saved [UserWallet]
* */
val isLocked: Flow<Boolean>
/**
* Indicates that all [UserWallet]s is unlocked
*
* Sync version
*
* @see [isLocked]
* @see [UserWallet.isLocked]
* */
val isLockedSync: Boolean
val hasSavedUserWallets: Boolean
/**
* Receive saved [UserWallet]s, populate [userWallets] flow with it and set [isLocked] as false.
* Require biometric user authorization
*
* @return [CompletionResult] of operation, with selected [UserWallet]
* or null if there is no selected [UserWallet]
* */
suspend fun unlockWithBiometry(): CompletionResult<UserWallet?>
/**
* Remove [UserWallet]s from [userWallets] and set [isLocked] as true
* */
fun lock()
val hasUserWallets: Boolean
/**
* Set [UserWallet] with provided [UserWalletId] as selected
*
* @param userWalletId [UserWalletId] of [UserWallet] which must be selected
*
* @return [CompletionResult] of operation with selected [UserWallet]
* @return [CompletionResult.Success] with selected [UserWallet]
* or [CompletionResult.Failure] with [NoSuchElementException] if [UserWallet] with [userWalletId] not found
* */
suspend fun selectWallet(userWalletId: UserWalletId): CompletionResult<UserWallet>
suspend fun select(userWalletId: UserWalletId): CompletionResult<UserWallet>
/**
* Save provided user wallet and set it as selected
@ -80,13 +52,16 @@ interface UserWalletsListManager {
* 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]
* @return [CompletionResult.Success] with updated [UserWallet]
* or [CompletionResult.Failure] with [NoSuchElementException] if [UserWallet] with [userWalletId] not found
* */
suspend fun update(userWalletId: UserWalletId, update: (UserWallet) -> UserWallet): CompletionResult<UserWallet>
/**
* Delete saved [UserWallet]s with provided [UserWalletId]s
*
* Sets [isLocked] as true if [userWallets] is empty or if all [userWallets] are locked
*
* @param userWalletIds [UserWalletId]s of [UserWallet]s which must be deleted
*
* @return [CompletionResult] of operation
@ -102,12 +77,45 @@ interface UserWalletsListManager {
/**
* Get [UserWallet] with provided [UserWalletId]
* May terminate with [NoSuchElementException] if [UserWallet] is not found
*
* @return [CompletionResult] of operation with found [UserWallet]
* @return [CompletionResult.Success] with found [UserWallet]
* or [CompletionResult.Failure] with [NoSuchElementException] if [UserWallet] with [userWalletId] not found
* */
suspend fun get(userWalletId: UserWalletId): CompletionResult<UserWallet>
interface Lockable : UserWalletsListManager {
/**
* Indicates that all [UserWallet]s is locked
*
* @see [isLockedSync]
* @see [UserWallet.isLocked]
* */
val isLocked: Flow<Boolean>
/**
* Indicates that all [UserWallet]s is locked
*
* Sync version
*
* @see [isLocked]
* @see [UserWallet.isLocked]
* */
val isLockedSync: Boolean
/**
* Receive saved [UserWallet]s, populate [userWallets] flow with it and set [isLocked] as false.
*
* @return [CompletionResult] of operation, with selected [UserWallet]
* or null if there is no selected [UserWallet]
* */
suspend fun unlock(): CompletionResult<UserWallet?>
/**
* Remove [UserWallet]s from [userWallets] and set [isLocked] as true
* */
fun lock()
}
// For provider
companion object
}

View file

@ -0,0 +1,51 @@
package com.tangem.tap.domain.userWalletList
import com.tangem.common.CompletionResult
import com.tangem.tap.domain.model.UserWallet
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
/**
* Indicates that the [UserWalletsListManager] is locked
*
* @return If [UserWalletsListManager] not implements [UserWalletsListManager.Lockable] returns [Flow] which
* produces only one false value
* */
val UserWalletsListManager.isLocked: Flow<Boolean>
get() = asLockable()?.isLocked ?: flowOf(false)
/**
* Indicates that the [UserWalletsListManager] is locked
*
* @return If [UserWalletsListManager] not implements [UserWalletsListManager.Lockable] returns false
* */
val UserWalletsListManager.isLockedSync: Boolean
get() = asLockable()?.isLockedSync ?: false
/**
* Call [UserWalletsListManager.Lockable.unlock] if [UserWalletsListManager] implements [UserWalletsListManager.Lockable]
*
* @return If [UserWalletsListManager] not implements [UserWalletsListManager.Lockable]
* returns [CompletionResult.Success] with [UserWalletsListManager.selectedUserWalletSync]
* */
suspend fun UserWalletsListManager.unlockIfLockable(): CompletionResult<UserWallet?> {
return asLockable()?.unlock() ?: CompletionResult.Success(selectedUserWalletSync)
}
/**
* Call [UserWalletsListManager.Lockable.lock] if [UserWalletsListManager] implements [UserWalletsListManager.Lockable]
* or do nothing otherwise
* */
fun UserWalletsListManager.lockIfLockable() {
asLockable()?.lock()
}
/**
* Safe cast [UserWalletsListManager] to [UserWalletsListManager.Lockable]
*
* @return If [UserWalletsListManager] not implements [UserWalletsListManager.Lockable] then returns null or
* [UserWalletsListManager.Lockable] otherwise
* */
fun UserWalletsListManager.asLockable(): UserWalletsListManager.Lockable? {
return this as? UserWalletsListManager.Lockable
}

View file

@ -10,6 +10,7 @@ import com.tangem.tangem_sdk_new.storage.createEncryptedSharedPreferences
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager
import com.tangem.tap.domain.userWalletList.implementation.RuntimeUserWalletsListManager
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
@ -68,4 +69,10 @@ fun UserWalletsListManager.Companion.provideBiometricImplementation(
sensitiveInformationRepository = sensitiveInformationRepository,
selectedUserWalletRepository = selectedUserWalletRepository,
)
}
// TODO: Will be used in further MR's
@Suppress("unused")
fun UserWalletsListManager.Companion.provideRuntimeImplementation(): UserWalletsListManager {
return RuntimeUserWalletsListManager()
}

View file

@ -23,7 +23,7 @@ internal class BiometricUserWalletsListManager(
private val publicInformationRepository: UserWalletsPublicInformationRepository,
private val sensitiveInformationRepository: UserWalletsSensitiveInformationRepository,
private val selectedUserWalletRepository: SelectedUserWalletRepository,
) : UserWalletsListManager {
) : UserWalletsListManager.Lockable {
private val state = MutableStateFlow(State())
override val userWallets: Flow<List<UserWallet>>
@ -50,10 +50,10 @@ internal class BiometricUserWalletsListManager(
override val isLockedSync: Boolean
get() = state.value.isLocked
override val hasSavedUserWallets: Boolean
override val hasUserWallets: Boolean
get() = keysRepository.hasSavedEncryptionKeys()
override suspend fun unlockWithBiometry(): CompletionResult<UserWallet?> {
override suspend fun unlock(): CompletionResult<UserWallet?> {
return unlockWithBiometryInternal()
.map { selectedUserWalletSync }
}
@ -62,20 +62,20 @@ internal class BiometricUserWalletsListManager(
state.update { State() }
}
override suspend fun selectWallet(userWalletId: UserWalletId): CompletionResult<UserWallet> = catching {
override suspend fun select(userWalletId: UserWalletId): CompletionResult<UserWallet> = catching {
if (state.value.selectedUserWalletId == userWalletId) {
return@catching findSelectedUserWallet()!!
}
selectedUserWalletRepository.set(userWalletId)
state.update { prevState ->
val newState = state.updateAndGet { prevState ->
prevState.copy(
selectedUserWalletId = userWalletId,
)
}
findSelectedUserWallet()!!
newState.userWallets.first { it.walletId == userWalletId }
}
override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit> {
@ -289,7 +289,7 @@ internal class BiometricUserWalletsListManager(
}
private fun findSelectedUserWallet(userWallets: List<UserWallet> = state.value.userWallets): UserWallet? {
return userWallets.find {
return userWallets.firstOrNull {
it.walletId == state.value.selectedUserWalletId
}
}

View file

@ -0,0 +1,103 @@
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.UserWalletListError
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.updateAndGet
@OptIn(ExperimentalCoroutinesApi::class)
internal class RuntimeUserWalletsListManager : UserWalletsListManager {
private val state = MutableStateFlow(State())
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 selectedUserWalletSync: UserWallet?
get() = state.value.userWallet
override val hasUserWallets: Boolean
get() = state.value.userWallet != null
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(UserWalletListError.WalletAlreadySaved)
} else {
saveInternal(userWallet)
}
}
}
override suspend fun update(
userWalletId: UserWalletId,
update: (UserWallet) -> UserWallet,
): CompletionResult<UserWallet> = catching {
val wallet = state.value.userWallet
?.takeIf { it.walletId == userWalletId }
?: walletNotFound()
state.updateAndGet { prevState ->
prevState.copy(
userWallet = update(wallet),
)
}.userWallet!!
}
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

@ -20,6 +20,8 @@ import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.model.builders.UserWalletBuilder
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
import com.tangem.tap.domain.userWalletList.isLockedSync
import com.tangem.tap.domain.userWalletList.lockIfLockable
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
@ -132,7 +134,7 @@ class DetailsMiddleware {
store.onUserWalletSelected(selectedUserWallet)
}
} else {
userWalletsListManager.lock()
userWalletsListManager.lockIfLockable()
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
}
}

View file

@ -26,6 +26,7 @@ import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.domain.userWalletList.isLockedSync
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.OnboardingHelper
@ -335,7 +336,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
}
private fun getPopBackScreen(): AppScreen {
return if (userWalletsListManager.hasSavedUserWallets) {
return if (userWalletsListManager.hasUserWallets) {
if (userWalletsListManager.isLockedSync) {
AppScreen.Welcome
} else {

View file

@ -90,7 +90,7 @@ internal class SaveWalletMiddleware {
.backupCardsIds(state.backupInfo?.backupCardsIds)
.build() ?: return@launch
val isFirstSavedWallet = !userWalletsListManager.hasSavedUserWallets
val isFirstSavedWallet = !userWalletsListManager.hasUserWallets
saveAccessCodeIfNeeded(state.backupInfo?.accessCode, userWallet.cardsInWallet)
.flatMap { userWalletsListManager.save(userWallet, canOverride = true) }

View file

@ -341,7 +341,7 @@ class TokensMiddleware {
private suspend fun removeCurrenciesIfNeeded(currencies: List<Currency>) {
when {
currencies.isEmpty() -> Unit
userWalletsListManager.hasSavedUserWallets -> {
userWalletsListManager.hasUserWallets -> {
walletCurrenciesManager.removeCurrencies(
userWallet = userWalletsListManager.selectedUserWalletSync!!,
currenciesToRemove = currencies,

View file

@ -103,7 +103,7 @@ data class WalletState(
primaryWalletData?.currencyData?.status != BalanceStatus.UnknownBlockchain
val hasSavedWallets: Boolean
get() = userWalletsListManager.hasSavedUserWallets
get() = userWalletsListManager.hasUserWallets
fun getWalletManager(currency: Currency?): WalletManager? {
if (currency?.blockchain == null) return null

View file

@ -377,7 +377,7 @@ class WalletMiddleware {
private fun changeWallet() {
when {
userWalletsListManager.hasSavedUserWallets -> {
userWalletsListManager.hasUserWallets -> {
Analytics.send(MainScreen.ButtonMyWallets())
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.WalletSelector))
}

View file

@ -114,7 +114,7 @@ class MultiWalletReducer {
val currency = Currency.fromBlockchainNetwork(action.blockchain, action.token)
val walletManager = state.getWalletManager(currency)
if (walletManager == null) {
val screen = if (userWalletsListManager.hasSavedUserWallets) {
val screen = if (userWalletsListManager.hasUserWallets) {
AppScreen.Welcome
} else {
AppScreen.Home

View file

@ -32,6 +32,7 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.statePrinter.printScanResponseState
import com.tangem.tap.domain.statePrinter.printWalletState
import com.tangem.tap.domain.userWalletList.lockIfLockable
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.wallet.redux.ErrorType
import com.tangem.tap.features.wallet.redux.ProgressState
@ -81,13 +82,13 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
val popBackTo = if (userWalletsListManager.hasSavedUserWallets) {
userWalletsListManager.lock()
userWalletsListManager.lockIfLockable()
val screen = if (userWalletsListManager.hasUserWallets) {
AppScreen.Welcome
} else {
AppScreen.Home
}
store.dispatch(NavigationAction.PopBackTo(popBackTo))
store.dispatch(NavigationAction.PopBackTo(screen))
}
},
)

View file

@ -23,6 +23,7 @@ import com.tangem.tap.domain.model.WalletStoreModel
import com.tangem.tap.domain.model.builders.UserWalletBuilder
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
import com.tangem.tap.domain.scanCard.ScanCardProcessor
import com.tangem.tap.domain.userWalletList.unlockIfLockable
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
@ -63,7 +64,7 @@ internal class WalletSelectorMiddleware {
updateBalances(action.walletsStores, state)
}
is WalletSelectorAction.UnlockWithBiometry -> {
unlockWalletsWithBiometry()
unlockWallets()
}
is WalletSelectorAction.AddWallet -> {
addWallet()
@ -115,11 +116,11 @@ internal class WalletSelectorMiddleware {
}
}
private fun unlockWalletsWithBiometry() {
private fun unlockWallets() {
Analytics.send(MyWallets.Button.UnlockWithBiometrics())
scope.launch {
userWalletsListManager.unlockWithBiometry()
userWalletsListManager.unlockIfLockable()
.doOnFailure { error ->
Timber.e(error, "Unable to unlock all user wallets")
store.dispatchOnMain(WalletSelectorAction.UnlockWithBiometry.Error(error))
@ -195,7 +196,7 @@ internal class WalletSelectorMiddleware {
if (userWallet.isLocked) {
unlockUserWalletWithScannedCard(userWallet)
} else {
userWalletsListManager.selectWallet(userWalletId)
userWalletsListManager.select(userWalletId)
}
}
.doOnFailure { error ->

View file

@ -8,6 +8,7 @@ import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.common.analytics.events.MyWallets
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.domain.userWalletList.isLocked
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.walletSelector.redux.WalletSelectorAction
import com.tangem.tap.features.walletSelector.redux.WalletSelectorState

View file

@ -17,6 +17,7 @@ import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.model.builders.UserWalletBuilder
import com.tangem.tap.domain.scanCard.ScanCardProcessor
import com.tangem.tap.domain.userWalletList.unlockIfLockable
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
import com.tangem.tap.intentHandler
import com.tangem.tap.preferencesStorage
@ -66,7 +67,7 @@ internal class WelcomeMiddleware {
private fun proceedWithBiometrics(state: WelcomeState) {
scope.launch {
userWalletsListManager.unlockWithBiometry()
userWalletsListManager.unlockIfLockable()
.doOnFailure { error ->
store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics.Error(error))
}