feat: add iOS support and harden receipt scanning
This commit is contained in:
78
app/ios/ScanReceipts/Networking/APIError.swift
Normal file
78
app/ios/ScanReceipts/Networking/APIError.swift
Normal file
@@ -0,0 +1,78 @@
|
||||
import Foundation
|
||||
|
||||
/// A decoded `{ "error": "some_code" }` body from the backend. Every route
|
||||
/// under `src/app/api/**` returns one of these machine-readable codes on
|
||||
/// failure (see `src/lib/auth/errors.ts` for the auth ones) — the app is
|
||||
/// responsible for turning the code into user-facing German copy, the same
|
||||
/// way the web dashboard's client code does.
|
||||
struct APIErrorBody: Decodable {
|
||||
let error: String
|
||||
let retryAfter: Int?
|
||||
}
|
||||
|
||||
enum APIError: Error, LocalizedError {
|
||||
/// HTTP 401 — no/invalid/expired session. Callers should route back to
|
||||
/// the login screen and clear the stored token.
|
||||
case unauthorized
|
||||
/// A recognised `{ error: <code> }` body, with the HTTP status attached
|
||||
/// (403 pro_required, 402 scan_limit_reached, 429 rate_limited, ...).
|
||||
case server(code: String, status: Int, retryAfterSeconds: Int?)
|
||||
/// A non-2xx response with a body that isn't the `{ error }` shape.
|
||||
case unexpectedStatus(Int)
|
||||
case network(Error)
|
||||
case decoding(Error)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .unauthorized:
|
||||
return "Bitte melde dich erneut an."
|
||||
case .server(let code, _, let retryAfter):
|
||||
return APIError.message(forCode: code, retryAfterSeconds: retryAfter)
|
||||
case .unexpectedStatus(let status):
|
||||
return "Unerwartete Antwort vom Server (\(status))."
|
||||
case .network:
|
||||
return "Keine Verbindung zum Server. Prüfe deine Internetverbindung."
|
||||
case .decoding:
|
||||
return "Antwort des Servers konnte nicht gelesen werden."
|
||||
}
|
||||
}
|
||||
|
||||
/// Central place to translate a backend error code into German UI copy.
|
||||
/// Extend this switch as features land — it deliberately mirrors what the
|
||||
/// web app's route handlers can return, not a guess at future codes.
|
||||
static func message(forCode code: String, retryAfterSeconds: Int?) -> String {
|
||||
switch code {
|
||||
case "invalid_credentials":
|
||||
return "E-Mail oder Passwort ist falsch."
|
||||
case "email_not_verified":
|
||||
return "Bitte bestätige zuerst deine E-Mail-Adresse."
|
||||
case "rate_limited":
|
||||
if let seconds = retryAfterSeconds, seconds > 0 {
|
||||
return "Zu viele Versuche. Bitte in \(seconds) Sekunden erneut versuchen."
|
||||
}
|
||||
return "Zu viele Versuche. Bitte später erneut versuchen."
|
||||
case "invalid_email":
|
||||
return "Diese E-Mail-Adresse ist ungültig."
|
||||
case "weak_password":
|
||||
return "Das Passwort ist zu schwach."
|
||||
case "disposable_email":
|
||||
return "Bitte verwende eine reguläre E-Mail-Adresse."
|
||||
case "mail_failed":
|
||||
return "Bestätigungs-E-Mail konnte nicht gesendet werden. Bitte später erneut versuchen."
|
||||
case "database_unavailable":
|
||||
return "Server vorübergehend nicht erreichbar. Bitte später erneut versuchen."
|
||||
case "pro_required":
|
||||
return "Diese Funktion ist Pro-Nutzern vorbehalten."
|
||||
case "scan_limit_reached":
|
||||
return "Dein monatliches Scan-Kontingent ist aufgebraucht."
|
||||
case "daily_scan_limit_reached":
|
||||
return "Tageslimit für Scans erreicht. Bitte morgen erneut versuchen."
|
||||
case "csrf_failed":
|
||||
return "Sicherheitsprüfung fehlgeschlagen. Bitte App neu starten."
|
||||
case "unauthorized":
|
||||
return "Bitte melde dich erneut an."
|
||||
default:
|
||||
return "Etwas ist schiefgelaufen (\(code))."
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user