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:
@@ -213,6 +213,10 @@ struct ContractorDetailView: View {
|
||||
.onAppear {
|
||||
viewModel.loadContractorDetail(id: contractorId)
|
||||
}
|
||||
.handleErrors(
|
||||
error: viewModel.errorMessage,
|
||||
onRetry: { viewModel.loadContractorDetail(id: contractorId) }
|
||||
)
|
||||
}
|
||||
|
||||
private func deleteContractor() {
|
||||
|
||||
@@ -281,6 +281,10 @@ struct ContractorFormSheet: View {
|
||||
loadContractorData()
|
||||
loadContractorSpecialties()
|
||||
}
|
||||
.handleErrors(
|
||||
error: viewModel.errorMessage,
|
||||
onRetry: { saveContractor() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
50
iosApp/iosApp/Helpers/ErrorMessageParser.swift
Normal file
50
iosApp/iosApp/Helpers/ErrorMessageParser.swift
Normal 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."
|
||||
}
|
||||
}
|
||||
@@ -265,6 +265,10 @@ struct LoginView: View {
|
||||
showPasswordReset = true
|
||||
}
|
||||
}
|
||||
.handleErrors(
|
||||
error: viewModel.errorMessage,
|
||||
onRetry: { viewModel.login() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)")
|
||||
|
||||
@@ -110,6 +110,10 @@ struct ForgotPasswordView: View {
|
||||
.onAppear {
|
||||
isEmailFocused = true
|
||||
}
|
||||
.handleErrors(
|
||||
error: viewModel.errorMessage,
|
||||
onRetry: { viewModel.requestPasswordReset() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)")
|
||||
|
||||
@@ -232,6 +232,10 @@ struct ResetPasswordView: View {
|
||||
.onAppear {
|
||||
focusedField = .newPassword
|
||||
}
|
||||
.handleErrors(
|
||||
error: viewModel.errorMessage,
|
||||
onRetry: { viewModel.resetPassword() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -158,6 +158,10 @@ struct VerifyResetCodeView: View {
|
||||
.onAppear {
|
||||
isCodeFocused = true
|
||||
}
|
||||
.handleErrors(
|
||||
error: viewModel.errorMessage,
|
||||
onRetry: { viewModel.verifyResetCode() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +132,10 @@ struct ProfileView: View {
|
||||
.onChange(of: viewModel.email) { _, _ in
|
||||
viewModel.clearMessages()
|
||||
}
|
||||
.handleErrors(
|
||||
error: viewModel.errorMessage,
|
||||
onRetry: { viewModel.updateProfile() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -127,6 +127,10 @@ struct RegisterView: View {
|
||||
}
|
||||
)
|
||||
}
|
||||
.handleErrors(
|
||||
error: viewModel.errorMessage,
|
||||
onRetry: { viewModel.register() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -177,6 +177,10 @@ struct ResidenceFormView: View {
|
||||
loadResidenceTypes()
|
||||
initializeForm()
|
||||
}
|
||||
.handleErrors(
|
||||
error: viewModel.errorMessage,
|
||||
onRetry: { submitForm() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -278,6 +278,10 @@ struct CompleteTaskView: View {
|
||||
.onAppear {
|
||||
contractorViewModel.loadContractors()
|
||||
}
|
||||
.handleErrors(
|
||||
error: errorMessage,
|
||||
onRetry: { handleComplete() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -128,6 +128,10 @@ struct EditTaskView: View {
|
||||
.onAppear {
|
||||
loadLookups()
|
||||
}
|
||||
.handleErrors(
|
||||
error: viewModel.errorMessage,
|
||||
onRetry: { submitForm() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,3 +185,4 @@ struct EditTaskView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -146,6 +146,10 @@ struct VerifyEmailView: View {
|
||||
onVerifySuccess()
|
||||
}
|
||||
}
|
||||
.handleErrors(
|
||||
error: viewModel.errorMessage,
|
||||
onRetry: { viewModel.verifyEmail() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user