Updated on 2026-08-14
This commit is contained in:
parent
bb95420224
commit
143dcc5a62
34 changed files with 632 additions and 114 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue