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

@@ -52,11 +52,12 @@ fun HttpClientConfig<*>.installCommonPlugins() {
gzip()
}
// Task 2: Retry with exponential backoff for server errors and IO exceptions
// Task 2: Retry with exponential backoff for server errors, IO exceptions,
// and 429 rate-limit responses (using the Retry-After header for delay).
install(HttpRequestRetry) {
maxRetries = 3
retryIf { _, response ->
response.status.value in 500..599
response.status.value in 500..599 || response.status.value == 429
}
retryOnExceptionIf { _, cause ->
// Retry on network-level IO errors (connection resets, timeouts, etc.)
@@ -67,8 +68,8 @@ fun HttpClientConfig<*>.installCommonPlugins() {
cause is kotlinx.io.IOException
}
exponentialDelay(
base = 1000.0, // 1 second base
maxDelayMs = 10000 // 10 second max
base = 1000.0,
maxDelayMs = 10_000
)
}

View File

@@ -15,8 +15,19 @@ object ErrorParser {
* Parses error response from the backend.
* The backend returns: {"error": "message"}
* Falls back to detail field or field-specific errors if present.
* For 429 responses, includes the Retry-After value in the message.
*/
suspend fun parseError(response: HttpResponse): String {
// Handle 429 rate-limit responses with Retry-After header
if (response.status.value == 429) {
val retryAfter = response.headers["Retry-After"]?.toLongOrNull()
return if (retryAfter != null) {
"Too many requests. Please try again in $retryAfter seconds."
} else {
"Too many requests. Please try again later."
}
}
return try {
val errorResponse = response.body<ErrorResponse>()