Add biometric lock and rate limit handling

Biometric lock: opt-in Face ID/Touch ID/fingerprint app lock with toggle
in ProfileScreen. Locks on background, requires auth on foreground return.
Platform implementations: BiometricPrompt (Android), LAContext (iOS).

Rate limit: 429 responses parsed with Retry-After header, user-friendly
error messages in all 10 locales, retry plugin respects 429.
ErrorMessageParser updated for both iOS Swift and KMM.
This commit is contained in:
Trey T
2026-03-26 14:37:04 -05:00
parent 334767cee7
commit 0d80df07f6
31 changed files with 871 additions and 7 deletions

View File

@@ -5,6 +5,8 @@ import com.tt.honeyDue.storage.TokenManager
import com.tt.honeyDue.storage.TokenStorage
import com.tt.honeyDue.storage.TaskCacheManager
import com.tt.honeyDue.storage.TaskCacheStorage
import com.tt.honeyDue.storage.BiometricPreference
import com.tt.honeyDue.storage.BiometricPreferenceManager
import com.tt.honeyDue.storage.ThemeStorage
import com.tt.honeyDue.storage.ThemeStorageManager
import com.tt.honeyDue.ui.theme.ThemeManager
@@ -20,5 +22,8 @@ fun MainViewController() = ComposeUIViewController {
ThemeStorage.initialize(ThemeStorageManager.getInstance())
ThemeManager.initialize()
// Initialize BiometricPreference storage
BiometricPreference.initialize(BiometricPreferenceManager.getInstance())
App()
}

View File

@@ -0,0 +1,57 @@
package com.tt.honeyDue.platform
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import kotlinx.cinterop.ExperimentalForeignApi
import platform.LocalAuthentication.LAContext
import platform.LocalAuthentication.LAPolicyDeviceOwnerAuthenticationWithBiometrics
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
/**
* iOS implementation of biometric authentication using LocalAuthentication framework (LAContext).
*/
@OptIn(ExperimentalForeignApi::class)
class IOSBiometricAuthPerformer : BiometricAuthPerformer {
override fun isBiometricAvailable(): Boolean {
val context = LAContext()
return context.canEvaluatePolicy(
LAPolicyDeviceOwnerAuthenticationWithBiometrics,
error = null
)
}
override fun authenticate(
title: String,
subtitle: String,
onResult: (BiometricResult) -> Unit
) {
if (!isBiometricAvailable()) {
onResult(BiometricResult.NotAvailable)
return
}
val context = LAContext()
context.localizedFallbackTitle = ""
context.evaluatePolicy(
LAPolicyDeviceOwnerAuthenticationWithBiometrics,
localizedReason = subtitle
) { success, error ->
MainScope().launch {
if (success) {
onResult(BiometricResult.Success)
} else {
val message = error?.localizedDescription ?: "Authentication failed"
onResult(BiometricResult.Failed(message))
}
}
}
}
}
@Composable
actual fun rememberBiometricAuth(): BiometricAuthPerformer {
return remember { IOSBiometricAuthPerformer() }
}

View File

@@ -0,0 +1,28 @@
package com.tt.honeyDue.storage
import platform.Foundation.NSUserDefaults
/**
* iOS implementation of biometric preference storage using NSUserDefaults.
* Follows the same pattern as ThemeStorageManager.ios.kt.
*/
actual class BiometricPreferenceManager {
private val defaults = NSUserDefaults.standardUserDefaults
actual fun isBiometricEnabled(): Boolean {
return defaults.boolForKey(KEY_BIOMETRIC_ENABLED)
}
actual fun setBiometricEnabled(enabled: Boolean) {
defaults.setBool(enabled, forKey = KEY_BIOMETRIC_ENABLED)
defaults.synchronize()
}
companion object {
private const val KEY_BIOMETRIC_ENABLED = "biometric_enabled"
private val instance by lazy { BiometricPreferenceManager() }
fun getInstance(): BiometricPreferenceManager = instance
}
}