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

@@ -60,6 +60,12 @@ import com.tt.honeyDue.network.APILayer
import com.tt.honeyDue.platform.ContractorImportHandler
import com.tt.honeyDue.platform.PlatformUpgradeScreen
import com.tt.honeyDue.platform.ResidenceImportHandler
import com.tt.honeyDue.storage.BiometricPreference
import com.tt.honeyDue.ui.screens.BiometricLockScreen
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import honeydue.composeapp.generated.resources.Res
import honeydue.composeapp.generated.resources.compose_multiplatform
@@ -82,6 +88,28 @@ fun App(
var hasCompletedOnboarding by remember { mutableStateOf(DataManager.hasCompletedOnboarding.value) }
val navController = rememberNavController()
// Biometric lock state - starts locked if biometric is enabled and user is logged in
var isLocked by rememberSaveable {
mutableStateOf(BiometricPreference.isBiometricEnabled() && DataManager.authToken.value != null)
}
// Lock the app when returning from background
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_STOP) {
// App going to background - lock if biometric is enabled
if (BiometricPreference.isBiometricEnabled() && DataManager.authToken.value != null) {
isLocked = true
}
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
// Handle navigation from notification tap
// Note: The actual navigation to the task column happens in MainScreen -> AllTasksScreen
// We just need to ensure the user is on MainRoute when a task navigation is requested
@@ -156,6 +184,7 @@ fun App(
else -> MainRoute
}
Box(modifier = Modifier.fillMaxSize()) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
@@ -692,6 +721,14 @@ fun App(
}
}
}
// Biometric lock overlay - shown when app is locked
if (isLocked && isLoggedIn && isVerified) {
BiometricLockScreen(
onUnlocked = { isLocked = false }
)
}
} // close Box
}