Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-19 13:17:04 +03:00
parent bb95420224
commit 143dcc5a62
34 changed files with 632 additions and 114 deletions

View file

@ -28,6 +28,7 @@ import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
@ -38,7 +39,9 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
import com.tangem.tap.common.log.TangemAppLoggerInitializer
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
@ -142,4 +145,10 @@ interface ApplicationEntryPoint {
fun getApiConfigsManager(): ApiConfigsManager
fun getUserTokensResponseStore(): UserTokensResponseStore
fun getUserWalletsListRepository(): UserWalletsListRepository
fun getTangemHotSdk(): TangemHotSdk
fun getHotWalletFeatureToggles(): HotWalletFeatureToggles
}

View file

@ -227,6 +227,15 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
private val userTokensResponseStore: UserTokensResponseStore
get() = entryPoint.getUserTokensResponseStore()
private val userWalletsListRepository
get() = entryPoint.getUserWalletsListRepository()
private val tangemHotSdk
get() = entryPoint.getTangemHotSdk()
private val hotWalletFeatureToggles
get() = entryPoint.getHotWalletFeatureToggles()
// endregion
private val appScope = MainScope()
@ -364,6 +373,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
uiMessageSender = uiMessageSender,
coldUserWalletBuilderFactory = coldUserWalletBuilderFactory,
userTokensResponseStore = userTokensResponseStore,
userWalletsListRepository = userWalletsListRepository,
tangemHotSdk = tangemHotSdk,
hotWalletFeatureToggles = hotWalletFeatureToggles,
),
),
)

View file

@ -26,10 +26,9 @@ internal object LegacyMiddleware {
{ action ->
when (action) {
is LegacyAction.PrepareDetailsScreen -> {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
userWalletsListManager.selectedUserWallet
selectedUserWallet()
.distinctUntilChanged()
.onEach { selectedUserWallet ->
val initializedAppSettingsStateContent = initializeAppSettingsState(
@ -52,6 +51,16 @@ internal object LegacyMiddleware {
}
}
private fun selectedUserWallet(): Flow<UserWallet> {
val hotWalletFeatureToggles = store.inject(DaggerGraphState::hotWalletFeatureToggles)
return if (hotWalletFeatureToggles.isHotWalletEnabled) {
store.inject(DaggerGraphState::userWalletsListRepository).selectedUserWallet.filterNotNull()
} else {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
userWalletsListManager.selectedUserWallet
}
}
/**
* LEGACY: We need to initialize [AppSettingsState] async to avoid drawing blocking
* previously it was initialized in runBlocking and blocked details screen
@ -64,6 +73,8 @@ internal object LegacyMiddleware {
selectedAppCurrency = store.state.globalState.appCurrency,
selectedThemeMode = store.inject(DaggerGraphState::appThemeModeRepository).getAppThemeMode().firstOrNull()
?: AppThemeMode.DEFAULT,
requireAccessCode = store.inject(DaggerGraphState::walletsRepository).requireAccessCode(),
useBiometricAuthentication = store.inject(DaggerGraphState::walletsRepository).useBiometricAuthentication(),
isHidingEnabled = store.inject(DaggerGraphState::balanceHidingRepository)
.getBalanceHidingSettings().isHidingEnabledInSettings,
needEnrollBiometrics = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() == true,

View file

@ -123,10 +123,12 @@ internal object WalletsDomainModule {
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
walletsRepository: WalletsRepository,
): SaveWalletUseCase {
return SaveWalletUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
walletsRepository = walletsRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
}

View file

@ -119,6 +119,7 @@ internal object UserWalletsListManagerModule {
@ApplicationContext applicationContext: Context,
dispatchers: CoroutineDispatcherProvider,
passwordRequester: HotWalletPasswordRequester,
appPreferencesStore: AppPreferencesStore,
): UserWalletsListRepository {
val moshi = buildMoshi()
val secureStorage = buildSecureStorage(applicationContext = applicationContext)
@ -162,8 +163,8 @@ internal object UserWalletsListManagerModule {
passwordRequester = passwordRequester,
userWalletEncryptionKeysRepository = userWalletEncryptionKeysRepository,
tangemSdkManagerProvider = Provider { tangemSdkManager },
appPreferencesStore = appPreferencesStore,
savePersistentInformation = ProviderSuspend { true }, // Always save persistent information for now
// TODO add a settings toggle to disable saving persistent information
)
}

View file

@ -8,6 +8,9 @@ import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.flatMap
import com.tangem.common.map
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isLocked
@ -43,6 +46,7 @@ internal class DefaultUserWalletsListRepository(
private val userWalletEncryptionKeysRepository: UserWalletEncryptionKeysRepository,
private val tangemSdkManagerProvider: Provider<TangemSdkManager>,
private val savePersistentInformation: ProviderSuspend<Boolean>,
private val appPreferencesStore: AppPreferencesStore,
) : UserWalletsListRepository {
override val userWallets = MutableStateFlow<List<UserWallet>?>(null)
@ -129,38 +133,46 @@ internal class DefaultUserWalletsListRepository(
userWallet
}
override suspend fun setLock(userWalletId: UserWalletId, lockMethod: LockMethod): Either<SetLockError, Unit> =
either {
val userWallet = userWallets.value?.find { it.walletId == userWalletId }
?: raise(SetLockError.UserWalletNotFound)
override suspend fun setLock(
userWalletId: UserWalletId,
lockMethod: LockMethod,
changeUnsecured: Boolean,
): Either<SetLockError, Unit> = either {
val userWallet = userWallets.value?.find { it.walletId == userWalletId }
?: raise(SetLockError.UserWalletNotFound)
val encryptionKey = userWallet.encryptionKey
?: raise(SetLockError.UserWalletLocked)
val encryptionKey = userWallet.encryptionKey
?: raise(SetLockError.UserWalletLocked)
runCatching {
userWalletEncryptionKeysRepository.save(
encryptionKey = UserWalletEncryptionKey(
walletId = userWalletId,
encryptionKey = encryptionKey,
),
method = when (lockMethod) {
is LockMethod.AccessCode -> {
UserWalletEncryptionKeysRepository.EncryptionMethod.Password(lockMethod.accessCode)
runCatching {
userWalletEncryptionKeysRepository.save(
encryptionKey = UserWalletEncryptionKey(
walletId = userWalletId,
encryptionKey = encryptionKey,
),
removeUnsecured = changeUnsecured,
method = when (lockMethod) {
is LockMethod.AccessCode -> {
UserWalletEncryptionKeysRepository.EncryptionMethod.Password(lockMethod.accessCode)
}
LockMethod.Biometric -> {
UserWalletEncryptionKeysRepository.EncryptionMethod.Biometric
}
LockMethod.NoLock -> {
if (userWallet is UserWallet.Cold) {
raise(SetLockError.UserWalletNotFound)
}
LockMethod.Biometric -> {
UserWalletEncryptionKeysRepository.EncryptionMethod.Biometric
}
LockMethod.NoLock -> {
if (userWallet is UserWallet.Cold) {
raise(SetLockError.UserWalletNotFound)
}
UserWalletEncryptionKeysRepository.EncryptionMethod.Unsecured
}
},
)
}.onFailure { raise(SetLockError.UnableToSetLock(it)) }
}
UserWalletEncryptionKeysRepository.EncryptionMethod.Unsecured
}
},
)
}.onFailure { raise(SetLockError.UnableToSetLock(it)) }
}
override suspend fun removeBiometricLock(userWalletId: UserWalletId) {
userWalletEncryptionKeysRepository.removeBiometricKey(userWalletId)
}
override suspend fun delete(userWalletIds: List<UserWalletId>): Either<DeleteWalletError, Unit> = either {
if (userWalletIds.isEmpty()) return Unit.right()
@ -269,9 +281,11 @@ internal class DefaultUserWalletsListRepository(
}
val unsecuredKeys = userWalletEncryptionKeysRepository.getAllUnsecured()
val allKeys = biometricKeys + unsecuredKeys
val allKeys = (biometricKeys + unsecuredKeys).distinct()
val unlockedWallets = allKeys.map { it.walletId }
if (allKeys.all { it.walletId in userWalletIds }.not()) {
// if we cant unlock all wallets
if (userWalletIds.all { it in unlockedWallets }.not()) {
raise(UnlockWalletError.UnableToUnlock)
}
@ -309,7 +323,7 @@ internal class DefaultUserWalletsListRepository(
biometryFallback: suspend () -> Either<UnlockWalletError, Unit>,
): Either<UnlockWalletError, UserWalletEncryptionKey?> {
val result = passwordRequester.requestPassword(
hasBiometry = tangemSdkManagerProvider.invoke().canUseBiometry,
hasBiometry = hasBiometry(),
)
return when (result) {
@ -339,6 +353,15 @@ internal class DefaultUserWalletsListRepository(
}
}
private suspend fun hasBiometry(): Boolean {
val useBiometricAuthentication = appPreferencesStore.getSyncOrDefault(
key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY,
default = false,
)
return tangemSdkManagerProvider.invoke().canUseBiometry && useBiometricAuthentication
}
/**
* Find the nearest available wallet that can be selected
*

View file

@ -25,8 +25,14 @@ internal class UserWalletEncryptionKeysRepository(
Types.newParameterizedType(List::class.java, UserWalletId::class.java),
)
suspend fun save(encryptionKey: UserWalletEncryptionKey, method: EncryptionMethod) = withContext(dispatchers.io) {
secureStorage.delete(StorageKey.UserWalletEncryptionKeyUnsecured(encryptionKey.walletId).name)
suspend fun save(
encryptionKey: UserWalletEncryptionKey,
removeUnsecured: Boolean = true,
method: EncryptionMethod,
) = withContext(dispatchers.io) {
if (removeUnsecured) {
secureStorage.delete(StorageKey.UserWalletEncryptionKeyUnsecured(encryptionKey.walletId).name)
}
when (method) {
EncryptionMethod.Unsecured -> {
@ -35,12 +41,6 @@ internal class UserWalletEncryptionKeysRepository(
data = encryptionKey.encode(),
)
}
EncryptionMethod.Biometric -> {
authenticatedStorage.store(
keyAlias = StorageKey.UserWalletEncryptionKey(encryptionKey.walletId).name,
data = encryptionKey.encode(),
)
}
is EncryptionMethod.Password -> {
val encodedWithPass = AESEncryptionProtocol.encryptWithPassword(
password = method.password,
@ -51,11 +51,21 @@ internal class UserWalletEncryptionKeysRepository(
data = encodedWithPass,
)
}
EncryptionMethod.Biometric -> {
authenticatedStorage.store(
keyAlias = StorageKey.UserWalletEncryptionKey(encryptionKey.walletId).name,
data = encryptionKey.encode(),
)
}
}
storeUserWalletId(userWalletId = encryptionKey.walletId)
}
fun removeBiometricKey(userWalletId: UserWalletId) {
authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name)
}
suspend fun getAllUnsecured(): List<UserWalletEncryptionKey> = withContext(dispatchers.io) {
getUserWalletsIds().mapNotNull { userWalletId ->
secureStorage.get(account = StorageKey.UserWalletEncryptionKeyUnsecured(userWalletId).name).decodeToKey()

View file

@ -6,7 +6,9 @@ import com.tangem.common.doOnSuccess
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.core.wallets.UserWalletsListRepository.LockMethod
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction
@ -64,6 +66,14 @@ class DetailsMiddleware {
when (action.setting) {
AppSetting.SaveWallets -> toggleSaveWallets(state, enable = action.enable)
AppSetting.SaveAccessCode -> toggleSaveAccessCodes(state, enable = action.enable)
AppSetting.RequireAccessCode -> toggleRequireAccessCode(
state = state,
enable = action.enable,
)
AppSetting.BiometricAuthentication -> toggleBiometricsAuthentication(
state = state,
enable = action.enable,
)
}
}
is DetailsAction.AppSettings.CheckBiometricsStatus -> {
@ -90,6 +100,91 @@ class DetailsMiddleware {
}
}
private fun toggleBiometricsAuthentication(state: DetailsState, enable: Boolean) {
scope.launch {
val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
// Nothing to change
if (walletsRepository.useBiometricAuthentication() == enable) {
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
return@launch
}
toggleRequireAccessCode(
state = state,
enable = true,
)
if (enable) {
setBiometricLockForAllWallets()
} else {
// Remove all biometric-related data
removeAllBiometricData()
}
walletsRepository.setUseBiometricAuthentication(value = enable)
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
}
}
private fun toggleRequireAccessCode(state: DetailsState, enable: Boolean) {
scope.launch {
val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
// Nothing to change
if (walletsRepository.requireAccessCode() == enable) {
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
return@launch
}
if (enable) {
// Remove all biometric sign data
removeAllBiometricSingData()
toggleSaveAccessCodes(state, enable = false)
} else {
toggleSaveAccessCodes(state, enable = true)
}
walletsRepository.setRequireAccessCode(value = enable)
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
}
}
private suspend fun setBiometricLockForAllWallets() {
val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository)
val userWallets = userWalletsListRepository.userWalletsSync()
userWallets.forEach {
userWalletsListRepository.setLock(
userWalletId = it.walletId,
lockMethod = LockMethod.Biometric,
changeUnsecured = false,
)
}
}
private suspend fun removeAllBiometricData() {
val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository)
userWalletsListRepository.userWalletsSync().forEach {
userWalletsListRepository.removeBiometricLock(it.walletId)
}
removeAllBiometricSingData()
}
private suspend fun removeAllBiometricSingData() {
deleteSavedAccessCodes()
val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository)
val tangemHotSdk = store.inject(DaggerGraphState::tangemHotSdk)
userWalletsListRepository.userWalletsSync().forEach {
if (it is UserWallet.Hot) {
userWalletsListRepository.saveWithoutLock(
userWallet = it.copy(
hotWalletId = tangemHotSdk.removeBiometryAuthIfPresented(it.hotWalletId),
),
)
}
}
}
private fun observeBiometricsStatusChanges(scope: CoroutineScope) {
val needEnrollBiometricsFlow = flow {
do {

View file

@ -33,6 +33,7 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsSta
)
}
@Suppress("LongMethod", "CyclomaticComplexMethod")
private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: DetailsState): DetailsState {
return when (action) {
is DetailsAction.AppSettings.SwitchPrivacySetting -> state.copy(
@ -46,6 +47,14 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
saveWallets = true, // User can't enable access codes saving without wallets saving
saveAccessCodes = action.enable,
)
AppSetting.RequireAccessCode -> state.appSettingsState.copy(
isInProgress = true,
requireAccessCode = action.enable,
)
AppSetting.BiometricAuthentication -> state.appSettingsState.copy(
isInProgress = true,
useBiometricAuthentication = action.enable,
)
},
)
is DetailsAction.AppSettings.SwitchPrivacySetting.Success -> state.copy(
@ -63,6 +72,14 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
isInProgress = false,
saveAccessCodes = action.prevState,
)
AppSetting.RequireAccessCode -> state.appSettingsState.copy(
isInProgress = false,
requireAccessCode = action.prevState,
)
AppSetting.BiometricAuthentication -> state.appSettingsState.copy(
isInProgress = false,
needEnrollBiometrics = action.prevState,
)
},
)
is DetailsAction.AppSettings.BiometricsStatusChanged -> state.copy(

View file

@ -12,9 +12,14 @@ data class DetailsState(
) : StateType
data class AppSettingsState(
@Deprecated("Delete after hot wallet release")
val saveWallets: Boolean = false,
@Deprecated("Delete after hot wallet release")
val saveAccessCodes: Boolean = false,
@Deprecated("Delete after hot wallet release")
val isBiometricsAvailable: Boolean = false,
val requireAccessCode: Boolean = false,
val useBiometricAuthentication: Boolean = false,
val needEnrollBiometrics: Boolean = false,
val isHidingEnabled: Boolean = false,
val isInProgress: Boolean = false,
@ -25,5 +30,5 @@ data class AppSettingsState(
enum class SecurityOption { LongTap, PassCode, AccessCode }
enum class AppSetting {
SaveWallets, SaveAccessCode
SaveWallets, SaveAccessCode, RequireAccessCode, BiometricAuthentication,
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.details.ui.appsettings
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog
import com.tangem.wallet.R
@ -55,4 +56,37 @@ internal class AppSettingsDialogsFactory {
onDismiss = onDismiss,
)
}
fun createDisableBiometricAuthenticationAlert(onDisable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert {
return Dialog.Alert(
title = resourceReference(R.string.common_attention),
description = resourceReference(
R.string.app_settings_off_biometrics_alert_message,
wrappedList(resourceReference(R.string.common_biometrics)),
),
confirmText = resourceReference(R.string.common_disable),
onConfirm = onDisable,
onDismiss = onDismiss,
)
}
fun createEnableRequireAccessCodeAlert(onEnable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert {
return Dialog.Alert(
title = resourceReference(R.string.common_attention),
description = resourceReference(R.string.app_settings_on_require_access_code_alert_message),
confirmText = resourceReference(R.string.common_enable),
onConfirm = { onEnable() },
onDismiss = onDismiss,
)
}
fun createDisableRequireAccessCodeAlert(onDisable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert {
return Dialog.Alert(
title = resourceReference(R.string.common_attention),
description = resourceReference(R.string.app_settings_off_require_access_code_alert_message),
confirmText = resourceReference(R.string.common_disable),
onConfirm = { onDisable() },
onDismiss = onDismiss,
)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.details.ui.appsettings
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item
import com.tangem.wallet.R
@ -33,6 +34,39 @@ internal class AppSettingsItemsFactory {
)
}
fun createUseBiometricsSwitch(
isChecked: Boolean,
isEnabled: Boolean,
onCheckedChange: (Boolean) -> Unit,
): Item.Switch {
return Item.Switch(
id = ID_USE_BIOMETRICS_SWITCH,
title = resourceReference(R.string.app_settings_enable_biometrics_title),
description = resourceReference(
R.string.app_settings_biometrics_footer,
wrappedList(resourceReference(R.string.common_biometrics)),
),
isEnabled = isEnabled,
isChecked = isChecked,
onCheckedChange = onCheckedChange,
)
}
fun createRequireAccessCodeSwitch(
isChecked: Boolean,
isEnabled: Boolean,
onCheckedChange: (Boolean) -> Unit,
): Item.Switch {
return Item.Switch(
id = ID_REQUIRE_ACCESS_CODE_SWITCH,
title = resourceReference(R.string.app_settings_require_access_code),
description = resourceReference(R.string.app_settings_require_access_code_footer),
isEnabled = isEnabled,
isChecked = isChecked,
onCheckedChange = onCheckedChange,
)
}
fun createSaveAccessCodeSwitch(
isChecked: Boolean,
isEnabled: Boolean,
@ -96,5 +130,7 @@ internal class AppSettingsItemsFactory {
const val ID_FLIP_TO_HIDE_BALANCE_SWITCH = "flip_to_hide_balance_switch"
const val ID_SELECT_APP_CURRENCY_BUTTON = "select_app_currency_button"
const val ID_SELECT_THEME_MODE_BUTTON = "select_theme_mode_button"
const val ID_USE_BIOMETRICS_SWITCH = "use_biometrics_switch"
const val ID_REQUIRE_ACCESS_CODE_SWITCH = "require_access_code_switch"
}
}

View file

@ -13,6 +13,7 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.settings.CanUseBiometryUseCase
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction
@ -51,6 +52,7 @@ internal class AppSettingsModel @Inject constructor(
private val appThemeModeRepository: AppThemeModeRepository,
private val settingsRepository: SettingsRepository,
private val appSettingsItemsAnalyticsSender: AppSettingsItemsAnalyticsSender,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
) : Model(), StoreSubscriber<DetailsState> {
private val itemsFactory = AppSettingsItemsFactory()
@ -109,20 +111,36 @@ internal class AppSettingsModel @Inject constructor(
onClick = ::showAppCurrencySelector,
).let(::add)
if (state.isBiometricsAvailable) {
if (hotWalletFeatureToggles.isHotWalletEnabled) {
val canUseBiometrics = !state.needEnrollBiometrics && !state.isInProgress
itemsFactory.createSaveWalletsSwitch(
isChecked = state.saveWallets,
itemsFactory.createUseBiometricsSwitch(
isChecked = state.useBiometricAuthentication,
isEnabled = canUseBiometrics,
onCheckedChange = ::onSaveWalletsToggled,
onCheckedChange = ::onBiometricAuthenticationToggled,
).let(::add)
itemsFactory.createSaveAccessCodeSwitch(
isChecked = state.saveAccessCodes,
isEnabled = canUseBiometrics,
onCheckedChange = ::onSaveAccessCodesToggled,
itemsFactory.createRequireAccessCodeSwitch(
isChecked = state.requireAccessCode,
isEnabled = canUseBiometrics && state.useBiometricAuthentication,
onCheckedChange = ::onRequireAccessCodeToggled,
).let(::add)
} else {
if (state.isBiometricsAvailable) {
val canUseBiometrics = !state.needEnrollBiometrics && !state.isInProgress
itemsFactory.createSaveWalletsSwitch(
isChecked = state.saveWallets,
isEnabled = canUseBiometrics,
onCheckedChange = ::onSaveWalletsToggled,
).let(::add)
itemsFactory.createSaveAccessCodeSwitch(
isChecked = state.saveAccessCodes,
isEnabled = canUseBiometrics,
onCheckedChange = ::onSaveAccessCodesToggled,
).let(::add)
}
}
itemsFactory.createFlipToHideBalanceSwitch(
@ -168,6 +186,56 @@ internal class AppSettingsModel @Inject constructor(
}
}
private fun onBiometricAuthenticationToggled(isChecked: Boolean) {
// TODO : Uncomment and implement analytics event when ready
// val param = AnalyticsParam.OnOffState(isChecked)
// analyticsEventHandler.send(Settings.AppSettings.BiometricAuthenticationChanged(param))
if (isChecked) {
onSettingsToggled(AppSetting.BiometricAuthentication, enable = true)
onSettingsToggled(AppSetting.RequireAccessCode, enable = true)
} else {
updateContentState {
copy(
dialog = dialogsFactory.createDisableBiometricAuthenticationAlert(
onDisable = {
onSettingsToggled(AppSetting.BiometricAuthentication, enable = false)
onSettingsToggled(AppSetting.RequireAccessCode, enable = true)
dismissDialog()
},
onDismiss = ::dismissDialog,
),
)
}
}
}
private fun onRequireAccessCodeToggled(isChecked: Boolean) {
// TODO : Uncomment and implement analytics event when ready
// val param = AnalyticsParam.OnOffState(isChecked)
// analyticsEventHandler.send(Settings.AppSettings.RequireAccessCodeChanged(param))
updateContentState {
copy(
dialog = if (isChecked) {
dialogsFactory.createEnableRequireAccessCodeAlert(
onEnable = {
onSettingsToggled(AppSetting.RequireAccessCode, enable = true)
dismissDialog()
},
onDismiss = ::dismissDialog,
)
} else {
dialogsFactory.createDisableRequireAccessCodeAlert(
onDisable = {
onSettingsToggled(AppSetting.RequireAccessCode, enable = false)
dismissDialog()
},
onDismiss = ::dismissDialog,
)
},
)
}
}
private fun onSaveWalletsToggled(isChecked: Boolean) {
if (isChecked) {
onSettingsToggled(AppSetting.SaveWallets, enable = true)
@ -236,6 +304,8 @@ internal class AppSettingsModel @Inject constructor(
saveWallets = walletsRepository.shouldSaveUserWalletsSync(),
saveAccessCodes = settingsRepository.shouldSaveAccessCodes(),
isBiometricsAvailable = canUseBiometryUseCase(),
useBiometricAuthentication = walletsRepository.useBiometricAuthentication(),
requireAccessCode = walletsRepository.requireAccessCode(),
isHidingEnabled = balanceHidingRepository.getBalanceHidingSettings().isHidingEnabledInSettings,
selectedAppCurrency = appCurrencyRepository.getSelectedAppCurrency().firstOrNull() ?: AppCurrency.Default,
selectedThemeMode = appThemeModeRepository.getAppThemeMode().firstOrNull() ?: AppThemeMode.DEFAULT,

View file

@ -21,6 +21,7 @@ import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
@ -31,7 +32,9 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.operations.attestation.CardArtworksProvider
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository
@ -77,4 +80,7 @@ data class DaggerGraphState(
val cardArworksProvider: CardArtworksProvider? = null,
val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory? = null,
val userTokensResponseStore: UserTokensResponseStore? = null,
val userWalletsListRepository: UserWalletsListRepository? = null,
val hotWalletFeatureToggles: HotWalletFeatureToggles? = null,
val tangemHotSdk: TangemHotSdk? = null,
) : StateType

View file

@ -84,6 +84,10 @@ object PreferencesKeys {
val SHOULD_SAVE_ACCESS_CODES_KEY by lazy { booleanPreferencesKey(name = "saveAccessCodes") }
val REQUIRE_ACCESS_CODE_KEY by lazy { booleanPreferencesKey(name = "requireAccessCode") }
val USE_BIOMETRIC_AUTHENTICATION_KEY by lazy { booleanPreferencesKey(name = "useBiometricAuthentication") }
val SHOULD_SHOW_MARKETS_TOOLTIP_KEY by lazy { booleanPreferencesKey(name = "shouldShowMarketsTooltip") }
val MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY by lazy {

View file

@ -947,9 +947,13 @@
<string name="send_validation_invalid_fee">Die Gebühr geht über die Bilanz hinaus</string>
<string name="send_validation_invalid_total">Der Gesamtbetrag geht über die Bilanz hinaus</string>
<string name="send_with_swap_confirm_title">Tauschen und senden</string>
<string name="send_with_swap_convert_token_alert_message">Mit der Konvertierung fortfahren? Dadurch werden Deine vorherigen Daten gelöscht.</string>
<string name="send_with_swap_correct_recipient_network_notification_message">Das Senden einer anderen Währung führt zu deren unwiderruflichem Verlust.</string>
<string name="send_with_swap_correct_recipient_network_notification_title">Wähle das richtige Empfängernetzwerk</string>
<string name="send_with_swap_notification_text">Sende uns ein Token, und wir konvertieren es unterwegs. Dein Empfänger erhält genau das, was er braucht nahtlos.</string>
<string name="send_with_swap_recipient_amount_text">Wird an den Empfänger gesendet</string>
<string name="send_with_swap_recipient_amount_title">Zu erhaltender Betrag</string>
<string name="send_with_swap_remove_convert_alert_message">Möchtest Du die Konvertierung wirklich abbrechen? Deine bisherigen Daten werden gelöscht.</string>
<string name="send_with_swap_title">Senden mit Swap</string>
<string name="sent_transaction_sent_title">Transaktion gesendet</string>
<string name="settings_card_settings_footer">Bereite das Scannen der Karte oder Ring vor, die du einrichten möchtest.</string>
@ -1408,7 +1412,7 @@
<string name="wc_alert_unsupported_method_title">Wir haben einen unbekannten Fehler festgestellt.</string>
<string name="wc_alert_unsupported_networks_description">Tangem unterstützt derzeit das erforderliches Netzwerk von %s nicht.</string>
<string name="wc_alert_unsupported_networks_title">Nicht unterstützte Netzwerke</string>
<string name="wc_alert_verified_domain_description">Tangem unterstützt ein erforderliches Netzwerk um %s</string>
<string name="wc_alert_verified_domain_description">Diese Domain hat die Überprüfungen bestanden und gilt als sicher, vertrauenswürdig und frei von bekannten Bedrohungen oder verdächtigen Aktivitäten. %s</string>
<string name="wc_alert_verified_domain_title">Verifizierte Domain</string>
<string name="wc_alert_wrong_card_description">Falsche Karte oder falscher Ring in der App ausgewählt</string>
<string name="wc_alert_wrong_card_title">Wir haben eine Art Problem</string>
@ -1432,7 +1436,7 @@
<string name="wc_copy_data_button_text">Daten kopieren</string>
<string name="wc_custom_allowance_title">Benutzerdefinierter Freibetrag</string>
<string name="wc_disconnect_all">Alle trennen</string>
<string name="wc_disconnect_all_alert_desc">Text über die Trennung aller dApps</string>
<string name="wc_disconnect_all_alert_desc">Alle dApp-Sitzungen werden getrennt. Ihre Wallet wird nicht mehr mit dApps verbunden sein.</string>
<string name="wc_disconnect_all_alert_title">Alle dApps trennen</string>
<string name="wc_errors_invalid_domain_subtitle">Versuchen Sie erneut, mit einer neuen URI zu koppeln</string>
<string name="wc_errors_invalid_domain_title">Ungültige dApp-Domain</string>

View file

@ -1354,7 +1354,7 @@
<string name="wc_alert_unsupported_method_title">Hemos encontrado un error desconocido</string>
<string name="wc_alert_unsupported_networks_description">Actualmente, Tangem no es compatible con una red requerida por %s.</string>
<string name="wc_alert_unsupported_networks_title">Redes no compatibles</string>
<string name="wc_alert_verified_domain_description">Tangem soporta una red requerida por %s</string>
<string name="wc_alert_verified_domain_description">Este dominio ha superado las verificaciones y se considera seguro, confiable y libre de amenazas conocidas o actividades sospechosas. %s</string>
<string name="wc_alert_verified_domain_title">Dominio verificado</string>
<string name="wc_alert_wrong_card_description">Se seleccionó una tarjeta o un anillo incorrectos en la app</string>
<string name="wc_alert_wrong_card_title">Tenemos algún tipo de problema</string>
@ -1382,7 +1382,7 @@
<string name="wc_custom_allowance_title">Asignación personalizada</string>
<string name="wc_dapp_disconnected">dApp desconectada</string>
<string name="wc_disconnect_all">Desconectar todo</string>
<string name="wc_disconnect_all_alert_desc">Texto sobre desconexión de todas las dApps</string>
<string name="wc_disconnect_all_alert_desc">Todas las sesiones de dApp se desconectarán. Su billetera ya no estará vinculada a ninguna dApp.</string>
<string name="wc_disconnect_all_alert_title">Desconectar todas las dApps</string>
<string name="wc_errors_invalid_domain_subtitle">Intente emparejar nuevamente con una URI nueva</string>
<string name="wc_errors_invalid_domain_title">Dominio de dApp no válido</string>
@ -1418,7 +1418,7 @@
<string name="wc_unlimited_amount">Cantidad ilimitada</string>
<string name="wc_uri_already_used_description">Asegúrese de que cada intento de emparejamiento utiliza un URI nuevo y único</string>
<string name="wc_uri_already_used_title">URI ya utilizado</string>
<string name="wc_wallet_connect">Wallet connect</string>
<string name="wc_wallet_connect">WalletConnect</string>
<string name="wc_warning_transaction">Transacción sospechosa</string>
<string name="welcome_interrupted_backup_alert_discard">Ignorar</string>
<string name="welcome_interrupted_backup_alert_message">Tiene un backup interrumpido. ¿Quiere reanudarlo?</string>

View file

@ -1324,6 +1324,7 @@
<string name="wc_alert_unsupported_dapps_title">dApp non prise en charge</string>
<string name="wc_alert_unsupported_method_description">Code d\'erreur : 8 005. Si le problème persiste, n\'hésitez pas à contacter notre support.</string>
<string name="wc_alert_unsupported_method_title">Nous avons rencontré une erreur inconnue</string>
<string name="wc_alert_verified_domain_description">Ce domaine a passé les contrôles de vérification et est considéré comme sûr, fiable et exempt de menaces connues ou dactivités suspectes.%s</string>
<string name="wc_allow_to_spend">Autoriser à dépenser</string>
<string name="wc_common_address">Adresse</string>
<string name="wc_common_loading">Chargement</string>
@ -1333,7 +1334,7 @@
<string name="wc_contents">Contenu</string>
<string name="wc_copy_data_button_text">Copier les données</string>
<string name="wc_disconnect_all">Déconnecter tout</string>
<string name="wc_disconnect_all_alert_desc">Texte sur la déconnexion de toutes les dApps</string>
<string name="wc_disconnect_all_alert_desc">Toutes les sessions dApp seront déconnectées. Votre portefeuille ne sera plus lié à aucune dApp.</string>
<string name="wc_disconnect_all_alert_title">Déconnecter toutes les dApps</string>
<string name="wc_errors_invalid_domain_subtitle">Essayez de jumeler à nouveau avec un nouvel URI</string>
<string name="wc_errors_invalid_domain_title">Domaine dApp invalide</string>

View file

@ -12,10 +12,13 @@
<string name="access_code_create_description">ウォレットのロックを解除するには、 %s桁のアクセスコードを設定します。</string>
<string name="access_code_create_title">アクセスコードの作成</string>
<string name="access_code_navtitle">アクセスコード</string>
<string name="account_archived_recover">回復する</string>
<string name="account_archived_title">アーカイブ済み</string>
<string name="account_details_archive">アカウントをアーカイブする</string>
<string name="account_details_archive_action">アーカイブ</string>
<string name="account_details_archive_description">このアカウントをアーカイブしますが、いつでも復元できます。</string>
<string name="account_details_title">アカウント</string>
<string name="account_form_account_index">アカウント番号%s — アドレス導出に使用されます。</string>
<string name="account_form_create_button">アカウントを追加</string>
<string name="account_form_edit_button">保存</string>
<string name="account_form_name">アカウント名</string>
@ -287,6 +290,7 @@
<string name="common_transaction_status">取引状況</string>
<string name="common_transactions">取引</string>
<string name="common_transfer">送金</string>
<string name="common_unable_to_load">データを読み込めません…</string>
<string name="common_understand">わかりました</string>
<string name="common_unknown_error">エラーが発生しました。もう一度お試しください。</string>
<string name="common_unreachable">アクセスできません</string>
@ -984,10 +988,13 @@
<string name="send_with_swap_confirm_title">スワップして送信</string>
<string name="send_with_swap_convert_token_alert_message">変換を続行しますか? これにより以前のデータは消去されます。</string>
<string name="send_with_swap_convert_token_alert_title">変換を確定</string>
<string name="send_with_swap_correct_recipient_network_notification_message">その他の通貨を送信すると、取り返しのつかない損失が発生します。</string>
<string name="send_with_swap_correct_recipient_network_notification_title">正しい受信者ネットワークを選択してください</string>
<string name="send_with_swap_notification_text">トークンを送信すれば、送信中に変換されます。受信者は必要なものをシームレスに受け取ります。</string>
<string name="send_with_swap_recipient_amount_success_title">受信者は受け取ります</string>
<string name="send_with_swap_recipient_amount_text">受取人へ</string>
<string name="send_with_swap_recipient_amount_title">受取金額</string>
<string name="send_with_swap_recipient_get_amount">受信者は%sを取得します</string>
<string name="send_with_swap_remove_convert_alert_message">変換をキャンセルしてもよろしいですか?以前のデータは消去されます。</string>
<string name="send_with_swap_remove_convert_alert_title">変換を削除</string>
<string name="send_with_swap_title">スワップして送信</string>
@ -1448,7 +1455,7 @@
<string name="wc_alert_unsupported_method_title">不明なエラーが発生しました</string>
<string name="wc_alert_unsupported_networks_description">Tangemは現在%sで必要なネットワークをサポートしていません。</string>
<string name="wc_alert_unsupported_networks_title">未対応のネットワーク</string>
<string name="wc_alert_verified_domain_description">Tangemは%sで必要なネットワークをサポートします</string>
<string name="wc_alert_verified_domain_description">このドメインは検証チェックに合格しており、安全で信頼でき、既知の脅威や不審な活動がないと判断されています。%s</string>
<string name="wc_alert_verified_domain_title">検証済みドメイン</string>
<string name="wc_alert_wrong_card_description">アプリで間違ったカードまたはリングが選択されました</string>
<string name="wc_alert_wrong_card_title">問題が起きています</string>
@ -1476,7 +1483,7 @@
<string name="wc_custom_allowance_title">使用可能量の設定</string>
<string name="wc_dapp_disconnected">dAppが接続解除されました</string>
<string name="wc_disconnect_all">すべての接続を解除する</string>
<string name="wc_disconnect_all_alert_desc">すべてのdAppsの接続解除に関するテキスト</string>
<string name="wc_disconnect_all_alert_desc">すべてのdAppセッションが切断されます。ウォレットはどのdAppにも接続されなくなります。</string>
<string name="wc_disconnect_all_alert_title">すべてのdAppを接続解除する</string>
<string name="wc_errors_invalid_domain_subtitle">新しいURIで、再度ペアリングを試してください</string>
<string name="wc_errors_invalid_domain_title">無効なdAppドメイン</string>
@ -1512,7 +1519,7 @@
<string name="wc_unlimited_amount">無制限</string>
<string name="wc_uri_already_used_description">各ペアリング試行で、新しくユニークなURIが使用されていることを確認します</string>
<string name="wc_uri_already_used_title">URIはすでに使用されています</string>
<string name="wc_wallet_connect">ウォレットコネクト</string>
<string name="wc_wallet_connect">WalletConnect</string>
<string name="wc_warning_transaction">不審な取引</string>
<string name="welcome_interrupted_backup_alert_discard">破棄</string>
<string name="welcome_interrupted_backup_alert_message">バックアップが中断されました。再開しますか?</string>

View file

@ -231,6 +231,7 @@
<string name="common_transaction_status">Статус транзакции</string>
<string name="common_transactions">Транзакции</string>
<string name="common_transfer">Перевод</string>
<string name="common_unable_to_load">Невозможно загрузить данные…</string>
<string name="common_understand">Я понял</string>
<string name="common_unknown_error">Произошла ошибка. Пожалуйста, попробуйте снова.</string>
<string name="common_unreachable">Недоступно</string>
@ -1317,12 +1318,12 @@
<string name="wc_alert_unknown_error_description">Код ошибки: %s. Если проблема сохраняется, обратитесь в нашу службу поддержки.</string>
<string name="wc_alert_unknown_error_description_no_error_code">Если проблема сохраняется, обратитесь в нашу службу поддержки</string>
<string name="wc_alert_unknown_error_title">Мы обнаружили неизвестную ошибку</string>
<string name="wc_alert_unsupported_dapps_description">Кошелек Tangem.в настоящий момент не поддерживает %s</string>
<string name="wc_alert_unsupported_dapps_description">Кошелек Tangem в настоящий момент не поддерживает %s</string>
<string name="wc_alert_unsupported_dapps_title">Неподдерживаемый dApp</string>
<string name="wc_alert_unsupported_method_title">Мы обнаружили неизвестную ошибку</string>
<string name="wc_alert_unsupported_networks_description">Tangem в настоящее время не поддерживает необходимую сеть для %s</string>
<string name="wc_alert_unsupported_networks_title">Неподдерживаемые сети</string>
<string name="wc_alert_verified_domain_description">Tangem поддерживает сеть, необходимую для %s</string>
<string name="wc_alert_verified_domain_description">Этот домен прошёл проверку и считается безопасным, надёжным и свободным от известных угроз или подозрительной активности. %s</string>
<string name="wc_alert_verified_domain_title">Верифицированный домен</string>
<string name="wc_alert_wrong_card_description">Выбрана не верная карта или кольцо</string>
<string name="wc_alert_wrong_card_title">Похоже, возникла проблема</string>
@ -1350,6 +1351,7 @@
<string name="wc_custom_allowance_title">Настраиваемый лимит</string>
<string name="wc_dapp_disconnected">dApp отключен</string>
<string name="wc_disconnect_all">Отключить все</string>
<string name="wc_disconnect_all_alert_desc">Все сессии dApp будут отключены. Ваш кошелёк больше не будет связан ни с одним dApp.</string>
<string name="wc_disconnect_all_alert_title">Отключить все dApp</string>
<string name="wc_errors_invalid_domain_subtitle">Попробуйте соединиться снова, используя новый URI</string>
<string name="wc_errors_invalid_domain_title">Недействительный домен dApp</string>
@ -1384,7 +1386,7 @@
<string name="wc_unlimited_amount">Безлимитное количество</string>
<string name="wc_uri_already_used_description">Убедитесь, что каждая попытка сопряжения использует новый и уникальный URI.</string>
<string name="wc_uri_already_used_title">URI уже используется</string>
<string name="wc_wallet_connect">Подключение кошелька</string>
<string name="wc_wallet_connect">WalletConnect</string>
<string name="wc_warning_transaction">Подозрительная транзакция</string>
<string name="welcome_interrupted_backup_alert_discard">Отказаться</string>
<string name="welcome_interrupted_backup_alert_message">Вы не закончили резервное копирование. Хотите продолжить?</string>

View file

@ -1304,7 +1304,7 @@
<string name="wc_alert_unknown_error_title">Ми зіткнулися з невідомою помилкою</string>
<string name="wc_alert_unsupported_networks_description">Tangem наразі не підтримує необхідну мережу для %s.</string>
<string name="wc_alert_unsupported_networks_title">Непідтримувані мережі</string>
<string name="wc_alert_verified_domain_description">Tangem підтримує мережу, необхідну для %s</string>
<string name="wc_alert_verified_domain_description">Цей домен пройшов перевірку та вважається безпечним, надійним і вільним від відомих загроз чи підозрілої активності. %s</string>
<string name="wc_alert_verified_domain_title">Верифікований домен</string>
<string name="wc_alert_wrong_card_description">Обрана не вірна картка або кільце</string>
<string name="wc_alert_wrong_card_title">Схоже, виникла проблема</string>
@ -1328,7 +1328,7 @@
<string name="wc_copy_data_button_text">Копіювати дані</string>
<string name="wc_dapp_disconnected">dApp відключено</string>
<string name="wc_disconnect_all">Розʼєднати все</string>
<string name="wc_disconnect_all_alert_desc">Відключити всі dApps</string>
<string name="wc_disconnect_all_alert_desc">Усі сесії dApp буде відключено. Ваш гаманець більше не буде пов’язаний із жодним dApp.</string>
<string name="wc_disconnect_all_alert_title">Відключити всі dApps</string>
<string name="wc_errors_invalid_domain_subtitle">Спробуйте ще раз з новим URI</string>
<string name="wc_errors_invalid_domain_title">Недійсний домен dApp</string>

View file

@ -71,8 +71,10 @@
<string name="app_settings_enable_biometrics_description">Go to settings to enable biometric authentication in the Tangem App</string>
<string name="app_settings_enable_biometrics_title">Enable biometric authentication</string>
<string name="app_settings_off_biometrics_alert_message">Disabling %1$s will require you to enter your passcode to unlock the app and to interact with your wallet.</string>
<string name="app_settings_off_require_access_code_alert_message">Youll be asked for your wallets access code later so we can securely store it for future use</string>
<string name="app_settings_off_saved_access_code_alert_message">This will delete all the saved wallet access codes. Any further operation with the wallet will require submitting the access code.</string>
<string name="app_settings_off_saved_wallet_alert_message">Removing the saved devices deletes all the saved wallets and their access codes from the app.</string>
<string name="app_settings_on_require_access_code_alert_message">This will delete all the saved wallet access codes. Any further interaction with the wallet will require submitting the access code.</string>
<string name="app_settings_require_access_code">Require Access Code</string>
<string name="app_settings_require_access_code_footer">This option disables biometric authentication for sensitive actions. You will be required to enter your access code every time, such as when signing a transaction.</string>
<string name="app_settings_saved_access_codes">Save Access Code</string>
@ -297,6 +299,7 @@
<string name="common_transaction_status">Transaction status</string>
<string name="common_transactions">Transactions</string>
<string name="common_transfer">Transfer</string>
<string name="common_unable_to_load">Unable to load the data…</string>
<string name="common_understand">I understand</string>
<string name="common_unknown_error">There was an error. Please try again.</string>
<string name="common_unreachable">Unreachable</string>
@ -1519,7 +1522,7 @@
<string name="wc_alert_unsupported_method_title">We\'ve encountered unknown error</string>
<string name="wc_alert_unsupported_networks_description">Tangem does not currently support a required network by %s.</string>
<string name="wc_alert_unsupported_networks_title">Unsupported networks</string>
<string name="wc_alert_verified_domain_description">Tangem support a required network by %s</string>
<string name="wc_alert_verified_domain_description">This domain has passed verification checks and is considered safe, reputable, and free from known threats or suspicious activity. %s</string>
<string name="wc_alert_verified_domain_title">Verified domain</string>
<string name="wc_alert_wrong_card_description">Wrong card or ring selected in the App</string>
<string name="wc_alert_wrong_card_title">We\'ve got some kind of problem</string>
@ -1547,7 +1550,7 @@
<string name="wc_custom_allowance_title">Custom allowance</string>
<string name="wc_dapp_disconnected">dApp disconnected</string>
<string name="wc_disconnect_all">Disconnect all</string>
<string name="wc_disconnect_all_alert_desc">Text about discnected all dApps</string>
<string name="wc_disconnect_all_alert_desc">All dApp sessions will be disconnected. Your wallet will no longer be linked to any dApps.</string>
<string name="wc_disconnect_all_alert_title">Disconect All dApps</string>
<string name="wc_errors_invalid_domain_subtitle">Try pairing again with a fresh URI</string>
<string name="wc_errors_invalid_domain_title">Invalid dApp domain</string>
@ -1584,7 +1587,7 @@
<string name="wc_unlimited_amount">Unlimited Amount</string>
<string name="wc_uri_already_used_description">Ensure that each pairing attempt uses a fresh and unique URI</string>
<string name="wc_uri_already_used_title">URI already used</string>
<string name="wc_wallet_connect">Wallet connect</string>
<string name="wc_wallet_connect">WalletConnect</string>
<string name="wc_warning_transaction">Suspicious transaction</string>
<string name="welcome_interrupted_backup_alert_discard">Discard</string>
<string name="welcome_interrupted_backup_alert_message">You have an interrupted backup. Do you want to resume?</string>

View file

@ -17,6 +17,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.SEED_FIRST_NOTIFI
import com.tangem.datasource.local.preferences.utils.get
import com.tangem.datasource.local.preferences.utils.getObjectMap
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
import com.tangem.datasource.local.preferences.utils.store
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.models.wallet.UserWallet
@ -47,14 +48,78 @@ internal class DefaultWalletsRepository(
return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false)
}
@Deprecated("Hot wallet feature makes app always save user wallets. Do not use this method")
override fun shouldSaveUserWallets(): Flow<Boolean> {
return appPreferencesStore.get(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false)
}
@Deprecated("Hot wallet feature makes app always save user wallets. Do not use this method")
override suspend fun saveShouldSaveUserWallets(item: Boolean) {
appPreferencesStore.store(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, value = item)
}
override suspend fun useBiometricAuthentication(): Boolean {
val useBiometricAuthentication = appPreferencesStore.getSyncOrNull(
key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY,
)
if (useBiometricAuthentication != null) {
return useBiometricAuthentication
}
val legacySaveWalletsInTheApp = appPreferencesStore.getSyncOrNull(
key = PreferencesKeys.SAVE_USER_WALLETS_KEY,
)
if (legacySaveWalletsInTheApp != null) {
// Migrate legacy setting to new one
appPreferencesStore.store(
key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY,
value = legacySaveWalletsInTheApp,
)
return legacySaveWalletsInTheApp
} else {
// Default value for new users
setUseBiometricAuthentication(false)
return false
}
}
override suspend fun setUseBiometricAuthentication(value: Boolean) {
appPreferencesStore.store(key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY, value = value)
}
override suspend fun requireAccessCode(): Boolean {
val requireAccessCode = appPreferencesStore.getSyncOrNull(
key = PreferencesKeys.REQUIRE_ACCESS_CODE_KEY,
)
if (requireAccessCode != null) {
return requireAccessCode
}
val legacyShouldSaveAccessCode = appPreferencesStore.getSyncOrNull(
key = PreferencesKeys.SHOULD_SAVE_ACCESS_CODES_KEY,
)
if (legacyShouldSaveAccessCode != null) {
// Migrate legacy setting to new one
appPreferencesStore.store(
key = PreferencesKeys.REQUIRE_ACCESS_CODE_KEY,
value = legacyShouldSaveAccessCode.not(),
)
return legacyShouldSaveAccessCode.not()
} else {
// Default value for new users
setRequireAccessCode(true)
return true
}
}
override suspend fun setRequireAccessCode(value: Boolean) {
appPreferencesStore.store(key = PreferencesKeys.REQUIRE_ACCESS_CODE_KEY, value = value)
}
override suspend fun isWalletWithRing(userWalletId: UserWalletId): Boolean {
return appPreferencesStore
.getSyncOrDefault(key = PreferencesKeys.ADDED_WALLETS_WITH_RING_KEY, default = emptySet())

View file

@ -1,7 +1,11 @@
package com.tangem.data.wallets.hot
import com.tangem.common.core.TangemSdkError
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.copy
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.hot.sdk.exception.WrongPasswordException
import com.tangem.hot.sdk.model.*
@ -9,7 +13,9 @@ import javax.inject.Inject
class HotWalletAccessor @Inject constructor(
private val tangemHotSdk: TangemHotSdk,
private val userWalletsListRepository: UserWalletsListRepository,
private val hotWalletPasswordRequester: HotWalletPasswordRequester,
private val walletsRepository: WalletsRepository,
) {
suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List<DataToSign>): List<SignedData> =
@ -23,10 +29,18 @@ class HotWalletAccessor @Inject constructor(
}
private suspend fun <T> hotSdkRequest(hotWalletId: HotWalletId, block: suspend (unlock: UnlockHotWallet) -> T): T {
val isAccessCodeRequired = walletsRepository.requireAccessCode()
val auth = when (hotWalletId.authType) {
HotWalletId.AuthType.NoPassword -> HotAuth.NoAuth
HotWalletId.AuthType.Password -> requestPassword(false)
HotWalletId.AuthType.Biometry -> HotAuth.Biometry
HotWalletId.AuthType.Biometry -> {
if (isAccessCodeRequired) {
requestPassword(false)
} else {
HotAuth.Biometry
}
}
}
return runCatchingSdkErrors(hotWalletId, auth) {
@ -47,20 +61,41 @@ class HotWalletAccessor @Inject constructor(
block = { blockAuth ->
block(blockAuth).also {
// Update biometry auth if the original auth was password
if (blockAuth is HotAuth.Password) {
tangemHotSdk.changeAuth(
unlockHotWallet = UnlockHotWallet(
walletId = hotWalletId,
auth = blockAuth,
),
auth = HotAuth.Biometry,
)
}
updateBiometryAuthIfNeeded(
hotWalletId = hotWalletId,
originalAuth = blockAuth,
)
}
},
)
}
private suspend fun updateBiometryAuthIfNeeded(hotWalletId: HotWalletId, originalAuth: HotAuth) {
val isAccessCodeRequired = walletsRepository.requireAccessCode()
if (originalAuth is HotAuth.Password && isAccessCodeRequired.not()) {
val userWallet = userWalletsListRepository.userWalletsSync()
.find { it is UserWallet.Hot && it.hotWalletId == hotWalletId }
as? UserWallet.Hot
?: return
val newHotWalletId = tangemHotSdk.changeAuth(
unlockHotWallet = UnlockHotWallet(
walletId = hotWalletId,
auth = originalAuth,
),
auth = HotAuth.Biometry,
)
userWalletsListRepository.saveWithoutLock(
userWallet = userWallet.copy(
hotWalletId = newHotWalletId,
),
canOverride = true,
)
}
}
private suspend fun <T> runCatchingWrongPassInternal(
originalAuth: HotAuth,
auth: HotAuth,

View file

@ -73,8 +73,21 @@ interface UserWalletsListRepository {
* If the wallet is not found, it returns [SetLockError.UserWalletNotFound]
* If the wallet is locked, it returns [SetLockError.UserWalletLocked]
* If the lock method is not supported, it returns [SetLockError.UnableToSetLock].
*
* @param userWalletId The ID of the user wallet to set the lock for.
* @param lockMethod The method to use for locking the wallet.
* @param changeUnsecured If false, the method will have no effect on unsecured wallets.
*/
suspend fun setLock(userWalletId: UserWalletId, lockMethod: LockMethod): Either<SetLockError, Unit>
suspend fun setLock(
userWalletId: UserWalletId,
lockMethod: LockMethod,
changeUnsecured: Boolean = true,
): Either<SetLockError, Unit>
/**
* Removes biometric lock for user wallet if it is set.
*/
suspend fun removeBiometricLock(userWalletId: UserWalletId)
/**
* Deletes user wallets by ids.

View file

@ -24,8 +24,10 @@ interface SettingsRepository {
suspend fun setShouldOpenWelcomeScreenOnResume(value: Boolean)
@Deprecated("Use walletsRepository.requireAccessCode instead")
suspend fun shouldSaveAccessCodes(): Boolean
@Deprecated("Use walletsRepository.requireAccessCode instead")
suspend fun setShouldSaveAccessCodes(value: Boolean)
suspend fun incrementAppLaunchCounter()

View file

@ -11,10 +11,20 @@ interface WalletsRepository {
suspend fun shouldSaveUserWalletsSync(): Boolean
@Deprecated("Hot wallet make always save user wallets. Do not use this method")
fun shouldSaveUserWallets(): Flow<Boolean>
@Deprecated("Hot wallet make always save user wallets. Do not use this method")
suspend fun saveShouldSaveUserWallets(item: Boolean)
suspend fun useBiometricAuthentication(): Boolean
suspend fun setUseBiometricAuthentication(value: Boolean)
suspend fun requireAccessCode(): Boolean
suspend fun setRequireAccessCode(value: Boolean)
suspend fun isWalletWithRing(userWalletId: UserWalletId): Boolean
suspend fun setHasWalletsWithRing(userWalletId: UserWalletId)

View file

@ -6,11 +6,12 @@ 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.UserWalletsListError
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.core.wallets.error.SaveWalletError
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.wallets.legacy.UserWalletsListError
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
/**
* Use case for saving user wallet
@ -22,6 +23,7 @@ import com.tangem.domain.core.wallets.UserWalletsListRepository
class SaveWalletUseCase(
private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
private val walletsRepository: WalletsRepository,
private val useNewRepository: Boolean,
) {
@ -35,10 +37,14 @@ class SaveWalletUseCase(
if (newUserWallet) {
when (userWallet) {
is UserWallet.Cold -> {
userWalletsListRepository.setLock(
userWallet.walletId,
UserWalletsListRepository.LockMethod.Biometric,
)
if (walletsRepository.useBiometricAuthentication()) {
userWalletsListRepository.setLock(
userWallet.walletId,
UserWalletsListRepository.LockMethod.Biometric,
)
} else {
Unit.right()
}
}
is UserWallet.Hot -> {
userWalletsListRepository.setLock(

View file

@ -13,6 +13,7 @@ android {
dependencies {
api(projects.features.biometry.api)
implementation(projects.features.hotWallet.api)
/** Core modules */
implementation(projects.core.ui)

View file

@ -13,6 +13,7 @@ import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.settings.SetSaveWalletScreenShownUseCase
import com.tangem.domain.settings.repositories.SettingsRepository
@ -20,6 +21,7 @@ import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.biometry.impl.ui.state.AskBiometryUM
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.delay
@ -45,6 +47,8 @@ internal class AskBiometryModel @Inject constructor(
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val settingsManager: SettingsManager,
private val uiMessageSender: UiMessageSender,
private val userWalletsListRepository: UserWalletsListRepository,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
) : Model() {
private val params = paramsContainer.require<AskBiometryComponent.Params>()
@ -109,10 +113,18 @@ internal class AskBiometryModel @Inject constructor(
walletsRepository.saveShouldSaveUserWallets(item = true)
settingsRepository.setShouldSaveAccessCodes(value = true)
if (userWallet is UserWallet.Cold) {
if (hotWalletFeatureToggles.isHotWalletEnabled) {
walletsRepository.setUseBiometricAuthentication(value = true)
setBiometryLockForAllWallets()
cardSdkConfigRepository.setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = userWallet.hasAccessCode,
isBiometricsRequestPolicy = walletsRepository.requireAccessCode().not(),
)
} else {
if (userWallet is UserWallet.Cold) {
cardSdkConfigRepository.setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = userWallet.hasAccessCode,
)
}
}
if (_uiState.value.bottomSheetVariant) {
@ -123,6 +135,18 @@ internal class AskBiometryModel @Inject constructor(
params.modelCallbacks.onAllowed()
}
private fun setBiometryLockForAllWallets() {
modelScope.launch {
userWalletsListRepository.userWalletsSync().forEach { userWallet ->
userWalletsListRepository.setLock(
userWalletId = userWallet.walletId,
lockMethod = UserWalletsListRepository.LockMethod.Biometric,
changeUnsecured = false,
)
}
}
}
private fun showEnrollBiometricsDialog() {
uiMessageSender.send(
DialogMessage(

View file

@ -8,6 +8,7 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.hotwallet.accesscode.entity.AccessCodeUM
import com.tangem.hot.sdk.TangemHotSdk
@ -28,6 +29,7 @@ internal class AccessCodeModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val userWalletsListRepository: UserWalletsListRepository,
private val walletsRepository: WalletsRepository,
private val tangemHotSdk: TangemHotSdk,
) : Model() {
@ -85,13 +87,15 @@ internal class AccessCodeModel @Inject constructor(
auth = HotAuth.Password(accessCode.toCharArray()),
)
updatedHotWalletId = tangemHotSdk.changeAuth(
unlockHotWallet = UnlockHotWallet(
walletId = updatedHotWalletId,
auth = HotAuth.Password(accessCode.toCharArray()),
),
auth = HotAuth.Biometry,
)
if (walletsRepository.requireAccessCode().not()) {
updatedHotWalletId = tangemHotSdk.changeAuth(
unlockHotWallet = UnlockHotWallet(
walletId = updatedHotWalletId,
auth = HotAuth.Password(accessCode.toCharArray()),
),
auth = HotAuth.Biometry,
)
}
userWalletsListRepository.saveWithoutLock(
userWallet.copy(
@ -106,10 +110,12 @@ internal class AccessCodeModel @Inject constructor(
UserWalletsListRepository.LockMethod.AccessCode(accessCode.toCharArray()),
)
userWalletsListRepository.setLock(
userWallet.walletId,
UserWalletsListRepository.LockMethod.Biometric,
)
if (walletsRepository.useBiometricAuthentication()) {
userWalletsListRepository.setLock(
userWallet.walletId,
UserWalletsListRepository.LockMethod.Biometric,
)
}
params.callbacks.onAccessCodeConfirmed(params.userWalletId)
}.onFailure {

View file

@ -91,7 +91,7 @@ internal class AddExistingWalletImportModel @Inject constructor(
val hotWalletId = tangemHotSdk.importWallet(mnemonic, passphrase?.toCharArray(), HotAuth.NoAuth)
val hotUserWalletBuilder = hotUserWalletBuilderFactory.create(hotWalletId)
val userWallet = hotUserWalletBuilder.build()
saveUserWalletUseCase(userWallet)
saveUserWalletUseCase(userWallet.copy(backedUp = true))
params.callbacks.onWalletImported(userWallet.walletId)
}.onFailure {
Timber.e(it)

View file

@ -14,6 +14,7 @@ import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.core.wallets.error.UnlockWalletError
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.GetIsBiometricsEnabledUseCase
import com.tangem.features.wallet.utils.UserWalletsFetcher
import com.tangem.features.welcome.impl.R
@ -35,6 +36,7 @@ import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped
internal class WelcomeModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
@ -43,6 +45,7 @@ internal class WelcomeModel @Inject constructor(
private val userWalletsFetcherFactory: UserWalletsFetcher.Factory,
private val userWalletsListRepository: UserWalletsListRepository,
private val getIsBiometricsEnabledUseCase: GetIsBiometricsEnabledUseCase,
private val walletsRepository: WalletsRepository,
) : Model() {
// TODO add intent handling
@ -154,8 +157,7 @@ internal class WelcomeModel @Inject constructor(
when (option) {
Create -> router.push(AppRoute.CreateWalletSelection)
Add -> router.push(AppRoute.AddExistingWallet)
Buy -> {
}
Buy -> Unit // TODO
}
}
@ -182,8 +184,8 @@ internal class WelcomeModel @Inject constructor(
unlockWallet(userWallet.walletId, unlockMethod)
}
private fun canUnlockWithBiometrics(): Boolean {
return getIsBiometricsEnabledUseCase.canUseBiometry()
private suspend fun canUnlockWithBiometrics(): Boolean {
return getIsBiometricsEnabledUseCase.canUseBiometry() && walletsRepository.useBiometricAuthentication()
}
suspend fun unlockWallet(userWalletId: UserWalletId, unlockMethod: UserWalletsListRepository.UnlockMethod) {

View file

@ -70,16 +70,18 @@ internal fun AnimatedContentScope.WelcomeSelectWallet(state: WelcomeUM.SelectWal
BottomFade(modifier = Modifier.align(Alignment.BottomCenter))
SecondaryButton(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.padding(16.dp)
.navigationBarsPadding()
.animateEnterExit(fadeIn(), fadeOut()),
text = "Unlock all with biometric",
onClick = state.onUnlockWithBiometricClick,
)
if (state.showUnlockWithBiometricButton) {
SecondaryButton(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.padding(16.dp)
.navigationBarsPadding()
.animateEnterExit(fadeIn(), fadeOut()),
text = "Unlock all with biometric",
onClick = state.onUnlockWithBiometricClick,
)
}
}
LaunchedEffect(state.wallets) {