Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-19 09:28:57 +03:00
parent 6638da98f1
commit 4574975639
11 changed files with 130 additions and 22 deletions

View file

@ -166,10 +166,34 @@ internal object WalletsDomainModule {
@Provides
@Singleton
fun providesUnlockWalletUseCase(userWalletsListManager: UserWalletsListManager): UnlockWalletsUseCase {
fun providesUnlockWalletsUseCase(userWalletsListManager: UserWalletsListManager): UnlockWalletsUseCase {
return UnlockWalletsUseCase(userWalletsListManager = userWalletsListManager)
}
@Provides
@Singleton
fun providesUnlockWalletUseCase(
nonBiometricUnlockWalletUseCase: NonBiometricUnlockWalletUseCase,
userWalletsListRepository: UserWalletsListRepository,
): UnlockWalletUseCase {
return UnlockWalletUseCase(
nonBiometricUnlockWalletUseCase = nonBiometricUnlockWalletUseCase,
userWalletsListRepository = userWalletsListRepository,
)
}
@Provides
@Singleton
fun providesNonBiometricUnlockWalletUseCase(
userWalletsListRepository: UserWalletsListRepository,
walletsRepository: WalletsRepository,
): NonBiometricUnlockWalletUseCase {
return NonBiometricUnlockWalletUseCase(
userWalletsListRepository = userWalletsListRepository,
walletsRepository = walletsRepository,
)
}
@Provides
@Singleton
fun providesSelectWalletUseCase(

View file

@ -32,6 +32,7 @@ class UserWalletItemUMConverter(
private val balance: TotalFiatBalance? = null,
private val isBalanceHidden: Boolean = false,
private val isAuthMode: Boolean = false,
private val isClickableIfLocked: Boolean = false,
private val endIcon: UserWalletItemUM.EndIcon = UserWalletItemUM.EndIcon.None,
artwork: UserWalletItemUM.ImageState? = null,
) : Converter<UserWallet, UserWalletItemUM> {
@ -54,7 +55,7 @@ class UserWalletItemUMConverter(
}
private fun isEnabled(userWallet: UserWallet): Boolean {
return isAuthMode || userWallet.isLocked.not()
return isAuthMode || isClickableIfLocked || userWallet.isLocked.not()
}
private fun getInfo(userWallet: UserWallet): UserWalletItemUM.Information.Loaded {

View file

@ -0,0 +1,43 @@
package com.tangem.domain.wallets.usecase
import arrow.core.Either
import arrow.core.raise.either
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.error.UnlockWalletError
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.repository.WalletsRepository
class NonBiometricUnlockWalletUseCase(
private val userWalletsListRepository: UserWalletsListRepository,
private val walletsRepository: WalletsRepository,
) {
suspend operator fun invoke(userWalletId: UserWalletId): Either<UnlockWalletError, Unit> = either {
val userWallet = userWalletsListRepository.userWalletsSync()
.find { it.walletId == userWalletId }
?: raise(UnlockWalletError.UserWalletNotFound)
if (!userWallet.isLocked) {
return@either
}
val method = when (userWallet) {
is UserWallet.Cold -> UserWalletsListRepository.UnlockMethod.Scan()
is UserWallet.Hot -> UserWalletsListRepository.UnlockMethod.AccessCode
}
userWalletsListRepository.unlock(userWalletId, method)
.onRight {
if (walletsRepository.useBiometricAuthentication()) {
// After successful unlock, set biometric lock if applicable
userWalletsListRepository.setLock(
userWalletId,
UserWalletsListRepository.LockMethod.Biometric,
)
}
}
.bind()
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.domain.wallets.usecase
import arrow.core.Either
import arrow.core.raise.either
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.error.UnlockWalletError
import com.tangem.domain.models.wallet.UserWalletId
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() }
}
}

View file

@ -17,6 +17,7 @@ import com.tangem.domain.wallets.models.UnlockWalletsError
*
[REDACTED_AUTHOR]
*/
@Deprecated("Use NonBiometricUnlockWalletUseCase after migrating to new wallets repository")
class UnlockWalletsUseCase(private val userWalletsListManager: UserWalletsListManager) {
suspend operator fun invoke(type: UnlockType = UnlockType.ANY): Either<UnlockWalletsError, Unit> = either {

View file

@ -8,7 +8,9 @@ import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase
import com.tangem.domain.wallets.usecase.UnlockWalletUseCase
import com.tangem.features.details.entity.UserWalletListUM
import com.tangem.features.details.impl.R
import com.tangem.features.details.utils.UserWalletSaver
@ -21,6 +23,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@ -33,6 +37,7 @@ internal class UserWalletListModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val userWalletSaver: UserWalletSaver,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val unlockWalletUseCase: UnlockWalletUseCase,
) : Model() {
private val isWalletSavingInProgress: MutableStateFlow<Boolean> = MutableStateFlow(value = false)
@ -40,7 +45,8 @@ internal class UserWalletListModel @Inject constructor(
messageSender = messageSender,
onlyMultiCurrency = false,
isAuthMode = false,
onWalletClick = { userWalletId -> router.push(AppRoute.WalletSettings(userWalletId)) },
isClickableIfLocked = hotWalletFeatureToggles.isHotWalletEnabled,
onWalletClick = ::onWalletClicked,
)
val state: MutableStateFlow<UserWalletListUM> = MutableStateFlow(
@ -87,4 +93,20 @@ internal class UserWalletListModel @Inject constructor(
}
}
}
private fun onWalletClicked(userWalletId: UserWalletId) {
if (hotWalletFeatureToggles.isHotWalletEnabled) {
modelScope.launch {
unlockWalletUseCase(userWalletId)
.onRight {
router.push(AppRoute.WalletSettings(userWalletId))
}
.onLeft { error ->
Timber.e("Failed to unlock wallet $userWalletId: $error")
}
}
} else {
router.push(AppRoute.WalletSettings(userWalletId))
}
}
}

View file

@ -15,6 +15,7 @@ interface UserWalletsFetcher {
messageSender: UiMessageSender,
onlyMultiCurrency: Boolean,
isAuthMode: Boolean,
isClickableIfLocked: Boolean,
onWalletClick: (UserWalletId) -> Unit,
): UserWalletsFetcher
}

View file

@ -14,7 +14,6 @@ import com.tangem.core.ui.components.bottomsheets.message.*
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.DialogMessage.Companion.invoke
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.core.ui.message.bottomSheetMessage
import com.tangem.domain.card.SetCardWasScannedUseCase
@ -125,6 +124,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
private val getUserWalletUseCase: GetUserWalletUseCase,
private val scanCardToUnlockWalletClickHandler: ScanCardToUnlockWalletClickHandler,
private val unlockWalletsUseCase: UnlockWalletsUseCase,
private val nonBiometricUnlockWalletUseCase: NonBiometricUnlockWalletUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val dispatchers: CoroutineDispatcherProvider,
private val shouldShowPromoWalletUseCase: ShouldShowPromoWalletUseCase,
@ -200,14 +200,8 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
modelScope.launch {
userWalletsListRepository.unlockAllWallets()
.onLeft {
val selectedUserWallet = getSelectedUserWallet() ?: return@onLeft
val selectedUserWalletId = selectedUserWallet.walletId
val method = when (selectedUserWallet) {
is UserWallet.Cold -> UserWalletsListRepository.UnlockMethod.Scan()
is UserWallet.Hot -> UserWalletsListRepository.UnlockMethod.AccessCode
}
userWalletsListRepository
.unlock(stateHolder.getSelectedWalletId(), method)
val selectedUserWalletId = stateHolder.getSelectedWalletId()
nonBiometricUnlockWalletUseCase(selectedUserWalletId)
.onLeft {
when (it) {
UnlockWalletError.AlreadyUnlocked -> Unit

View file

@ -48,6 +48,7 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor(
@Assisted private val messageSender: UiMessageSender,
@Assisted("onlyMultiCurrency") private val onlyMultiCurrency: Boolean,
@Assisted("isAuthMode") private val isAuthMode: Boolean,
@Assisted("isClickableIfLocked") private val isClickableIfLocked: Boolean,
private val userWalletImageFetcher: UserWalletImageFetcher,
dispatchers: CoroutineDispatcherProvider,
) : UserWalletsFetcher {
@ -60,6 +61,7 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor(
val uiModels = UserWalletItemUMConverter(
onClick = onWalletClick,
isAuthMode = isAuthMode,
isClickableIfLocked = isClickableIfLocked,
).convertList(wallets)
.toImmutableList()
@ -144,6 +146,7 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor(
UserWalletItemUM.EndIcon.None
},
isAuthMode = isAuthMode,
isClickableIfLocked = isClickableIfLocked,
)
.convert(userWallet)
}
@ -163,6 +166,7 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor(
messageSender: UiMessageSender,
@Assisted("onlyMultiCurrency") onlyMultiCurrency: Boolean,
@Assisted("isAuthMode") isAuthMode: Boolean,
@Assisted("isClickableIfLocked") isClickableIfLocked: Boolean,
onWalletClick: (UserWalletId) -> Unit,
): DefaultUserWalletsFetcher
}

View file

@ -30,6 +30,7 @@ internal class WcUserWalletsFetcher(
messageSender = messageSender,
onlyMultiCurrency = true,
isAuthMode = false,
isClickableIfLocked = false,
onWalletClick = { onWalletSelected(it) },
)

View file

@ -17,6 +17,7 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.settings.CanUseBiometryUseCase
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.NonBiometricUnlockWalletUseCase
import com.tangem.features.wallet.utils.UserWalletsFetcher
import com.tangem.features.welcome.impl.R
import com.tangem.features.welcome.impl.ui.state.WelcomeUM
@ -38,6 +39,7 @@ internal class WelcomeModel @Inject constructor(
private val router: Router,
private val uiMessageSender: UiMessageSender,
private val userWalletsListRepository: UserWalletsListRepository,
private val nonBiometricUnlockWalletUseCase: NonBiometricUnlockWalletUseCase,
private val canUseBiometryUseCase: CanUseBiometryUseCase,
private val walletsRepository: WalletsRepository,
userWalletsFetcherFactory: UserWalletsFetcher.Factory,
@ -50,6 +52,7 @@ internal class WelcomeModel @Inject constructor(
messageSender = uiMessageSender,
onlyMultiCurrency = false,
isAuthMode = true,
isClickableIfLocked = true,
onWalletClick = { walletId ->
modelScope.launch {
val userWallets = userWalletsListRepository.userWalletsSync()
@ -109,7 +112,7 @@ internal class WelcomeModel @Inject constructor(
val hotWalletLockedWithAccessCode = userWalletsListRepository.userWalletsSync()
.first { it.isLocked && it is UserWallet.Hot } as UserWallet.Hot
uiState.value = WelcomeUM.Empty
unlockWallet(hotWalletLockedWithAccessCode.walletId, UserWalletsListRepository.UnlockMethod.AccessCode)
nonBiometricUnlockWallet(hotWalletLockedWithAccessCode.walletId)
}
}
@ -161,15 +164,11 @@ internal class WelcomeModel @Inject constructor(
return@launch
}
val unlockMethod = when (userWallet) {
is UserWallet.Cold -> UserWalletsListRepository.UnlockMethod.Scan()
is UserWallet.Hot -> {
uiState.value = WelcomeUM.Empty
UserWalletsListRepository.UnlockMethod.AccessCode
}
if (userWallet is UserWallet.Hot) {
uiState.value = WelcomeUM.Empty
}
unlockWallet(userWallet.walletId, unlockMethod)
nonBiometricUnlockWallet(userWallet.walletId)
setSelectWalletState()
}
@ -177,8 +176,8 @@ internal class WelcomeModel @Inject constructor(
return canUseBiometryUseCase() && walletsRepository.useBiometricAuthentication()
}
suspend fun unlockWallet(userWalletId: UserWalletId, unlockMethod: UserWalletsListRepository.UnlockMethod) {
userWalletsListRepository.unlock(userWalletId, unlockMethod)
suspend fun nonBiometricUnlockWallet(userWalletId: UserWalletId) {
nonBiometricUnlockWalletUseCase(userWalletId)
.onRight {
routedOut = true
userWalletsListRepository.select(userWalletId)