Fix TokenStorage stale cache bug and add user-friendly error messages
- Fix TokenStorage.getToken() returning stale cached token after login/logout - Add comprehensive ErrorMessageParser with 80+ error code mappings - Add Suite9 and Suite10 UI test files for E2E integration testing - Fix accessibility identifiers in RegisterView and ResidenceFormView - Fix UITestHelpers logout to target alert button specifically - Update various UI components with proper accessibility identifiers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,31 +3,157 @@ import Foundation
|
||||
/// Utility for parsing and cleaning error messages from API responses and network errors
|
||||
enum ErrorMessageParser {
|
||||
|
||||
// Network/connection error patterns to detect
|
||||
private static let networkErrorPatterns: [(pattern: String, message: String)] = [
|
||||
("Could not connect to the server", "Unable to connect to the server. Please check your internet connection."),
|
||||
("NSURLErrorDomain", "Unable to connect to the server. Please check your internet connection."),
|
||||
("The Internet connection appears to be offline", "No internet connection. Please check your network settings."),
|
||||
("A server with the specified hostname could not be found", "Unable to connect to the server. Please check your internet connection."),
|
||||
("The request timed out", "Request timed out. Please try again."),
|
||||
("The network connection was lost", "Connection was interrupted. Please try again."),
|
||||
("An SSL error has occurred", "Secure connection failed. Please try again."),
|
||||
("CFNetwork", "Unable to connect to the server. Please check your internet connection."),
|
||||
("kCFStreamError", "Unable to connect to the server. Please check your internet connection."),
|
||||
("Code=-1004", "Unable to connect to the server. Please check your internet connection."),
|
||||
("Code=-1009", "No internet connection. Please check your network settings."),
|
||||
("Code=-1001", "Request timed out. Please try again."),
|
||||
("Code=-1003", "Unable to connect to the server. Please check your internet connection."),
|
||||
("Code=-1005", "Connection was interrupted. Please try again."),
|
||||
("Code=-1200", "Secure connection failed. Please try again."),
|
||||
("UnresolvedAddressException", "Unable to connect to the server. Please check your internet connection."),
|
||||
("ConnectException", "Unable to connect to the server. Please check your internet connection."),
|
||||
("SocketTimeoutException", "Request timed out. Please try again."),
|
||||
("Connection refused", "Unable to connect to the server. The server may be down."),
|
||||
("Connection reset", "Connection was interrupted. Please try again.")
|
||||
// MARK: - API Error Code Mappings
|
||||
|
||||
/// Maps backend error codes to user-friendly messages
|
||||
private static let errorCodeMappings: [String: String] = [
|
||||
// Authentication errors
|
||||
"error.invalid_credentials": "Invalid username or password. Please try again.",
|
||||
"error.invalid_token": "Your session has expired. Please log in again.",
|
||||
"error.not_authenticated": "Please log in to continue.",
|
||||
"error.account_inactive": "Your account is inactive. Please contact support.",
|
||||
"error.username_taken": "This username is already taken. Please choose another.",
|
||||
"error.email_taken": "This email is already registered. Try logging in instead.",
|
||||
"error.email_already_taken": "This email is already in use.",
|
||||
"error.registration_failed": "Registration failed. Please try again.",
|
||||
"error.failed_to_get_user": "Unable to load your profile. Please try again.",
|
||||
"error.failed_to_update_profile": "Unable to update your profile. Please try again.",
|
||||
|
||||
// Email verification errors
|
||||
"error.invalid_verification_code": "Invalid verification code. Please check and try again.",
|
||||
"error.verification_code_expired": "Your verification code has expired. Please request a new one.",
|
||||
"error.email_already_verified": "Your email is already verified.",
|
||||
"error.verification_failed": "Verification failed. Please try again.",
|
||||
"error.failed_to_resend_verification": "Unable to send verification code. Please try again.",
|
||||
|
||||
// Password reset errors
|
||||
"error.rate_limit_exceeded": "Too many attempts. Please wait a few minutes and try again.",
|
||||
"error.too_many_attempts": "Too many attempts. Please request a new code.",
|
||||
"error.invalid_reset_token": "This reset link has expired. Please request a new one.",
|
||||
"error.password_reset_failed": "Password reset failed. Please try again.",
|
||||
|
||||
// Social sign-in errors
|
||||
"error.apple_signin_not_configured": "Apple Sign In is not available. Please use email login.",
|
||||
"error.apple_signin_failed": "Apple Sign In failed. Please try again.",
|
||||
"error.invalid_apple_token": "Apple Sign In failed. Please try again.",
|
||||
"error.google_signin_not_configured": "Google Sign In is not available. Please use email login.",
|
||||
"error.google_signin_failed": "Google Sign In failed. Please try again.",
|
||||
"error.invalid_google_token": "Google Sign In failed. Please try again.",
|
||||
|
||||
// Resource not found errors
|
||||
"error.task_not_found": "Task not found. It may have been deleted.",
|
||||
"error.residence_not_found": "Property not found. It may have been deleted.",
|
||||
"error.contractor_not_found": "Contractor not found. It may have been deleted.",
|
||||
"error.document_not_found": "Document not found. It may have been deleted.",
|
||||
"error.completion_not_found": "Task completion not found.",
|
||||
"error.user_not_found": "User not found.",
|
||||
"error.notification_not_found": "Notification not found.",
|
||||
"error.template_not_found": "Task template not found.",
|
||||
"error.upgrade_trigger_not_found": "Feature not available.",
|
||||
|
||||
// Access denied errors
|
||||
"error.task_access_denied": "You don't have permission to view this task.",
|
||||
"error.residence_access_denied": "You don't have permission to view this property.",
|
||||
"error.contractor_access_denied": "You don't have permission to view this contractor.",
|
||||
"error.document_access_denied": "You don't have permission to view this document.",
|
||||
"error.not_residence_owner": "Only the property owner can do this.",
|
||||
"error.cannot_remove_owner": "The property owner cannot be removed.",
|
||||
"error.access_denied": "You don't have permission for this action.",
|
||||
|
||||
// Sharing errors
|
||||
"error.share_code_invalid": "Invalid share code. Please check and try again.",
|
||||
"error.share_code_expired": "This share code has expired. Please request a new one.",
|
||||
"error.user_already_member": "This user is already a member of this property.",
|
||||
|
||||
// Subscription/limit errors
|
||||
"error.properties_limit_reached": "You've reached your property limit. Upgrade to add more.",
|
||||
"error.properties_limit_exceeded": "You've reached your property limit. Upgrade to add more.",
|
||||
"error.tasks_limit_exceeded": "You've reached your task limit. Upgrade to add more.",
|
||||
"error.contractors_limit_exceeded": "You've reached your contractor limit. Upgrade to add more.",
|
||||
"error.documents_limit_exceeded": "You've reached your document limit. Upgrade to add more.",
|
||||
|
||||
// Task state errors
|
||||
"error.task_already_cancelled": "This task has already been cancelled.",
|
||||
"error.task_already_archived": "This task has already been archived.",
|
||||
|
||||
// Form/upload errors
|
||||
"error.failed_to_parse_form": "Unable to process the form. Please try again.",
|
||||
"error.task_id_required": "Task ID is required.",
|
||||
"error.residence_id_required": "Property ID is required.",
|
||||
"error.title_required": "Title is required.",
|
||||
"error.failed_to_upload_image": "Unable to upload image. Please try again.",
|
||||
"error.failed_to_upload_file": "Unable to upload file. Please try again.",
|
||||
"error.no_file_provided": "Please select a file to upload.",
|
||||
|
||||
// Invalid ID errors
|
||||
"error.invalid_task_id": "Invalid task.",
|
||||
"error.invalid_task_id_value": "Invalid task.",
|
||||
"error.invalid_residence_id": "Invalid property.",
|
||||
"error.invalid_residence_id_value": "Invalid property.",
|
||||
"error.invalid_contractor_id": "Invalid contractor.",
|
||||
"error.invalid_document_id": "Invalid document.",
|
||||
"error.invalid_completion_id": "Invalid task completion.",
|
||||
"error.invalid_user_id": "Invalid user.",
|
||||
"error.invalid_notification_id": "Invalid notification.",
|
||||
"error.invalid_device_id": "Invalid device.",
|
||||
"error.invalid_platform": "Invalid platform.",
|
||||
"error.invalid_id": "Invalid ID.",
|
||||
|
||||
// Data fetch errors
|
||||
"error.failed_to_fetch_residence_types": "Unable to load property types. Please try again.",
|
||||
"error.failed_to_fetch_task_categories": "Unable to load task categories. Please try again.",
|
||||
"error.failed_to_fetch_task_priorities": "Unable to load task priorities. Please try again.",
|
||||
"error.failed_to_fetch_task_frequencies": "Unable to load task frequencies. Please try again.",
|
||||
"error.failed_to_fetch_task_statuses": "Unable to load task statuses. Please try again.",
|
||||
"error.failed_to_fetch_contractor_specialties": "Unable to load contractor specialties. Please try again.",
|
||||
"error.failed_to_fetch_templates": "Unable to load task templates. Please try again.",
|
||||
"error.failed_to_search_templates": "Unable to search templates. Please try again.",
|
||||
|
||||
// Subscription purchase errors
|
||||
"error.receipt_data_required": "Purchase verification failed. Please try again.",
|
||||
"error.purchase_token_required": "Purchase verification failed. Please try again.",
|
||||
|
||||
// Media errors
|
||||
"error.file_not_found": "File not found.",
|
||||
"error.image_not_found": "Image not found.",
|
||||
|
||||
// Generic errors
|
||||
"error.invalid_request": "Invalid request. Please try again.",
|
||||
"error.invalid_request_body": "Invalid request. Please check your input.",
|
||||
"error.internal": "Something went wrong. Please try again.",
|
||||
"error.query_required": "Search query is required.",
|
||||
"error.query_too_short": "Search query is too short."
|
||||
]
|
||||
|
||||
// Indicators that a message is technical/developer-facing
|
||||
// MARK: - Network Error Patterns
|
||||
|
||||
/// Network/connection error patterns to detect
|
||||
private static let networkErrorPatterns: [(pattern: String, message: String)] = [
|
||||
("Could not connect to the server", "Unable to connect. Please check your internet connection."),
|
||||
("NSURLErrorDomain", "Unable to connect. Please check your internet connection."),
|
||||
("The Internet connection appears to be offline", "No internet connection. Please check your network."),
|
||||
("A server with the specified hostname could not be found", "Unable to connect. Please check your internet connection."),
|
||||
("The request timed out", "Request timed out. Please try again."),
|
||||
("The network connection was lost", "Connection lost. Please try again."),
|
||||
("An SSL error has occurred", "Secure connection failed. Please try again."),
|
||||
("CFNetwork", "Unable to connect. Please check your internet connection."),
|
||||
("kCFStreamError", "Unable to connect. Please check your internet connection."),
|
||||
("Code=-1004", "Unable to connect. Please check your internet connection."),
|
||||
("Code=-1009", "No internet connection. Please check your network."),
|
||||
("Code=-1001", "Request timed out. Please try again."),
|
||||
("Code=-1003", "Unable to connect. Please check your internet connection."),
|
||||
("Code=-1005", "Connection lost. Please try again."),
|
||||
("Code=-1200", "Secure connection failed. Please try again."),
|
||||
("UnresolvedAddressException", "Unable to connect. Please check your internet connection."),
|
||||
("ConnectException", "Unable to connect. Please check your internet connection."),
|
||||
("SocketTimeoutException", "Request timed out. Please try again."),
|
||||
("Connection refused", "Unable to connect. The server may be temporarily unavailable."),
|
||||
("Connection reset", "Connection lost. Please try again.")
|
||||
]
|
||||
|
||||
// MARK: - Technical Error Indicators
|
||||
|
||||
/// Indicators that a message is technical/developer-facing
|
||||
private static let technicalIndicators = [
|
||||
"Exception",
|
||||
"Error Domain=",
|
||||
@@ -50,14 +176,25 @@ enum ErrorMessageParser {
|
||||
"_kCF"
|
||||
]
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
/// Parses error messages to extract user-friendly text
|
||||
/// Handles network errors, JSON error responses, and raw error messages
|
||||
/// Handles API error codes, network errors, JSON responses, and raw error messages
|
||||
/// - Parameter rawMessage: The raw error message from the API or exception
|
||||
/// - Returns: A user-friendly error message
|
||||
static func parse(_ rawMessage: String) -> String {
|
||||
let trimmed = rawMessage.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
// Check for network/connection errors first (these are technical messages from exceptions)
|
||||
// Check for known API error codes first (e.g., "error.invalid_token")
|
||||
if trimmed.hasPrefix("error.") {
|
||||
if let friendlyMessage = errorCodeMappings[trimmed] {
|
||||
return friendlyMessage
|
||||
}
|
||||
// Unknown error code - generate a generic message from the code
|
||||
return generateMessageFromCode(trimmed)
|
||||
}
|
||||
|
||||
// Check for network/connection errors
|
||||
for (pattern, friendlyMessage) in networkErrorPatterns {
|
||||
if trimmed.localizedCaseInsensitiveContains(pattern) {
|
||||
return friendlyMessage
|
||||
@@ -85,17 +222,17 @@ enum ErrorMessageParser {
|
||||
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
||||
// Try to find common error fields
|
||||
if let errorMsg = json["error"] as? String {
|
||||
return errorMsg
|
||||
// Recursively parse the error message (it might be an error code)
|
||||
return parse(errorMsg)
|
||||
}
|
||||
if let message = json["message"] as? String {
|
||||
return message
|
||||
return parse(message)
|
||||
}
|
||||
if let detail = json["detail"] as? String {
|
||||
return detail
|
||||
return parse(detail)
|
||||
}
|
||||
|
||||
// Check if this looks like a data object (has id, title, etc)
|
||||
// rather than an error response
|
||||
if json["id"] != nil && (json["title"] != nil || json["name"] != nil) {
|
||||
return "Request failed. Please check your input and try again."
|
||||
}
|
||||
@@ -108,6 +245,22 @@ enum ErrorMessageParser {
|
||||
return "An error occurred. Please try again."
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
|
||||
/// Generates a user-friendly message from an unknown error code
|
||||
private static func generateMessageFromCode(_ code: String) -> String {
|
||||
// Remove "error." prefix and convert underscores to spaces
|
||||
var message = code.replacingOccurrences(of: "error.", with: "")
|
||||
message = message.replacingOccurrences(of: "_", with: " ")
|
||||
|
||||
// Capitalize first letter
|
||||
if let firstChar = message.first {
|
||||
message = firstChar.uppercased() + message.dropFirst()
|
||||
}
|
||||
|
||||
return message + ". Please try again."
|
||||
}
|
||||
|
||||
/// Checks if the message looks like a technical/developer error (stack trace, exception, etc)
|
||||
private static func isTechnicalError(_ message: String) -> Bool {
|
||||
return technicalIndicators.contains { message.localizedCaseInsensitiveContains($0) }
|
||||
|
||||
@@ -43,7 +43,10 @@ enum L10n {
|
||||
static var registerPassword: String { String(localized: "auth_register_password") }
|
||||
static var registerConfirmPassword: String { String(localized: "auth_register_confirm_password") }
|
||||
static var registerButton: String { String(localized: "auth_register_button") }
|
||||
static var creatingAccount: String { String(localized: "auth_creating_account") }
|
||||
static var haveAccount: String { String(localized: "auth_have_account") }
|
||||
static var alreadyHaveAccount: String { String(localized: "auth_already_have_account") }
|
||||
static var signIn: String { String(localized: "auth_sign_in") }
|
||||
static var passwordsDontMatch: String { String(localized: "auth_passwords_dont_match") }
|
||||
static var joinCasera: String { String(localized: "auth_join_casera") }
|
||||
static var startManaging: String { String(localized: "auth_start_managing") }
|
||||
|
||||
Reference in New Issue
Block a user