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 fun sendFeedback(type: String, message: String) {57 if (_feedbackState.value is FeedbackState.Sending) return58 _feedbackState.value = FeedbackState.Sending59 viewModelScope.launch {60 _feedbackState.value = try {61 feedback.submit(type, message)62 FeedbackState.Sent63 } catch (e: com.peachpasswords.feedback.FeedbackUnavailableException) {64 FeedbackState.Error("Feedback unavailable — endpoint not configured")65 } catch (e: Exception) {66 FeedbackState.Error("Couldn't send feedback — check your connection")67 }68 }69 }7071 72 fun resetFeedback() {73 _feedbackState.value = FeedbackState.Idle74 }75 val entries: StateFlow<List<com.peachpasswords.vault.VaultEntry>> = vault.entries76 val settings: StateFlow<VaultSettings> = vault.settings7778 79 private val _isPro = MutableStateFlow(license.isProActive())80 val isPro: StateFlow<Boolean> = _isPro.asStateFlow()8182 private val _activating = MutableStateFlow(false)83 val activating: StateFlow<Boolean> = _activating.asStateFlow()8485 private val _activationError = MutableStateFlow<String?>(null)86 val activationError: StateFlow<String?> = _activationError.asStateFlow()8788 89 private val _showCelebration = MutableStateFlow(false)90 val showCelebration: StateFlow<Boolean> = _showCelebration.asStateFlow()9192 9394959697 fun activateLicense(rawKey: String) {98 if (_activating.value) return99 _activating.value = true100 _activationError.value = null101 viewModelScope.launch {102 val key = rawKey.trim()103 if (license.validateLicense(key)) {104 license.storeLicense(key)105 _isPro.value = true106 _showCelebration.value = true107 } else {108 _activationError.value = "Invalid license key"109 }110 _activating.value = false111 }112 }113114 fun clearActivationError() { _activationError.value = null }115116 fun dismissCelebration() {117 license.markProCelebrated()118 _showCelebration.value = false119 }120121 fun deactivate() {122 license.clearLicense()123 _isPro.value = false124 }125126 private fun mutate(block: (VaultSettings) -> VaultSettings) {127 viewModelScope.launch { vault.updateSettings(block(vault.settings.value)) }128 }129130 fun setTheme(theme: String) = mutate { it.copy(theme = theme) }131 fun setAccent(name: String) = mutate { it.copy(accentColor = name) }132 fun setTextSize(size: String) = mutate { it.copy(textSize = size) }133 fun setReduceMotion(on: Boolean) = mutate { it.copy(reduceMotion = on) }134 fun setBreachMonitoring(on: Boolean) = mutate { it.copy(breachMonitoring = on) }135 fun setBiometrics(on: Boolean) = mutate { it.copy(biometricsEnabled = on) }136137 138139140141142143 fun enableBiometrics(activity: FragmentActivity) {144 val key = vault.currentVaultKey() ?: return145 val cipher = try {146 BiometricKeyStore.encryptCipher()147 } catch (_: Exception) {148 VaultCrypto.zero(key); return149 }150 Biometrics.authenticateCrypto(151 activity = activity,152 title = "Enable biometric unlock",153 subtitle = "Confirm to secure your vault key",154 crypto = BiometricPrompt.CryptoObject(cipher),155 onSuccess = { result ->156 val used = result.cryptoObject?.cipher157 if (used != null) {158 runCatching { BiometricKeyStore.wrapAndStore(appContext, used, key) }159 .onSuccess { mutate { it.copy(biometricsEnabled = true) } }160 }161 VaultCrypto.zero(key)162 },163 onFail = { VaultCrypto.zero(key) },164 )165 }166167 168 fun disableBiometrics() {169 BiometricKeyStore.clear(appContext)170 mutate { it.copy(biometricsEnabled = false) }171 }172 fun setAutoLockMinutes(min: Int?) = mutate { it.copy(autoLockMinutes = min) }173 fun setTrashRetentionDays(days: Int?) = mutate { it.copy(trashRetentionDays = days) }174175 suspend fun changePassword(current: String, next: String): Boolean =176 vault.changePassword(current, next)177178 179 fun lede(id: CardId, entriesCount: Int, s: VaultSettings): String = when (id) {180 CardId.Pro -> if (_isPro.value) "active · paid once, yours forever" else "$39 once · yours forever"181 CardId.Protection ->182 "${if (s.breachMonitoring) "breaches watched" else "breach watch off"} · manual lock"183 CardId.Vault ->184 "$entriesCount ${if (entriesCount == 1) "entry" else "entries"} · backups, history, cleanup"185 CardId.Sync -> if (_isPro.value) "pair by chirp, keep in step" else "Pro · encrypted device pairing"186 CardId.Appearance -> "${s.theme} · ${s.accentColor} · ${s.textSize}"187 CardId.Shortcuts -> "bindings · what your fingers know"188 CardId.Feedback -> "tell us what would make it better"189 }190}