diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index ed9d07c577..aa3d4f2a9a 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index ef784f8347..09c556cae1 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -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, ), ), ) diff --git a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt index 15530c62b3..f944a89717 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt @@ -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 { + 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, diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index 5b21957999..44dd702fc3 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -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, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt index f6323a20de..d8173d8bc7 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt @@ -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 ) } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt index 6d907c384b..d900b3f687 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt @@ -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, private val savePersistentInformation: ProviderSuspend, + private val appPreferencesStore: AppPreferencesStore, ) : UserWalletsListRepository { override val userWallets = MutableStateFlow?>(null) @@ -129,38 +133,46 @@ internal class DefaultUserWalletsListRepository( userWallet } - override suspend fun setLock(userWalletId: UserWalletId, lockMethod: LockMethod): Either = - either { - val userWallet = userWallets.value?.find { it.walletId == userWalletId } - ?: raise(SetLockError.UserWalletNotFound) + override suspend fun setLock( + userWalletId: UserWalletId, + lockMethod: LockMethod, + changeUnsecured: Boolean, + ): Either = 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): Either = 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, ): Either { 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 * diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt index 43a5b4916f..104cfc0945 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt @@ -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 = withContext(dispatchers.io) { getUserWalletsIds().mapNotNull { userWalletId -> secureStorage.get(account = StorageKey.UserWalletEncryptionKeyUnsecured(userWalletId).name).decodeToKey() diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 3aece96fa9..9281cbc507 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -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 { diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index a6f2b28868..e783c18e37 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -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( diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt index bc7f17a7b6..980cacb286 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt @@ -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, } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt index 43e66b065a..73459bc7b9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt @@ -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, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt index 47bbb8376f..bde3cf116f 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt @@ -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" } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt index ee93849f31..8b9f0e5a01 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt @@ -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 { 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, diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index 1c696ac842..e0b2794648 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -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 \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index a737416dd5..6c0e164b4e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -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 { diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 554f254397..38278a60f0 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -947,9 +947,13 @@ Die Gebühr geht über die Bilanz hinaus Der Gesamtbetrag geht über die Bilanz hinaus Tauschen und senden + Mit der Konvertierung fortfahren? Dadurch werden Deine vorherigen Daten gelöscht. + Das Senden einer anderen Währung führt zu deren unwiderruflichem Verlust. + Wähle das richtige Empfängernetzwerk Sende uns ein Token, und wir konvertieren es unterwegs. Dein Empfänger erhält genau das, was er braucht – nahtlos. Wird an den Empfänger gesendet Zu erhaltender Betrag + Möchtest Du die Konvertierung wirklich abbrechen? Deine bisherigen Daten werden gelöscht. Senden mit Swap Transaktion gesendet Bereite das Scannen der Karte oder Ring vor, die du einrichten möchtest. @@ -1408,7 +1412,7 @@ Wir haben einen unbekannten Fehler festgestellt. Tangem unterstützt derzeit das erforderliches Netzwerk von %s nicht. Nicht unterstützte Netzwerke - Tangem unterstützt ein erforderliches Netzwerk um %s + Diese Domain hat die Überprüfungen bestanden und gilt als sicher, vertrauenswürdig und frei von bekannten Bedrohungen oder verdächtigen Aktivitäten. %s Verifizierte Domain Falsche Karte oder falscher Ring in der App ausgewählt Wir haben eine Art Problem @@ -1432,7 +1436,7 @@ Daten kopieren Benutzerdefinierter Freibetrag Alle trennen - Text über die Trennung aller dApps + Alle dApp-Sitzungen werden getrennt. Ihre Wallet wird nicht mehr mit dApps verbunden sein. Alle dApps trennen Versuchen Sie erneut, mit einer neuen URI zu koppeln Ungültige dApp-Domain diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index b5eeeccbc8..3810328a13 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -1354,7 +1354,7 @@ Hemos encontrado un error desconocido Actualmente, Tangem no es compatible con una red requerida por %s. Redes no compatibles - Tangem soporta una red requerida por %s + Este dominio ha superado las verificaciones y se considera seguro, confiable y libre de amenazas conocidas o actividades sospechosas. %s Dominio verificado Se seleccionó una tarjeta o un anillo incorrectos en la app Tenemos algún tipo de problema @@ -1382,7 +1382,7 @@ Asignación personalizada dApp desconectada Desconectar todo - Texto sobre desconexión de todas las dApps + Todas las sesiones de dApp se desconectarán. Su billetera ya no estará vinculada a ninguna dApp. Desconectar todas las dApps Intente emparejar nuevamente con una URI nueva Dominio de dApp no válido @@ -1418,7 +1418,7 @@ Cantidad ilimitada Asegúrese de que cada intento de emparejamiento utiliza un URI nuevo y único URI ya utilizado - Wallet connect + WalletConnect Transacción sospechosa Ignorar Tiene un backup interrumpido. ¿Quiere reanudarlo? diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 8e7d9898e8..900aa6723d 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -1324,6 +1324,7 @@ dApp non prise en charge Code d\'erreur : 8 005. Si le problème persiste, n\'hésitez pas à contacter notre support. Nous avons rencontré une erreur inconnue + Ce domaine a passé les contrôles de vérification et est considéré comme sûr, fiable et exempt de menaces connues ou d’activités suspectes.%s Autoriser à dépenser Adresse Chargement @@ -1333,7 +1334,7 @@ Contenu Copier les données Déconnecter tout - Texte sur la déconnexion de toutes les dApps + Toutes les sessions dApp seront déconnectées. Votre portefeuille ne sera plus lié à aucune dApp. Déconnecter toutes les dApps Essayez de jumeler à nouveau avec un nouvel URI Domaine dApp invalide diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 2986582d9d..a66d2ba966 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -12,10 +12,13 @@ ウォレットのロックを解除するには、 %s桁のアクセスコードを設定します。 アクセスコードの作成 アクセスコード + 回復する + アーカイブ済み アカウントをアーカイブする アーカイブ このアカウントをアーカイブしますが、いつでも復元できます。 アカウント + アカウント番号%s — アドレス導出に使用されます。 アカウントを追加 保存 アカウント名 @@ -287,6 +290,7 @@ 取引状況 取引 送金 + データを読み込めません… わかりました エラーが発生しました。もう一度お試しください。 アクセスできません @@ -984,10 +988,13 @@ スワップして送信 変換を続行しますか? これにより以前のデータは消去されます。 変換を確定 + その他の通貨を送信すると、取り返しのつかない損失が発生します。 + 正しい受信者ネットワークを選択してください トークンを送信すれば、送信中に変換されます。受信者は必要なものをシームレスに受け取ります。 受信者は受け取ります 受取人へ 受取金額 + 受信者は%sを取得します 変換をキャンセルしてもよろしいですか?以前のデータは消去されます。 変換を削除 スワップして送信 @@ -1448,7 +1455,7 @@ 不明なエラーが発生しました Tangemは現在%sで必要なネットワークをサポートしていません。 未対応のネットワーク - Tangemは%sで必要なネットワークをサポートします + このドメインは検証チェックに合格しており、安全で信頼でき、既知の脅威や不審な活動がないと判断されています。%s 検証済みドメイン アプリで間違ったカードまたはリングが選択されました 問題が起きています @@ -1476,7 +1483,7 @@ 使用可能量の設定 dAppが接続解除されました すべての接続を解除する - すべてのdAppsの接続解除に関するテキスト + すべてのdAppセッションが切断されます。ウォレットはどのdAppにも接続されなくなります。 すべてのdAppを接続解除する 新しいURIで、再度ペアリングを試してください 無効なdAppドメイン @@ -1512,7 +1519,7 @@ 無制限 各ペアリング試行で、新しくユニークなURIが使用されていることを確認します URIはすでに使用されています - ウォレットコネクト + WalletConnect 不審な取引 破棄 バックアップが中断されました。再開しますか? diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 32374c0f84..f9fdb1b3f2 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -231,6 +231,7 @@ Статус транзакции Транзакции Перевод + Невозможно загрузить данные… Я понял Произошла ошибка. Пожалуйста, попробуйте снова. Недоступно @@ -1317,12 +1318,12 @@ Код ошибки: %s. Если проблема сохраняется, обратитесь в нашу службу поддержки. Если проблема сохраняется, обратитесь в нашу службу поддержки Мы обнаружили неизвестную ошибку - Кошелек Tangem.в настоящий момент не поддерживает %s + Кошелек Tangem в настоящий момент не поддерживает %s Неподдерживаемый dApp Мы обнаружили неизвестную ошибку Tangem в настоящее время не поддерживает необходимую сеть для %s Неподдерживаемые сети - Tangem поддерживает сеть, необходимую для %s + Этот домен прошёл проверку и считается безопасным, надёжным и свободным от известных угроз или подозрительной активности. %s Верифицированный домен Выбрана не верная карта или кольцо Похоже, возникла проблема @@ -1350,6 +1351,7 @@ Настраиваемый лимит dApp отключен Отключить все + Все сессии dApp будут отключены. Ваш кошелёк больше не будет связан ни с одним dApp. Отключить все dApp Попробуйте соединиться снова, используя новый URI Недействительный домен dApp @@ -1384,7 +1386,7 @@ Безлимитное количество Убедитесь, что каждая попытка сопряжения использует новый и уникальный URI. URI уже используется - Подключение кошелька + WalletConnect Подозрительная транзакция Отказаться Вы не закончили резервное копирование. Хотите продолжить? diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 0b17da86ca..952d5a7dd6 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -1304,7 +1304,7 @@ Ми зіткнулися з невідомою помилкою Tangem наразі не підтримує необхідну мережу для %s. Непідтримувані мережі - Tangem підтримує мережу, необхідну для %s + Цей домен пройшов перевірку та вважається безпечним, надійним і вільним від відомих загроз чи підозрілої активності. %s Верифікований домен Обрана не вірна картка або кільце Схоже, виникла проблема @@ -1328,7 +1328,7 @@ Копіювати дані dApp відключено Розʼєднати все - Відключити всі dApps + Усі сесії dApp буде відключено. Ваш гаманець більше не буде пов’язаний із жодним dApp. Відключити всі dApps Спробуйте ще раз з новим URI Недійсний домен dApp diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index f0624f96ea..a76469f95b 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -71,8 +71,10 @@ Go to settings to enable biometric authentication in the Tangem App Enable biometric authentication Disabling %1$s will require you to enter your passcode to unlock the app and to interact with your wallet. + You’ll be asked for your wallet’s access code later so we can securely store it for future use This will delete all the saved wallet access codes. Any further operation with the wallet will require submitting the access code. Removing the saved devices deletes all the saved wallets and their access codes from the app. + This will delete all the saved wallet access codes. Any further interaction with the wallet will require submitting the access code. Require Access Code 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. Save Access Code @@ -297,6 +299,7 @@ Transaction status Transactions Transfer + Unable to load the data… I understand There was an error. Please try again. Unreachable @@ -1519,7 +1522,7 @@ We\'ve encountered unknown error Tangem does not currently support a required network by %s. Unsupported networks - Tangem support a required network by %s + This domain has passed verification checks and is considered safe, reputable, and free from known threats or suspicious activity. %s Verified domain Wrong card or ring selected in the App We\'ve got some kind of problem @@ -1547,7 +1550,7 @@ Custom allowance dApp disconnected Disconnect all - Text about discnected all dApps + All dApp sessions will be disconnected. Your wallet will no longer be linked to any dApps. Disconect All dApps Try pairing again with a fresh URI Invalid dApp domain @@ -1584,7 +1587,7 @@ Unlimited Amount Ensure that each pairing attempt uses a fresh and unique URI URI already used - Wallet connect + WalletConnect Suspicious transaction Discard You have an interrupted backup. Do you want to resume? diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt index 8808bd336a..2127d9b80b 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt @@ -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 { 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()) diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt index 9c26e8a3ee..b53cd193d1 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt @@ -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): List = @@ -23,10 +29,18 @@ class HotWalletAccessor @Inject constructor( } private suspend fun 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 runCatchingWrongPassInternal( originalAuth: HotAuth, auth: HotAuth, diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/UserWalletsListRepository.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/UserWalletsListRepository.kt index f0f9ee988a..fbcfeb9a0c 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/UserWalletsListRepository.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/UserWalletsListRepository.kt @@ -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 + suspend fun setLock( + userWalletId: UserWalletId, + lockMethod: LockMethod, + changeUnsecured: Boolean = true, + ): Either + + /** + * Removes biometric lock for user wallet if it is set. + */ + suspend fun removeBiometricLock(userWalletId: UserWalletId) /** * Deletes user wallets by ids. diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt index 1a64989894..dd8608db95 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt @@ -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() diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt index 5a6c051057..a1eb1f0cd8 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt @@ -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 + @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) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt index 23b10bf3c9..7e328ae146 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt @@ -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( diff --git a/features/biometry/impl/build.gradle.kts b/features/biometry/impl/build.gradle.kts index 4ee4b4358b..def97bf1f3 100644 --- a/features/biometry/impl/build.gradle.kts +++ b/features/biometry/impl/build.gradle.kts @@ -13,6 +13,7 @@ android { dependencies { api(projects.features.biometry.api) + implementation(projects.features.hotWallet.api) /** Core modules */ implementation(projects.core.ui) diff --git a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt index 0713f99e5b..614b462e24 100644 --- a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt +++ b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt @@ -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() @@ -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( diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt index 373af1cf05..2db69f48ee 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt @@ -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 { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt index 29196737e8..d495361f44 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt @@ -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) diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt index b81a8806c5..0c6739540e 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt @@ -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) { diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt index b24c0a7b55..305b00750c 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt @@ -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) {