Add comprehensive error message parsing to prevent raw JSON display

- Created ErrorMessageParser utility for both iOS (Swift) and Android (Kotlin)
- Parser detects JSON-formatted error messages and extracts user-friendly text
- Identifies when data objects (not errors) are returned and provides generic messages
- Updated all API error handling to pass raw error bodies instead of concatenating
- Applied ErrorMessageParser across all ViewModels and screens on both platforms
- Fixed ContractorApi and DocumentApi to not concatenate error bodies with messages
- Updated ApiResultHandler to automatically parse all error messages
- Error messages now show "Request failed. Please check your input and try again." instead of raw JSON

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-11-14 22:59:42 -06:00
parent 225bdbc2bc
commit 2730c94e4d
48 changed files with 415 additions and 265 deletions

View File

@@ -213,6 +213,10 @@ struct ContractorDetailView: View {
.onAppear {
viewModel.loadContractorDetail(id: contractorId)
}
.handleErrors(
error: viewModel.errorMessage,
onRetry: { viewModel.loadContractorDetail(id: contractorId) }
)
}
private func deleteContractor() {

View File

@@ -281,6 +281,10 @@ struct ContractorFormSheet: View {
loadContractorData()
loadContractorSpecialties()
}
.handleErrors(
error: viewModel.errorMessage,
onRetry: { saveContractor() }
)
}
}

View File

@@ -57,7 +57,7 @@ class ContractorViewModel: ObservableObject {
break
} else if let error = state as? ApiResultError {
await MainActor.run {
self.errorMessage = error.message
self.errorMessage = ErrorMessageParser.parse(error.message)
self.isLoading = false
}
break
@@ -87,7 +87,7 @@ class ContractorViewModel: ObservableObject {
break
} else if let error = state as? ApiResultError {
await MainActor.run {
self.errorMessage = error.message
self.errorMessage = ErrorMessageParser.parse(error.message)
self.isLoading = false
}
break
@@ -119,7 +119,7 @@ class ContractorViewModel: ObservableObject {
break
} else if let error = state as? ApiResultError {
await MainActor.run {
self.errorMessage = error.message
self.errorMessage = ErrorMessageParser.parse(error.message)
self.isCreating = false
}
sharedViewModel.resetCreateState()
@@ -153,7 +153,7 @@ class ContractorViewModel: ObservableObject {
break
} else if let error = state as? ApiResultError {
await MainActor.run {
self.errorMessage = error.message
self.errorMessage = ErrorMessageParser.parse(error.message)
self.isUpdating = false
}
sharedViewModel.resetUpdateState()
@@ -187,7 +187,7 @@ class ContractorViewModel: ObservableObject {
break
} else if let error = state as? ApiResultError {
await MainActor.run {
self.errorMessage = error.message
self.errorMessage = ErrorMessageParser.parse(error.message)
self.isDeleting = false
}
sharedViewModel.resetDeleteState()
@@ -210,7 +210,7 @@ class ContractorViewModel: ObservableObject {
break
} else if let error = state as? ApiResultError {
await MainActor.run {
self.errorMessage = error.message
self.errorMessage = ErrorMessageParser.parse(error.message)
}
sharedViewModel.resetToggleFavoriteState()
completion(false)

View File

@@ -166,6 +166,10 @@ struct ContractorsListView: View {
.onChange(of: searchText) { newValue in
loadContractors()
}
.handleErrors(
error: viewModel.errorMessage,
onRetry: { loadContractors() }
)
}
private func loadContractors(forceRefresh: Bool = false) {

View File

@@ -57,7 +57,7 @@ class DocumentViewModel: ObservableObject {
break
} else if let error = state as? ApiResultError {
await MainActor.run {
self.errorMessage = error.message
self.errorMessage = ErrorMessageParser.parse(error.message)
self.isLoading = false
}
break
@@ -142,7 +142,7 @@ class DocumentViewModel: ObservableObject {
break
} else if let error = state as? ApiResultError {
await MainActor.run {
self.errorMessage = error.message
self.errorMessage = ErrorMessageParser.parse(error.message)
self.isLoading = false
}
sharedViewModel.resetCreateState()
@@ -219,7 +219,7 @@ class DocumentViewModel: ObservableObject {
break
} else if let error = state as? ApiResultError {
await MainActor.run {
self.errorMessage = error.message
self.errorMessage = ErrorMessageParser.parse(error.message)
self.isLoading = false
}
sharedViewModel.resetUpdateState()
@@ -251,7 +251,7 @@ class DocumentViewModel: ObservableObject {
break
} else if let error = state as? ApiResultError {
await MainActor.run {
self.errorMessage = error.message
self.errorMessage = ErrorMessageParser.parse(error.message)
self.isLoading = false
}
sharedViewModel.resetDeleteState()

View File

@@ -0,0 +1,50 @@
import Foundation
/// Utility for parsing and cleaning error messages from API responses
enum ErrorMessageParser {
/// Parses error messages to extract user-friendly text
/// If the error message is JSON, extract relevant error details
/// - Parameter rawMessage: The raw error message from the API
/// - Returns: A user-friendly error message
static func parse(_ rawMessage: String) -> String {
// Check if the message looks like JSON (starts with { or [)
let trimmed = rawMessage.trimmingCharacters(in: .whitespacesAndNewlines)
guard trimmed.hasPrefix("{") || trimmed.hasPrefix("[") else {
// Not JSON, return as-is
return rawMessage
}
// If it's JSON, it's not meant for user display
// Try to parse and extract meaningful error info
guard let data = trimmed.data(using: .utf8) else {
return "An error occurred. Please try again."
}
do {
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
}
if let message = json["message"] as? String {
return message
}
if let detail = json["detail"] as? String {
return 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."
}
}
} catch {
// JSON parsing failed
}
// If we couldn't parse or extract a message, return a generic error
return "An error occurred. Please try again."
}
}

View File

@@ -265,6 +265,10 @@ struct LoginView: View {
showPasswordReset = true
}
}
.handleErrors(
error: viewModel.errorMessage,
onRetry: { viewModel.login() }
)
}
}

View File

@@ -108,10 +108,10 @@ class LoginViewModel: ObservableObject {
case 500...599:
self.errorMessage = "Server error. Please try again later."
default:
self.errorMessage = self.cleanErrorMessage(error.message)
self.errorMessage = ErrorMessageParser.parse(error.message)
}
} else {
self.errorMessage = self.cleanErrorMessage(error.message)
self.errorMessage = ErrorMessageParser.parse(error.message)
}
print("API Error: \(error.message)")

View File

@@ -110,6 +110,10 @@ struct ForgotPasswordView: View {
.onAppear {
isEmailFocused = true
}
.handleErrors(
error: viewModel.errorMessage,
onRetry: { viewModel.requestPasswordReset() }
)
}
}
}

View File

@@ -290,10 +290,10 @@ class PasswordResetViewModel: ObservableObject {
} else if message.contains("Invalid") && message.contains("token") {
self.errorMessage = "Invalid or expired reset token. Please start over."
} else {
self.errorMessage = message
self.errorMessage = ErrorMessageParser.parse(message)
}
} else {
self.errorMessage = errorResult.message
self.errorMessage = ErrorMessageParser.parse(errorResult.message)
}
print("API Error: \(errorResult.message)")

View File

@@ -232,6 +232,10 @@ struct ResetPasswordView: View {
.onAppear {
focusedField = .newPassword
}
.handleErrors(
error: viewModel.errorMessage,
onRetry: { viewModel.resetPassword() }
)
}
}

View File

@@ -158,6 +158,10 @@ struct VerifyResetCodeView: View {
.onAppear {
isCodeFocused = true
}
.handleErrors(
error: viewModel.errorMessage,
onRetry: { viewModel.verifyResetCode() }
)
}
}
}

View File

@@ -132,6 +132,10 @@ struct ProfileView: View {
.onChange(of: viewModel.email) { _, _ in
viewModel.clearMessages()
}
.handleErrors(
error: viewModel.errorMessage,
onRetry: { viewModel.updateProfile() }
)
}
}
}

View File

@@ -60,7 +60,7 @@ class ProfileViewModel: ObservableObject {
break
} else if let error = state as? ApiResultError {
await MainActor.run {
self.errorMessage = error.message
self.errorMessage = ErrorMessageParser.parse(error.message)
self.isLoadingUser = false
}
sharedViewModel.resetCurrentUserState()
@@ -114,7 +114,7 @@ class ProfileViewModel: ObservableObject {
} else if let error = state as? ApiResultError {
await MainActor.run {
self.isLoading = false
self.errorMessage = error.message
self.errorMessage = ErrorMessageParser.parse(error.message)
self.successMessage = nil
}
sharedViewModel.resetUpdateProfileState()

View File

@@ -127,6 +127,10 @@ struct RegisterView: View {
}
)
}
.handleErrors(
error: viewModel.errorMessage,
onRetry: { viewModel.register() }
)
}
}
}

View File

@@ -82,7 +82,7 @@ class RegisterViewModel: ObservableObject {
break
} else if let error = state as? ApiResultError {
await MainActor.run {
self.errorMessage = error.message
self.errorMessage = ErrorMessageParser.parse(error.message)
self.isLoading = false
}
sharedViewModel.resetRegisterState()

View File

@@ -89,7 +89,7 @@ struct JoinResidenceView: View {
break
} else if let error = state as? ApiResultError {
await MainActor.run {
viewModel.errorMessage = error.message
viewModel.errorMessage = ErrorMessageParser.parse(error.message)
viewModel.sharedViewModel.resetJoinResidenceState()
}
break

View File

@@ -100,7 +100,7 @@ struct ManageUsersView: View {
self.ownerId = responseData.ownerId as? Int32
self.isLoading = false
} else if let errorResult = result as? ApiResultError {
self.errorMessage = errorResult.message
self.errorMessage = ErrorMessageParser.parse(errorResult.message)
self.isLoading = false
} else {
self.errorMessage = "Failed to load users"
@@ -149,7 +149,7 @@ struct ManageUsersView: View {
self.shareCode = successResult.data
self.isGeneratingCode = false
} else if let errorResult = result as? ApiResultError {
self.errorMessage = errorResult.message
self.errorMessage = ErrorMessageParser.parse(errorResult.message)
self.isGeneratingCode = false
} else {
self.errorMessage = "Failed to generate share code"
@@ -177,7 +177,7 @@ struct ManageUsersView: View {
// Remove user from local list
self.users.removeAll { $0.id == userId }
} else if let errorResult = result as? ApiResultError {
self.errorMessage = errorResult.message
self.errorMessage = ErrorMessageParser.parse(errorResult.message)
} else {
self.errorMessage = "Failed to remove user"
}

View File

@@ -148,6 +148,10 @@ struct ResidenceDetailView: View {
.onAppear {
loadResidenceData()
}
.handleErrors(
error: viewModel.errorMessage,
onRetry: { loadResidenceData() }
)
}
}
@@ -158,10 +162,6 @@ private extension ResidenceDetailView {
var mainContent: some View {
if !hasAppeared || viewModel.isLoading {
loadingView
} else if let error = viewModel.errorMessage {
ErrorView(message: error) {
loadResidenceData()
}
} else if let residence = viewModel.selectedResidence {
contentView(for: residence)
}
@@ -328,7 +328,7 @@ private extension ResidenceDetailView {
if result is ApiResultSuccess<KotlinUnit> {
dismiss()
} else if let errorResult = result as? ApiResultError {
self.viewModel.errorMessage = errorResult.message
self.viewModel.errorMessage = ErrorMessageParser.parse(errorResult.message)
} else {
self.viewModel.errorMessage = "Failed to delete residence"
}

View File

@@ -44,7 +44,7 @@ class ResidenceViewModel: ObservableObject {
break
} else if let error = state as? ApiResultError {
await MainActor.run {
self.errorMessage = error.message
self.errorMessage = ErrorMessageParser.parse(error.message)
self.isLoading = false
}
break
@@ -74,7 +74,7 @@ class ResidenceViewModel: ObservableObject {
break
} else if let error = state as? ApiResultError {
await MainActor.run {
self.errorMessage = error.message
self.errorMessage = ErrorMessageParser.parse(error.message)
self.isLoading = false
}
break
@@ -93,7 +93,7 @@ class ResidenceViewModel: ObservableObject {
self.selectedResidence = success.data
self.isLoading = false
} else if let error = result as? ApiResultError {
self.errorMessage = error.message
self.errorMessage = ErrorMessageParser.parse(error.message)
self.isLoading = false
}
}
@@ -122,7 +122,7 @@ class ResidenceViewModel: ObservableObject {
break
} else if let error = state as? ApiResultError {
await MainActor.run {
self.errorMessage = error.message
self.errorMessage = ErrorMessageParser.parse(error.message)
self.isLoading = false
}
sharedViewModel.resetCreateState()
@@ -156,7 +156,7 @@ class ResidenceViewModel: ObservableObject {
break
} else if let error = state as? ApiResultError {
await MainActor.run {
self.errorMessage = error.message
self.errorMessage = ErrorMessageParser.parse(error.message)
self.isLoading = false
}
sharedViewModel.resetUpdateState()

View File

@@ -19,10 +19,6 @@ struct ResidencesListView: View {
.font(.body)
.foregroundColor(Color(.secondaryLabel))
}
} else if let error = viewModel.errorMessage {
ErrorView(message: error) {
viewModel.loadMyResidences()
}
} else if let response = viewModel.myResidences {
if response.residences.isEmpty {
EmptyResidencesView()
@@ -100,6 +96,10 @@ struct ResidencesListView: View {
.onAppear {
viewModel.loadMyResidences()
}
.handleErrors(
error: viewModel.errorMessage,
onRetry: { viewModel.loadMyResidences() }
)
}
}

View File

@@ -177,6 +177,10 @@ struct ResidenceFormView: View {
loadResidenceTypes()
initializeForm()
}
.handleErrors(
error: viewModel.errorMessage,
onRetry: { submitForm() }
)
}
}

View File

@@ -278,6 +278,10 @@ struct CompleteTaskView: View {
.onAppear {
contractorViewModel.loadContractors()
}
.handleErrors(
error: errorMessage,
onRetry: { handleComplete() }
)
}
}

View File

@@ -128,6 +128,10 @@ struct EditTaskView: View {
.onAppear {
loadLookups()
}
.handleErrors(
error: viewModel.errorMessage,
onRetry: { submitForm() }
)
}
}
@@ -181,3 +185,4 @@ struct EditTaskView: View {
}
}
}

View File

@@ -49,7 +49,7 @@ class TaskViewModel: ObservableObject {
break
} else if let error = state as? ApiResultError {
await MainActor.run {
self.errorMessage = error.message
self.errorMessage = ErrorMessageParser.parse(error.message)
self.isLoading = false
}
sharedViewModel.resetAddTaskState()

View File

@@ -146,6 +146,10 @@ struct VerifyEmailView: View {
onVerifySuccess()
}
}
.handleErrors(
error: viewModel.errorMessage,
onRetry: { viewModel.verifyEmail() }
)
}
}
}