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:
Trey t
2025-12-17 11:48:35 -06:00
parent b05e52521f
commit bcd8b36a9b
24 changed files with 1653 additions and 232 deletions
+182 -29
View File
@@ -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) }
+3
View File
@@ -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") }