1package com.peachpasswords.ui.settings23import android.content.Context4import androidx.biometric.BiometricPrompt5import androidx.fragment.app.FragmentActivity6import androidx.lifecycle.ViewModel7import androidx.lifecycle.viewModelScope8import com.peachpasswords.feedback.FeedbackRepository9import com.peachpasswords.feedback.FeedbackState10import com.peachpasswords.license.LicenseRepository11import com.peachpasswords.ui.lock.BiometricKeyStore12import com.peachpasswords.ui.lock.Biometrics13import com.peachpasswords.vault.VaultCrypto14import com.peachpasswords.vault.VaultRepository15import com.peachpasswords.vault.VaultSettings16import dagger.hilt.android.lifecycle.HiltViewModel17import dagger.hilt.android.qualifiers.ApplicationContext18import kotlinx.coroutines.flow.MutableStateFlow19import kotlinx.coroutines.flow.StateFlow20import kotlinx.coroutines.flow.asStateFlow21import kotlinx.coroutines.launch22import javax.inject.Inject2324@HiltViewModel25class SettingsViewModel @Inject constructor(26 private val vault: VaultRepository,27 private val license: LicenseRepository,28 private val feedback: FeedbackRepository,29 private val duress: com.peachpasswords.vault.DuressConfigStore,30 @ApplicationContext private val appContext: Context,31) : ViewModel() {3233 private val _duressConfigured = MutableStateFlow(duress.isConfigured())34 val duressConfigured: StateFlow<Boolean> = _duressConfigured.asStateFlow()3536 37383940 suspend fun saveDuress(password: String, action: com.peachpasswords.vault.DuressAction): Boolean {41 if (!vault.setDuressPassphrase(password)) return false42 val ok = duress.enable(action)43 if (ok) _duressConfigured.value = true44 return ok45 }4647 fun disableDuress() {48 viewModelScope.launch { vault.clearDuress() }49 duress.clear()50 _duressConfigured.value = false51 }5253 private val _feedbackState = MutableStateFlow<FeedbackState>(FeedbackState.Idle)54 val feedbackState: StateFlow<FeedbackState> = _feedbackState.asStateFlow()5556 private val _dedupCount = MutableStateFlow<Int?>(null)57 val dedupCount: StateFlow<Int?> = _dedupCount.asStateFlow()5859 private val _dedupRunning = MutableStateFlow(false)60 val dedupRunning: StateFlow<Boolean> = _dedupRunning.asStateFlow()6162 private val _dedupDone = MutableStateFlow<Int?>(null)63 val dedupDone: StateFlow<Int?> = _dedupDone.asStateFlow()6465 fun sendFeedback(type: String, message: String) {66 if (_feedbackState.value is FeedbackState.Sending) return67 _feedbackState.value = FeedbackState.Sending68 viewModelScope.launch {69 _feedbackState.value = try {70 feedback.submit(type, message)71 FeedbackState.Sent72 } catch (e: com.peachpasswords.feedback.FeedbackUnavailableException) {73 FeedbackState.Error("Feedback unavailable — endpoint not configured")74 } catch (e: Exception) {75 FeedbackState.Error("Couldn't send feedback — check your connection")76 }77 }78 }7980 81 fun resetFeedback() {82 _feedbackState.value = FeedbackState.Idle83 }8485 fun scanDuplicates() {86 if (_dedupRunning.value) return87 _dedupRunning.value = true88 _dedupDone.value = null89 viewModelScope.launch {90 _dedupCount.value = runCatching { vault.countDuplicateEntries() }.getOrDefault(0)91 _dedupRunning.value = false92 }93 }9495 fun cancelDuplicateRemoval() {96 if (_dedupRunning.value) return97 _dedupCount.value = null98 }99100 fun removeDuplicates() {101 if (_dedupRunning.value) return102 _dedupRunning.value = true103 viewModelScope.launch {104 val removed = runCatching { vault.deduplicateEntries() }.getOrDefault(0)105 _dedupDone.value = removed106 _dedupCount.value = 0107 _dedupRunning.value = false108 }109 }110111 val entries: StateFlow<List<com.peachpasswords.vault.VaultEntry>> = vault.entries112 val settings: StateFlow<VaultSettings> = vault.settings113114 115 private val _isPro = MutableStateFlow(license.isProActive())116 val isPro: StateFlow<Boolean> = _isPro.asStateFlow()117118 private val _activating = MutableStateFlow(false)119 val activating: StateFlow<Boolean> = _activating.asStateFlow()120121 private val _activationError = MutableStateFlow<String?>(null)122 val activationError: StateFlow<String?> = _activationError.asStateFlow()123124 125 private val _showCelebration = MutableStateFlow(false)126 val showCelebration: StateFlow<Boolean> = _showCelebration.asStateFlow()127128 129130131132133 fun activateLicense(rawKey: String) {134 if (_activating.value) return135 _activating.value = true136 _activationError.value = null137 viewModelScope.launch {138 val key = rawKey.trim()139 if (license.validateLicense(key)) {140 license.storeLicense(key)141 _isPro.value = true142 _showCelebration.value = true143 } else {144 _activationError.value = "Invalid license key"145 }146 _activating.value = false147 }148 }149150 fun clearActivationError() { _activationError.value = null }151152 fun dismissCelebration() {153 license.markProCelebrated()154 _showCelebration.value = false155 }156157 fun deactivate() {158 license.clearLicense()159 _isPro.value = false160 }161162 private fun mutate(block: (VaultSettings) -> VaultSettings) {163 viewModelScope.launch { vault.updateSettings(block(vault.settings.value)) }164 }165166 fun setTheme(theme: String) = mutate { it.copy(theme = theme) }167 fun setAccent(name: String) = mutate { it.copy(accentColor = name) }168 fun setTextSize(size: String) = mutate { it.copy(textSize = size) }169 fun setReduceMotion(on: Boolean) = mutate { it.copy(reduceMotion = on) }170 fun setBreachMonitoring(on: Boolean) = mutate { it.copy(breachMonitoring = on) }171 fun setBiometrics(on: Boolean) = mutate { it.copy(biometricsEnabled = on) }172173 174175176177178179 fun enableBiometrics(activity: FragmentActivity) {180 val key = vault.currentVaultKey() ?: return181 val cipher = try {182 BiometricKeyStore.encryptCipher()183 } catch (_: Exception) {184 VaultCrypto.zero(key); return185 }186 Biometrics.authenticateCrypto(187 activity = activity,188 title = "Enable biometric unlock",189 subtitle = "Confirm to secure your vault key",190 crypto = BiometricPrompt.CryptoObject(cipher),191 onSuccess = { result ->192 val used = result.cryptoObject?.cipher193 if (used != null) {194 runCatching { BiometricKeyStore.wrapAndStore(appContext, used, key) }195 .onSuccess { mutate { it.copy(biometricsEnabled = true) } }196 }197 VaultCrypto.zero(key)198 },199 onFail = { VaultCrypto.zero(key) },200 )201 }202203 204 fun disableBiometrics() {205 BiometricKeyStore.clear(appContext)206 mutate { it.copy(biometricsEnabled = false) }207 }208 fun setAutoLockMinutes(min: Int?) = mutate { it.copy(autoLockMinutes = min) }209 fun setTrashRetentionDays(days: Int?) = mutate { it.copy(trashRetentionDays = days) }210211 suspend fun changePassword(current: String, next: String): Boolean =212 vault.changePassword(current, next)213214 215 fun lede(id: CardId, entriesCount: Int, s: VaultSettings): String = when (id) {216 CardId.Pro -> if (_isPro.value) "active · paid once, yours forever" else "$39 once · yours forever"217 CardId.Protection ->218 "${if (s.breachMonitoring) "breaches watched" else "breach watch off"} · manual lock"219 CardId.Vault ->220 "$entriesCount ${if (entriesCount == 1) "entry" else "entries"} · backups, history, cleanup"221 CardId.Sync -> if (_isPro.value) "pair by chirp, keep in step" else "Pro · encrypted device pairing"222 CardId.Appearance -> "${s.theme} · ${s.accentColor} · ${s.textSize}"223 CardId.Shortcuts -> "bindings · what your fingers know"224 CardId.Feedback -> "tell us what would make it better"225 }226}