From eb68d1a86ce1acbd06a9e73e5be16ce608b01e69 Mon Sep 17 00:00:00 2001 From: dadachi Date: Thu, 2 Apr 2026 14:42:51 +0900 Subject: [PATCH 1/2] Add CodedError system with NATI-prefixed error codes Introduce CodedError protocol for structured, platform-distinguishable error codes (NATI for iOS, NATA reserved for Android). Add error types: AppError (NATI-1xxx), NativeAppTemplateAPIError (NATI-2xxx), NFCError (NATI-3xxx). Replace all error.localizedDescription with codedDescription across the codebase. Add unit tests and update test assertions. Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 14 +++ NativeAppTemplate.xcodeproj/project.pbxproj | 36 ++++++- .../Common/Errors/AppError.swift | 36 +++++++ .../Common/Errors/CodedError.swift | 26 +++++ .../Common/Errors/NFCError.swift | 24 +++++ .../Errors/NativeAppTemplateAPIError.swift | 49 ++++++++++ .../AccountPasswordRepository.swift | 2 +- .../Data/Repositories/ItemTagRepository.swift | 16 ++-- .../Data/Repositories/ShopRepository.swift | 12 +-- NativeAppTemplate/Login/LoginRepository.swift | 4 +- .../Login/SignUpRepository.swift | 10 +- .../Models/ScanResultError.swift | 19 ---- NativeAppTemplate/NFCManager.swift | 16 ++-- .../Network/NativeAppTemplateAPI.swift | 30 ------ .../Sessions/SessionController.swift | 10 +- .../UI/App Root/AcceptPrivacyViewModel.swift | 2 +- .../UI/App Root/AcceptTermsViewModel.swift | 2 +- .../UI/App Root/MainViewModel.swift | 4 +- .../SignInEmailAndPasswordViewModel.swift | 8 +- .../UI/App Root/SignUpViewModel.swift | 2 +- NativeAppTemplate/UI/Scan/ScanViewModel.swift | 10 +- .../UI/Settings/PasswordEditViewModel.swift | 6 +- .../UI/Settings/SettingsViewModel.swift | 2 +- .../UI/Settings/ShopkeeperEditViewModel.swift | 10 +- .../UI/Shop Detail/ShopDetailViewModel.swift | 12 +-- .../UI/Shop List/ShopCreateViewModel.swift | 6 +- .../ItemTagDetailViewModel.swift | 8 +- .../ItemTag Detail/ItemTagEditViewModel.swift | 12 +-- .../ItemTag List/ItemTagCreateViewModel.swift | 8 +- .../ItemTag List/ItemTagListViewModel.swift | 2 +- .../ShopBasicSettingsViewModel.swift | 12 +-- .../Shop Settings/ShopSettingsViewModel.swift | 6 +- NativeAppTemplate/Utilities/MessageBus.swift | 4 + .../Shop Detail/ShopDetailViewModelTest.swift | 2 +- .../Shop List/ShopCreateViewModelTest.swift | 4 +- .../ShopBasicSettingsViewModelTest.swift | 4 +- .../ShopSettingsViewModelTest.swift | 6 +- .../Utilities/AppErrorTest.swift | 49 ++++++++++ .../Utilities/CodedErrorTest.swift | 49 ++++++++++ .../Utilities/MessageBusTest.swift | 32 +++++++ .../Utilities/NFCErrorTest.swift | 34 +++++++ .../NativeAppTemplateAPIErrorTest.swift | 95 +++++++++++++++++++ 42 files changed, 518 insertions(+), 177 deletions(-) create mode 100644 NativeAppTemplate/Common/Errors/AppError.swift create mode 100644 NativeAppTemplate/Common/Errors/CodedError.swift create mode 100644 NativeAppTemplate/Common/Errors/NFCError.swift create mode 100644 NativeAppTemplate/Common/Errors/NativeAppTemplateAPIError.swift delete mode 100644 NativeAppTemplate/Models/ScanResultError.swift create mode 100644 NativeAppTemplateTests/Utilities/AppErrorTest.swift create mode 100644 NativeAppTemplateTests/Utilities/CodedErrorTest.swift create mode 100644 NativeAppTemplateTests/Utilities/NFCErrorTest.swift create mode 100644 NativeAppTemplateTests/Utilities/NativeAppTemplateAPIErrorTest.swift diff --git a/CLAUDE.md b/CLAUDE.md index fc3cc0a..6c45041 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -106,6 +106,20 @@ NativeAppTemplate/ └── NFC/ # NFC functionality ``` +### Error Handling (CodedError System) +All errors use the `CodedError` protocol in `NativeAppTemplate/Common/Errors/`. Error codes use the `NATI-XXXX` prefix (NativeAppTemplate iOS) to distinguish from Android (`NATA-XXXX`). + +| Range | Type | File | +|-------|------|------| +| NATI-1xxx | App/general errors | `AppError.swift` | +| NATI-2xxx | API/network errors | `NativeAppTemplateAPIError.swift` | +| NATI-3xxx | NFC/scan errors | `NFCError.swift` | + +- New error types must conform to `CodedError` and be placed in `Common/Errors/` +- Use `error.codedDescription` (not `error.localizedDescription`) in all error messages +- Use `Message(error: error)` convenience to post errors to `MessageBus` +- Error code numbers must match across iOS and Android (only the prefix differs) + ### Dependencies (Swift Package Manager) - KeychainAccess (4.2.2) - Secure credential storage - Swift Collections (1.1.4) - Additional data structures diff --git a/NativeAppTemplate.xcodeproj/project.pbxproj b/NativeAppTemplate.xcodeproj/project.pbxproj index 935f07a..5c461ac 100644 --- a/NativeAppTemplate.xcodeproj/project.pbxproj +++ b/NativeAppTemplate.xcodeproj/project.pbxproj @@ -85,7 +85,6 @@ 017278642D7D83E700CE424F /* ItemTagInfoFromNdefMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172785E2D7D83E700CE424F /* ItemTagInfoFromNdefMessage.swift */; }; 017278652D7D83E700CE424F /* ItemTagType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017278602D7D83E700CE424F /* ItemTagType.swift */; }; 017278682D7D83F600CE424F /* ScanState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017278672D7D83F600CE424F /* ScanState.swift */; }; - 017278692D7D83F600CE424F /* ScanResultError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017278662D7D83F600CE424F /* ScanResultError.swift */; }; 0172786B2D7D840A00CE424F /* ShowTagInfoScanResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172786A2D7D840A00CE424F /* ShowTagInfoScanResult.swift */; }; 0172786F2D7D87D000CE424F /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172786E2D7D87D000CE424F /* String+Extensions.swift */; }; 017278702D7D87D000CE424F /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172786C2D7D87D000CE424F /* Date+Extensions.swift */; }; @@ -167,6 +166,10 @@ 01EE363E29A6DCEB009BCD9D /* ShopkeeperEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01EE363D29A6DCEB009BCD9D /* ShopkeeperEditView.swift */; }; 01FC03E22B3329B700E6CD8E /* NeedAppUpdatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01FC03E12B3329B700E6CD8E /* NeedAppUpdatesView.swift */; }; 7249A60C06FE44338E16BC50 /* CertificatePinningDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C597C0551370446BB931F19B /* CertificatePinningDelegate.swift */; }; + A2B3C4D500000002 /* CodedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2B3C4D500000001 /* CodedError.swift */; }; + A2B3C4D500000004 /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2B3C4D500000003 /* AppError.swift */; }; + A2B3C4D500000006 /* NFCError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2B3C4D500000005 /* NFCError.swift */; }; + A2B3C4D500000008 /* NativeAppTemplateAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2B3C4D500000007 /* NativeAppTemplateAPIError.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -258,7 +261,6 @@ 0172785E2D7D83E700CE424F /* ItemTagInfoFromNdefMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTagInfoFromNdefMessage.swift; sourceTree = ""; }; 0172785F2D7D83E700CE424F /* ItemTagState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTagState.swift; sourceTree = ""; }; 017278602D7D83E700CE424F /* ItemTagType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTagType.swift; sourceTree = ""; }; - 017278662D7D83F600CE424F /* ScanResultError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanResultError.swift; sourceTree = ""; }; 017278672D7D83F600CE424F /* ScanState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanState.swift; sourceTree = ""; }; 0172786A2D7D840A00CE424F /* ShowTagInfoScanResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowTagInfoScanResult.swift; sourceTree = ""; }; 0172786C2D7D87D000CE424F /* Date+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = ""; }; @@ -341,6 +343,10 @@ 01EE363D29A6DCEB009BCD9D /* ShopkeeperEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopkeeperEditView.swift; sourceTree = ""; }; 01FC03E12B3329B700E6CD8E /* NeedAppUpdatesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NeedAppUpdatesView.swift; sourceTree = ""; }; C597C0551370446BB931F19B /* CertificatePinningDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificatePinningDelegate.swift; sourceTree = ""; }; + A2B3C4D500000001 /* CodedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodedError.swift; sourceTree = ""; }; + A2B3C4D500000003 /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = ""; }; + A2B3C4D500000005 /* NFCError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NFCError.swift; sourceTree = ""; }; + A2B3C4D500000007 /* NativeAppTemplateAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeAppTemplateAPIError.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -411,6 +417,7 @@ 011F6DF9259EF16600BED22E /* Info.plist */, 0172782B2D7D575900CE424F /* NativeAppTemplate.entitlements */, 015C78042B72DA2C00B6523C /* PrivacyInfo.xcprivacy */, + A2B3C4D50000000A /* Common */, 0172049125AA8449008FD63B /* Data */, 017203A725A96FBF008FD63B /* Extensions */, 017203E925AA6601008FD63B /* Logging */, @@ -581,7 +588,6 @@ 017278602D7D83E700CE424F /* ItemTagType.swift */, 01B526532AF4E36400655131 /* MainTab.swift */, 017278082D7D4F7400CE424F /* Onboarding.swift */, - 017278662D7D83F600CE424F /* ScanResultError.swift */, 017278672D7D83F600CE424F /* ScanState.swift */, 01B526552AF4E82A00655131 /* ScrollToTopID.swift */, 0110A15E2AC816F5003EDCBA /* SendConfirmation.swift */, @@ -825,6 +831,25 @@ path = Shared; sourceTree = ""; }; + A2B3C4D500000009 /* Errors */ = { + isa = PBXGroup; + children = ( + A2B3C4D500000003 /* AppError.swift */, + A2B3C4D500000001 /* CodedError.swift */, + A2B3C4D500000007 /* NativeAppTemplateAPIError.swift */, + A2B3C4D500000005 /* NFCError.swift */, + ); + path = Errors; + sourceTree = ""; + }; + A2B3C4D50000000A /* Common */ = { + isa = PBXGroup; + children = ( + A2B3C4D500000009 /* Errors */, + ); + path = Common; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -997,7 +1022,10 @@ 0150A36629B14BB300907F96 /* SendResetPassword.swift in Sources */, 017204B625AA8467008FD63B /* DataManager.swift in Sources */, 017278682D7D83F600CE424F /* ScanState.swift in Sources */, - 017278692D7D83F600CE424F /* ScanResultError.swift in Sources */, + A2B3C4D500000002 /* CodedError.swift in Sources */, + A2B3C4D500000004 /* AppError.swift in Sources */, + A2B3C4D500000006 /* NFCError.swift in Sources */, + A2B3C4D500000008 /* NativeAppTemplateAPIError.swift in Sources */, 0172787D2D7D92DF00CE424F /* CompleteScanResult.swift in Sources */, 017278822D7D935700CE424F /* QRCodeGenerator.swift in Sources */, 017278832D7D935700CE424F /* ImageSaver.swift in Sources */, diff --git a/NativeAppTemplate/Common/Errors/AppError.swift b/NativeAppTemplate/Common/Errors/AppError.swift new file mode 100644 index 0000000..7a1de92 --- /dev/null +++ b/NativeAppTemplate/Common/Errors/AppError.swift @@ -0,0 +1,36 @@ +// +// AppError.swift +// NativeAppTemplate +// + +import Foundation + +enum AppError: CodedError { + case unexpected( + description: String, + file: String = #file, + line: Int = #line, + function: String = #function + ) + + var errorCode: String { + switch self { + case .unexpected: + "NATI-1001" + } + } + + var errorDescription: String? { + switch self { + case let .unexpected(description, _, _, _): + "An unexpected error occurred. \(description)" + } + } + + var debugDescription: String { + switch self { + case let .unexpected(description, file, line, function): + "[\(errorCode)] \(description) (file: \(file), line: \(line), function: \(function))" + } + } +} diff --git a/NativeAppTemplate/Common/Errors/CodedError.swift b/NativeAppTemplate/Common/Errors/CodedError.swift new file mode 100644 index 0000000..1cd9c64 --- /dev/null +++ b/NativeAppTemplate/Common/Errors/CodedError.swift @@ -0,0 +1,26 @@ +// +// CodedError.swift +// NativeAppTemplate +// + +// Error codes use the `NATI-XXXX` prefix (NativeAppTemplate iOS). +// Android uses `NATA-XXXX`. +// Ranges: 1xxx App errors, 2xxx API errors, 3xxx NFC errors. + +import Foundation + +protocol CodedError: LocalizedError, Sendable { + var errorCode: String { get } +} + +extension CodedError { + var formattedDescription: String { + "[\(errorCode)] \(errorDescription ?? "Unknown error")" + } +} + +extension Error { + var codedDescription: String { + (self as? CodedError)?.formattedDescription ?? localizedDescription + } +} diff --git a/NativeAppTemplate/Common/Errors/NFCError.swift b/NativeAppTemplate/Common/Errors/NFCError.swift new file mode 100644 index 0000000..024b15e --- /dev/null +++ b/NativeAppTemplate/Common/Errors/NFCError.swift @@ -0,0 +1,24 @@ +// +// NFCError.swift +// NativeAppTemplate +// + +import Foundation + +enum NFCError: CodedError { + case scanFailed(String) + + var errorCode: String { + switch self { + case .scanFailed: + "NATI-3001" + } + } + + var errorDescription: String? { + switch self { + case let .scanFailed(message): + message + } + } +} diff --git a/NativeAppTemplate/Common/Errors/NativeAppTemplateAPIError.swift b/NativeAppTemplate/Common/Errors/NativeAppTemplateAPIError.swift new file mode 100644 index 0000000..7c90fd8 --- /dev/null +++ b/NativeAppTemplate/Common/Errors/NativeAppTemplateAPIError.swift @@ -0,0 +1,49 @@ +// +// NativeAppTemplateAPIError.swift +// NativeAppTemplate +// + +import Foundation + +enum NativeAppTemplateAPIError: CodedError { + case requestFailed(Error?, Int, String?) + case processingError(Error?) + case responseMissingRequiredMeta(field: String?) + case responseHasIncorrectNumberOfElements + case noData + + nonisolated var errorCode: String { + switch self { + case .requestFailed: + "NATI-2001" + case .processingError: + "NATI-2002" + case .responseMissingRequiredMeta: + "NATI-2003" + case .responseHasIncorrectNumberOfElements: + "NATI-2004" + case .noData: + "NATI-2005" + } + } + + nonisolated var errorDescription: String? { + switch self { + case let .requestFailed(error, statusCode, message): + if let message { + "\(message) [Status: \(statusCode)]" + } else { + "NativeAppTemplateAPIError::RequestFailed" + + "[Status: \(statusCode) | Error: \(error?.localizedDescription ?? "UNKNOWN")]" + } + case let .processingError(error): + "NativeAppTemplateAPIError::ProcessingError[Error: \(error?.localizedDescription ?? "UNKNOWN")]" + case let .responseMissingRequiredMeta(field: field): + "NativeAppTemplateAPIError::ResponseMissingRequiredMeta[Field: \(field ?? "UNKNOWN")]" + case .responseHasIncorrectNumberOfElements: + "NativeAppTemplateAPIError::ResponseHasIncorrectNumberOfElements" + case .noData: + "NativeAppTemplateAPIError::NoData" + } + } +} diff --git a/NativeAppTemplate/Data/Repositories/AccountPasswordRepository.swift b/NativeAppTemplate/Data/Repositories/AccountPasswordRepository.swift index 9ea9a0f..88f6dfd 100644 --- a/NativeAppTemplate/Data/Repositories/AccountPasswordRepository.swift +++ b/NativeAppTemplate/Data/Repositories/AccountPasswordRepository.swift @@ -17,7 +17,7 @@ try await accountPasswordService.updatePassword(updatePassword: updatePassword) } catch { Failure - .destroy(from: Self.self, reason: error.localizedDescription) + .destroy(from: Self.self, reason: error.codedDescription) .log() throw error } diff --git a/NativeAppTemplate/Data/Repositories/ItemTagRepository.swift b/NativeAppTemplate/Data/Repositories/ItemTagRepository.swift index f195dd3..2952501 100644 --- a/NativeAppTemplate/Data/Repositories/ItemTagRepository.swift +++ b/NativeAppTemplate/Data/Repositories/ItemTagRepository.swift @@ -42,7 +42,7 @@ import SwiftUI } catch { state = .failed Failure - .fetch(from: Self.self, reason: error.localizedDescription) + .fetch(from: Self.self, reason: error.codedDescription) .log() } } @@ -54,7 +54,7 @@ import SwiftUI return itemTags } catch { Failure - .fetch(from: Self.self, reason: error.localizedDescription) + .fetch(from: Self.self, reason: error.codedDescription) .log() throw error } @@ -73,7 +73,7 @@ import SwiftUI return itemTag } catch { Failure - .fetch(from: Self.self, reason: error.localizedDescription) + .fetch(from: Self.self, reason: error.codedDescription) .log() throw error } @@ -84,7 +84,7 @@ import SwiftUI return try await itemTagsService.makeItemTag(shopId: shopId, itemTag: itemTag) } catch { Failure - .create(from: Self.self, reason: error.localizedDescription) + .create(from: Self.self, reason: error.codedDescription) .log() throw error } @@ -101,7 +101,7 @@ import SwiftUI return updatedItemTag } catch { Failure - .update(from: Self.self, reason: error.localizedDescription) + .update(from: Self.self, reason: error.codedDescription) .log() throw error } @@ -112,7 +112,7 @@ import SwiftUI try await itemTagsService.destroyItemTag(id: id) } catch { Failure - .destroy(from: Self.self, reason: error.localizedDescription) + .destroy(from: Self.self, reason: error.codedDescription) .log() throw error } @@ -130,7 +130,7 @@ import SwiftUI } catch { state = .failed Failure - .update(from: Self.self, reason: error.localizedDescription) + .update(from: Self.self, reason: error.codedDescription) .log() throw error } @@ -148,7 +148,7 @@ import SwiftUI } catch { state = .failed Failure - .update(from: Self.self, reason: error.localizedDescription) + .update(from: Self.self, reason: error.codedDescription) .log() throw error } diff --git a/NativeAppTemplate/Data/Repositories/ShopRepository.swift b/NativeAppTemplate/Data/Repositories/ShopRepository.swift index 0392ec6..ee7a99a 100644 --- a/NativeAppTemplate/Data/Repositories/ShopRepository.swift +++ b/NativeAppTemplate/Data/Repositories/ShopRepository.swift @@ -46,7 +46,7 @@ import SwiftUI } catch { state = .failed Failure - .fetch(from: Self.self, reason: error.localizedDescription) + .fetch(from: Self.self, reason: error.codedDescription) .log() } } @@ -65,7 +65,7 @@ import SwiftUI return shop } catch { Failure - .fetch(from: Self.self, reason: error.localizedDescription) + .fetch(from: Self.self, reason: error.codedDescription) .log() throw error } @@ -76,7 +76,7 @@ import SwiftUI return try await shopsService.makeShop(shop: shop) } catch { Failure - .create(from: Self.self, reason: error.localizedDescription) + .create(from: Self.self, reason: error.codedDescription) .log() throw error } @@ -93,7 +93,7 @@ import SwiftUI return updatedShop } catch { Failure - .update(from: Self.self, reason: error.localizedDescription) + .update(from: Self.self, reason: error.codedDescription) .log() throw error } @@ -104,7 +104,7 @@ import SwiftUI try await shopsService.destroyShop(id: id) } catch { Failure - .destroy(from: Self.self, reason: error.localizedDescription) + .destroy(from: Self.self, reason: error.codedDescription) .log() throw error } @@ -115,7 +115,7 @@ import SwiftUI try await shopsService.resetShop(id: id) } catch { Failure - .destroy(from: Self.self, reason: error.localizedDescription) + .destroy(from: Self.self, reason: error.codedDescription) .log() throw error } diff --git a/NativeAppTemplate/Login/LoginRepository.swift b/NativeAppTemplate/Login/LoginRepository.swift index dcf711f..3eeefc6 100644 --- a/NativeAppTemplate/Login/LoginRepository.swift +++ b/NativeAppTemplate/Login/LoginRepository.swift @@ -39,7 +39,7 @@ import os _currentShopkeeper = shopkeeper } catch { Failure - .fetch(from: Self.self, reason: error.localizedDescription) + .fetch(from: Self.self, reason: error.codedDescription) .log() throw error } @@ -54,7 +54,7 @@ import os _currentShopkeeper = .none } catch { Failure - .fetch(from: Self.self, reason: error.localizedDescription) + .fetch(from: Self.self, reason: error.codedDescription) .log() removeShopkeeper() _currentShopkeeper = .none diff --git a/NativeAppTemplate/Login/SignUpRepository.swift b/NativeAppTemplate/Login/SignUpRepository.swift index 666770f..11dd747 100644 --- a/NativeAppTemplate/Login/SignUpRepository.swift +++ b/NativeAppTemplate/Login/SignUpRepository.swift @@ -21,7 +21,7 @@ import os shopkeeper = try await signUpsService.makeShopkeeper(signUp: signUp) } catch { Failure - .fetch(from: Self.self, reason: error.localizedDescription) + .fetch(from: Self.self, reason: error.codedDescription) .log() throw error } @@ -36,7 +36,7 @@ import os shopkeeper = try await signUpsService.updateShopkeeper(id: id, signUp: signUp) } catch { Failure - .update(from: Self.self, reason: error.localizedDescription) + .update(from: Self.self, reason: error.codedDescription) .log() throw error } @@ -50,7 +50,7 @@ import os removeShopkeeper() } catch { Failure - .fetch(from: Self.self, reason: error.localizedDescription) + .fetch(from: Self.self, reason: error.codedDescription) .log() removeShopkeeper() @@ -70,7 +70,7 @@ import os try await signUpsService.sendResetPasswordInstruction(sendResetPassword: sendResetPassword) } catch { Failure - .fetch(from: Self.self, reason: error.localizedDescription) + .fetch(from: Self.self, reason: error.codedDescription) .log() throw error @@ -89,7 +89,7 @@ import os try await signUpsService.sendConfirmationInstruction(sendConfirmation: sendConfirmation) } catch { Failure - .fetch(from: Self.self, reason: error.localizedDescription) + .fetch(from: Self.self, reason: error.codedDescription) .log() throw error } diff --git a/NativeAppTemplate/Models/ScanResultError.swift b/NativeAppTemplate/Models/ScanResultError.swift deleted file mode 100644 index 0cdfd89..0000000 --- a/NativeAppTemplate/Models/ScanResultError.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// ScanResultError.swift -// NativeAppTemplate -// - -import Foundation - -enum ScanResultError: Error { - case failed(String) -} - -extension ScanResultError: LocalizedError { - var errorDescription: String? { - switch self { - case let .failed(message): - message - } - } -} diff --git a/NativeAppTemplate/NFCManager.swift b/NativeAppTemplate/NFCManager.swift index a922e5d..a16562e 100644 --- a/NativeAppTemplate/NFCManager.swift +++ b/NativeAppTemplate/NFCManager.swift @@ -107,13 +107,13 @@ extension NFCManager: NFCNDEFReaderSessionDelegate { session.connect(to: tag) { error in if let error { - session.invalidate(errorMessage: "Connection error: \(error.localizedDescription)") + session.invalidate(errorMessage: "Connection error: \(error.codedDescription)") return } tag.queryNDEFStatus { status, capacity, error in if let error { - session.invalidate(errorMessage: "Checking NDEF status error: \(error.localizedDescription)") + session.invalidate(errorMessage: "Checking NDEF status error: \(error.codedDescription)") return } @@ -162,7 +162,7 @@ extension NFCManager: NFCNDEFReaderSessionDelegate { nonisolated(unsafe) let tag = tag tag.readNDEF { [weak self] message, error in if let error { - session.invalidate(errorMessage: "Reading error: \(error.localizedDescription)") + session.invalidate(errorMessage: "Reading error: \(error.codedDescription)") if test { self?.internalIsScanResultChangedForTesting = true } else { @@ -173,7 +173,7 @@ extension NFCManager: NFCNDEFReaderSessionDelegate { guard let message else { session.invalidate(errorMessage: String.noRecrodsFound) - self?.internalScanResult = .failure(ScanResultError.failed(String.tagNotValid)) + self?.internalScanResult = .failure(NFCError.scanFailed(String.tagNotValid)) if test { self?.internalIsScanResultChangedForTesting = true @@ -222,13 +222,13 @@ extension NFCManager: NFCNDEFReaderSessionDelegate { nonisolated(unsafe) let completion = completion tag.writeNDEF(ndefMessage) { error in if let error { - session.invalidate(errorMessage: "Writing error: \(error.localizedDescription)") + session.invalidate(errorMessage: "Writing error: \(error.codedDescription)") completion(error) } else { if isLock { tag.writeLock { error in if let error { - session.invalidate(errorMessage: "Writing lock error: \(error.localizedDescription)") + session.invalidate(errorMessage: "Writing lock error: \(error.codedDescription)") completion(error) } else { session.alertMessage = String.writingSucceeded @@ -257,13 +257,13 @@ extension NFCManager: NFCNDEFReaderSessionDelegate { ) internalScanResult = .success(itemTagData) } else { - internalScanResult = .failure(ScanResultError.failed(itemTagInfo.message)) + internalScanResult = .failure(NFCError.scanFailed(itemTagInfo.message)) } } func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) {} func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) { - appLogger.debug("readerSession error: \(error.localizedDescription, privacy: .private)") + appLogger.debug("readerSession error: \(error.codedDescription, privacy: .private)") } } diff --git a/NativeAppTemplate/Networking/Network/NativeAppTemplateAPI.swift b/NativeAppTemplate/Networking/Network/NativeAppTemplateAPI.swift index 215d8a8..060a629 100644 --- a/NativeAppTemplate/Networking/Network/NativeAppTemplateAPI.swift +++ b/NativeAppTemplate/Networking/Network/NativeAppTemplateAPI.swift @@ -9,36 +9,6 @@ import Observation typealias HTTPHeaders = [String: String] typealias HTTPHeader = HTTPHeaders.Element -enum NativeAppTemplateAPIError: Error { - case requestFailed(Error?, Int, String?) - case processingError(Error?) - case responseMissingRequiredMeta(field: String?) - case responseHasIncorrectNumberOfElements - case noData -} - -extension NativeAppTemplateAPIError: LocalizedError { - var errorDescription: String? { - switch self { - case let .requestFailed(error, statusCode, message): - if let message { - "\(message) [Status: \(statusCode)]" - } else { - "NativeAppTemplateAPIError::RequestFailed" + - "[Status: \(statusCode) | Error: \(error?.localizedDescription ?? "UNKNOWN")]" - } - case let .processingError(error): - "NativeAppTemplateAPIError::ProcessingError[Error: \(error?.localizedDescription ?? "UNKNOWN")]" - case let .responseMissingRequiredMeta(field: field): - "NativeAppTemplateAPIError::ResponseMissingRequiredMeta[Field: \(field ?? "UNKNOWN")]" - case .responseHasIncorrectNumberOfElements: - "NativeAppTemplateAPIError::ResponseHasIncorrectNumberOfElements" - case .noData: - "NativeAppTemplateAPIError::NoData" - } - } -} - @MainActor public struct NativeAppTemplateAPI: Equatable { public nonisolated static func == (lhs: NativeAppTemplateAPI, rhs: NativeAppTemplateAPI) -> Bool { diff --git a/NativeAppTemplate/Sessions/SessionController.swift b/NativeAppTemplate/Sessions/SessionController.swift index 71d3bdb..8e7d509 100644 --- a/NativeAppTemplate/Sessions/SessionController.swift +++ b/NativeAppTemplate/Sessions/SessionController.swift @@ -110,7 +110,7 @@ import Observation permissionState = .notLoaded Failure - .login(from: Self.self, reason: error.localizedDescription) + .login(from: Self.self, reason: error.codedDescription) .log() throw error @@ -128,7 +128,7 @@ import Observation shopkeeper = nil } catch { Failure - .login(from: Self.self, reason: error.localizedDescription) + .login(from: Self.self, reason: error.codedDescription) .log() userState = .notLoggedIn @@ -183,7 +183,7 @@ import Observation } catch { enum Permissions {} Failure - .fetch(from: Permissions.self, reason: error.localizedDescription) + .fetch(from: Permissions.self, reason: error.codedDescription) .log() self.permissionState = .error @@ -201,7 +201,7 @@ import Observation try await meService.updateConfirmedPrivacyVersion() } catch { Failure - .update(from: Self.self, reason: error.localizedDescription) + .update(from: Self.self, reason: error.codedDescription) .log() throw error } @@ -212,7 +212,7 @@ import Observation try await meService.updateConfirmedTermsVersion() } catch { Failure - .update(from: Self.self, reason: error.localizedDescription) + .update(from: Self.self, reason: error.codedDescription) .log() throw error } diff --git a/NativeAppTemplate/UI/App Root/AcceptPrivacyViewModel.swift b/NativeAppTemplate/UI/App Root/AcceptPrivacyViewModel.swift index c48b9fe..d4e0c21 100644 --- a/NativeAppTemplate/UI/App Root/AcceptPrivacyViewModel.swift +++ b/NativeAppTemplate/UI/App Root/AcceptPrivacyViewModel.swift @@ -33,7 +33,7 @@ final class AcceptPrivacyViewModel { } catch { messageBus.post(message: Message( level: .error, - message: "\(String.confirmedPrivacyVersionUpdatedError) \(error.localizedDescription)", + message: "\(String.confirmedPrivacyVersionUpdatedError) \(error.codedDescription)", autoDismiss: false )) } diff --git a/NativeAppTemplate/UI/App Root/AcceptTermsViewModel.swift b/NativeAppTemplate/UI/App Root/AcceptTermsViewModel.swift index bae7cd9..36a07a7 100644 --- a/NativeAppTemplate/UI/App Root/AcceptTermsViewModel.swift +++ b/NativeAppTemplate/UI/App Root/AcceptTermsViewModel.swift @@ -33,7 +33,7 @@ final class AcceptTermsViewModel { } catch { messageBus.post(message: Message( level: .error, - message: "\(String.confirmedTermsVersionUpdatedError) \(error.localizedDescription)", + message: "\(String.confirmedTermsVersionUpdatedError) \(error.codedDescription)", autoDismiss: false )) } diff --git a/NativeAppTemplate/UI/App Root/MainViewModel.swift b/NativeAppTemplate/UI/App Root/MainViewModel.swift index 1210d72..c7edd72 100644 --- a/NativeAppTemplate/UI/App Root/MainViewModel.swift +++ b/NativeAppTemplate/UI/App Root/MainViewModel.swift @@ -104,7 +104,7 @@ final class MainViewModel { } catch { sessionController.completeScanResult = CompleteScanResult( type: .failed, - message: error.localizedDescription + message: error.codedDescription ) } @@ -126,7 +126,7 @@ final class MainViewModel { } catch { sessionController.completeScanResult = CompleteScanResult( type: .failed, - message: error.localizedDescription + message: error.codedDescription ) } diff --git a/NativeAppTemplate/UI/App Root/SignInEmailAndPasswordViewModel.swift b/NativeAppTemplate/UI/App Root/SignInEmailAndPasswordViewModel.swift index 1e85eda..3bb1a40 100644 --- a/NativeAppTemplate/UI/App Root/SignInEmailAndPasswordViewModel.swift +++ b/NativeAppTemplate/UI/App Root/SignInEmailAndPasswordViewModel.swift @@ -70,13 +70,7 @@ final class SignInEmailAndPasswordViewModel { isLoggingIn = true try await sessionController.login(email: theEmail, password: thePassword) } catch { - messageBus.post( - message: Message( - level: .error, - message: error.localizedDescription, - autoDismiss: false - ) - ) + messageBus.post(message: Message(error: error)) } isLoggingIn = false diff --git a/NativeAppTemplate/UI/App Root/SignUpViewModel.swift b/NativeAppTemplate/UI/App Root/SignUpViewModel.swift index 69d7d82..bbd21ea 100644 --- a/NativeAppTemplate/UI/App Root/SignUpViewModel.swift +++ b/NativeAppTemplate/UI/App Root/SignUpViewModel.swift @@ -110,7 +110,7 @@ final class SignUpViewModel { errorMessage = message ?? "UNKNOWN" isShowingAlert = true } catch { - errorMessage = error.localizedDescription + errorMessage = error.codedDescription isShowingAlert = true } diff --git a/NativeAppTemplate/UI/Scan/ScanViewModel.swift b/NativeAppTemplate/UI/Scan/ScanViewModel.swift index 7a1aa66..9e94119 100644 --- a/NativeAppTemplate/UI/Scan/ScanViewModel.swift +++ b/NativeAppTemplate/UI/Scan/ScanViewModel.swift @@ -53,7 +53,7 @@ final class ScanViewModel { case let .failure(error): sessionController.completeScanResult = CompleteScanResult( type: .failed, - message: error.localizedDescription + message: error.codedDescription ) default: break @@ -70,7 +70,7 @@ final class ScanViewModel { case let .failure(error): sessionController.showTagInfoScanResult = ShowTagInfoScanResult( type: .failed, - message: error.localizedDescription + message: error.codedDescription ) default: break @@ -140,7 +140,7 @@ final class ScanViewModel { } catch { sessionController.completeScanResult = CompleteScanResult( type: .failed, - message: error.localizedDescription + message: error.codedDescription ) } } @@ -159,7 +159,7 @@ final class ScanViewModel { } catch { sessionController.completeScanResult = CompleteScanResult( type: .failed, - message: error.localizedDescription + message: error.codedDescription ) } @@ -184,7 +184,7 @@ final class ScanViewModel { } catch { sessionController.showTagInfoScanResult = ShowTagInfoScanResult( type: .failed, - message: error.localizedDescription + message: error.codedDescription ) } diff --git a/NativeAppTemplate/UI/Settings/PasswordEditViewModel.swift b/NativeAppTemplate/UI/Settings/PasswordEditViewModel.swift index 3781c9b..8bd96eb 100644 --- a/NativeAppTemplate/UI/Settings/PasswordEditViewModel.swift +++ b/NativeAppTemplate/UI/Settings/PasswordEditViewModel.swift @@ -80,11 +80,7 @@ final class PasswordEditViewModel { messageBus.post(message: Message(level: .success, message: .passwordUpdated)) shouldDismiss = true } catch { - messageBus.post(message: Message( - level: .error, - message: error.localizedDescription, - autoDismiss: false - )) + messageBus.post(message: Message(error: error)) } isUpdating = false diff --git a/NativeAppTemplate/UI/Settings/SettingsViewModel.swift b/NativeAppTemplate/UI/Settings/SettingsViewModel.swift index 31424aa..731311c 100644 --- a/NativeAppTemplate/UI/Settings/SettingsViewModel.swift +++ b/NativeAppTemplate/UI/Settings/SettingsViewModel.swift @@ -46,7 +46,7 @@ final class SettingsViewModel { #if DEBUG messageBus.post(message: Message( level: .error, - message: "\(String.signedOutError) \(error.localizedDescription)", + message: "\(String.signedOutError) \(error.codedDescription)", autoDismiss: false )) #endif diff --git a/NativeAppTemplate/UI/Settings/ShopkeeperEditViewModel.swift b/NativeAppTemplate/UI/Settings/ShopkeeperEditViewModel.swift index 2bd9663..7243876 100644 --- a/NativeAppTemplate/UI/Settings/ShopkeeperEditViewModel.swift +++ b/NativeAppTemplate/UI/Settings/ShopkeeperEditViewModel.swift @@ -117,11 +117,7 @@ final class ShopkeeperEditViewModel { shouldDismiss = true } catch { - messageBus.post(message: Message( - level: .error, - message: error.localizedDescription, - autoDismiss: false - )) + messageBus.post(message: Message(error: error)) } isUpdating = false @@ -138,7 +134,7 @@ final class ShopkeeperEditViewModel { } catch { messageBus.post(message: Message( level: .error, - message: "\(String.shopkeeperDeletedError) \(error.localizedDescription)", + message: "\(String.shopkeeperDeletedError) \(error.codedDescription)", autoDismiss: false )) } @@ -148,7 +144,7 @@ final class ShopkeeperEditViewModel { } catch { messageBus.post(message: Message( level: .error, - message: "\(String.shopkeeperDeletedError) \(error.localizedDescription)", + message: "\(String.shopkeeperDeletedError) \(error.codedDescription)", autoDismiss: false )) } diff --git a/NativeAppTemplate/UI/Shop Detail/ShopDetailViewModel.swift b/NativeAppTemplate/UI/Shop Detail/ShopDetailViewModel.swift index 74ca94a..8d03110 100644 --- a/NativeAppTemplate/UI/Shop Detail/ShopDetailViewModel.swift +++ b/NativeAppTemplate/UI/Shop Detail/ShopDetailViewModel.swift @@ -64,13 +64,7 @@ final class ShopDetailViewModel { itemTags = try await itemTagRepository.fetchAll(shopId: shopId) isFetching = false } catch { - messageBus.post( - message: Message( - level: .error, - message: error.localizedDescription, - autoDismiss: false - ) - ) + messageBus.post(message: Message(error: error)) shouldDismiss = true } } @@ -95,7 +89,7 @@ final class ShopDetailViewModel { messageBus.post( message: Message( level: .error, - message: "\(String.itemTagCompletedError) \(error.localizedDescription)", + message: "\(String.itemTagCompletedError) \(error.codedDescription)", autoDismiss: false ) ) @@ -117,7 +111,7 @@ final class ShopDetailViewModel { messageBus.post( message: Message( level: .error, - message: "\(String.itemTagResetError) \(error.localizedDescription)", + message: "\(String.itemTagResetError) \(error.codedDescription)", autoDismiss: false ) ) diff --git a/NativeAppTemplate/UI/Shop List/ShopCreateViewModel.swift b/NativeAppTemplate/UI/Shop List/ShopCreateViewModel.swift index d858788..9ba91f1 100644 --- a/NativeAppTemplate/UI/Shop List/ShopCreateViewModel.swift +++ b/NativeAppTemplate/UI/Shop List/ShopCreateViewModel.swift @@ -49,11 +49,7 @@ final class ShopCreateViewModel { messageBus.post(message: Message(level: .success, message: .shopCreated)) shouldDismiss = true } catch { - messageBus.post(message: Message( - level: .error, - message: error.localizedDescription, - autoDismiss: false - )) + messageBus.post(message: Message(error: error)) // e.g. Limit shops count error guard case NativeAppTemplateAPIError.requestFailed(_, 422, _) = error else { diff --git a/NativeAppTemplate/UI/Shop Settings/ItemTag Detail/ItemTagDetailViewModel.swift b/NativeAppTemplate/UI/Shop Settings/ItemTag Detail/ItemTagDetailViewModel.swift index d58679f..f5b2978 100644 --- a/NativeAppTemplate/UI/Shop Settings/ItemTag Detail/ItemTagDetailViewModel.swift +++ b/NativeAppTemplate/UI/Shop Settings/ItemTag Detail/ItemTagDetailViewModel.swift @@ -148,7 +148,7 @@ final class ItemTagDetailViewModel { } catch { messageBus.post(message: Message( level: .error, - message: "\(String.itemTagDeletedError) \(error.localizedDescription)", + message: "\(String.itemTagDeletedError) \(error.codedDescription)", autoDismiss: false )) } @@ -163,11 +163,7 @@ final class ItemTagDetailViewModel { isFetching = true itemTag = try await itemTagRepository.fetchDetail(id: itemTagId) } catch { - messageBus.post(message: Message( - level: .error, - message: error.localizedDescription, - autoDismiss: false - )) + messageBus.post(message: Message(error: error)) shouldDismiss = true } diff --git a/NativeAppTemplate/UI/Shop Settings/ItemTag Detail/ItemTagEditViewModel.swift b/NativeAppTemplate/UI/Shop Settings/ItemTag Detail/ItemTagEditViewModel.swift index 1e77e3c..a7c2381 100644 --- a/NativeAppTemplate/UI/Shop Settings/ItemTag Detail/ItemTagEditViewModel.swift +++ b/NativeAppTemplate/UI/Shop Settings/ItemTag Detail/ItemTagEditViewModel.swift @@ -91,11 +91,7 @@ final class ItemTagEditViewModel { _ = try await itemTagRepository.update(id: itemTagId, itemTag: itemTag) messageBus.post(message: Message(level: .success, message: .itemTagUpdated)) } catch { - messageBus.post(message: Message( - level: .error, - message: error.localizedDescription, - autoDismiss: false - )) + messageBus.post(message: Message(error: error)) } isUpdating = false @@ -113,11 +109,7 @@ final class ItemTagEditViewModel { queueNumber = String(itemTag.queueNumber) } } catch { - messageBus.post(message: Message( - level: .error, - message: error.localizedDescription, - autoDismiss: false - )) + messageBus.post(message: Message(error: error)) shouldDismiss = true } diff --git a/NativeAppTemplate/UI/Shop Settings/ItemTag List/ItemTagCreateViewModel.swift b/NativeAppTemplate/UI/Shop Settings/ItemTag List/ItemTagCreateViewModel.swift index 29c6744..f299624 100644 --- a/NativeAppTemplate/UI/Shop Settings/ItemTag List/ItemTagCreateViewModel.swift +++ b/NativeAppTemplate/UI/Shop Settings/ItemTag List/ItemTagCreateViewModel.swift @@ -71,13 +71,7 @@ final class ItemTagCreateViewModel { _ = try await itemTagRepository.create(shopId: shopId, itemTag: itemTag) messageBus.post(message: Message(level: .success, message: .itemTagCreated)) } catch { - messageBus.post( - message: Message( - level: .error, - message: error.localizedDescription, - autoDismiss: false - ) - ) + messageBus.post(message: Message(error: error)) } shouldDismiss = true diff --git a/NativeAppTemplate/UI/Shop Settings/ItemTag List/ItemTagListViewModel.swift b/NativeAppTemplate/UI/Shop Settings/ItemTag List/ItemTagListViewModel.swift index b38da15..e4019c1 100644 --- a/NativeAppTemplate/UI/Shop Settings/ItemTag List/ItemTagListViewModel.swift +++ b/NativeAppTemplate/UI/Shop Settings/ItemTag List/ItemTagListViewModel.swift @@ -60,7 +60,7 @@ final class ItemTagListViewModel { } catch { messageBus.post(message: Message( level: .error, - message: "\(String.itemTagDeletedError) \(error.localizedDescription)", + message: "\(String.itemTagDeletedError) \(error.codedDescription)", autoDismiss: false )) } diff --git a/NativeAppTemplate/UI/Shop Settings/ShopBasicSettingsViewModel.swift b/NativeAppTemplate/UI/Shop Settings/ShopBasicSettingsViewModel.swift index 6d4f521..bba5758 100644 --- a/NativeAppTemplate/UI/Shop Settings/ShopBasicSettingsViewModel.swift +++ b/NativeAppTemplate/UI/Shop Settings/ShopBasicSettingsViewModel.swift @@ -73,11 +73,7 @@ final class ShopBasicSettingsViewModel { isFetching = false } catch { - messageBus.post(message: Message( - level: .error, - message: error.localizedDescription, - autoDismiss: false - )) + messageBus.post(message: Message(error: error)) shouldDismiss = true } } @@ -97,11 +93,7 @@ final class ShopBasicSettingsViewModel { _ = try await shopRepository.update(id: shop.id, shop: shop) messageBus.post(message: Message(level: .success, message: .basicSettingsUpdated)) } catch { - messageBus.post(message: Message( - level: .error, - message: error.localizedDescription, - autoDismiss: false - )) + messageBus.post(message: Message(error: error)) } isUpdating = false diff --git a/NativeAppTemplate/UI/Shop Settings/ShopSettingsViewModel.swift b/NativeAppTemplate/UI/Shop Settings/ShopSettingsViewModel.swift index a52cf7b..e3f8ab5 100644 --- a/NativeAppTemplate/UI/Shop Settings/ShopSettingsViewModel.swift +++ b/NativeAppTemplate/UI/Shop Settings/ShopSettingsViewModel.swift @@ -47,7 +47,7 @@ final class ShopSettingsViewModel { do { shop = try await shopRepository.fetchDetail(id: shopId) } catch { - messageBus.post(message: .init(level: .error, message: error.localizedDescription, autoDismiss: false)) + messageBus.post(message: Message(error: error)) shouldDismiss = true } isFetching = false @@ -65,7 +65,7 @@ final class ShopSettingsViewModel { } catch { messageBus.post(message: .init( level: .error, - message: "\(String.shopResetError) \(error.localizedDescription)", + message: "\(String.shopResetError) \(error.codedDescription)", autoDismiss: false )) } @@ -85,7 +85,7 @@ final class ShopSettingsViewModel { } catch { messageBus.post(message: .init( level: .error, - message: "\(String.shopDeletedError) \(error.localizedDescription)", + message: "\(String.shopDeletedError) \(error.codedDescription)", autoDismiss: false )) try await sessionController.logout() diff --git a/NativeAppTemplate/Utilities/MessageBus.swift b/NativeAppTemplate/Utilities/MessageBus.swift index cef1ddf..ec2e98f 100644 --- a/NativeAppTemplate/Utilities/MessageBus.swift +++ b/NativeAppTemplate/Utilities/MessageBus.swift @@ -18,6 +18,10 @@ struct Message { } extension Message { + init(error: any Error, autoDismiss: Bool = false) { + self.init(level: .error, message: error.codedDescription, autoDismiss: autoDismiss) + } + var snackbarState: SnackbarState { .init(status: level.snackbarStatus, message: message) } diff --git a/NativeAppTemplateTests/UI/Shop Detail/ShopDetailViewModelTest.swift b/NativeAppTemplateTests/UI/Shop Detail/ShopDetailViewModelTest.swift index 5d70d2e..dd835f9 100644 --- a/NativeAppTemplateTests/UI/Shop Detail/ShopDetailViewModelTest.swift +++ b/NativeAppTemplateTests/UI/Shop Detail/ShopDetailViewModelTest.swift @@ -111,7 +111,7 @@ struct ShopDetailViewModelTest { // swiftlint:disable:this type_body_length } await reloadTask.value - #expect(viewModel.messageBus.currentMessage?.message == "\(message) [Status: \(httpResponseCode)]") + #expect(viewModel.messageBus.currentMessage?.message == "[NATI-2001] \(message) [Status: \(httpResponseCode)]") #expect(viewModel.shouldDismiss) } diff --git a/NativeAppTemplateTests/UI/Shop List/ShopCreateViewModelTest.swift b/NativeAppTemplateTests/UI/Shop List/ShopCreateViewModelTest.swift index a789948..ce489f0 100644 --- a/NativeAppTemplateTests/UI/Shop List/ShopCreateViewModelTest.swift +++ b/NativeAppTemplateTests/UI/Shop List/ShopCreateViewModelTest.swift @@ -105,7 +105,7 @@ struct ShopCreateViewModelTest { } await createShopTask.value - #expect(viewModel.messageBus.currentMessage?.message == "\(message) [Status: \(httpResponseCode)]") + #expect(viewModel.messageBus.currentMessage?.message == "[NATI-2001] \(message) [Status: \(httpResponseCode)]") #expect(viewModel.isCreating) #expect(shopRepository.shops.count == createdShopsCount) #expect(viewModel.shouldDismiss) @@ -140,7 +140,7 @@ struct ShopCreateViewModelTest { } await createShopTask.value - #expect(viewModel.messageBus.currentMessage?.message == "\(message) [Status: \(httpResponseCode)]") + #expect(viewModel.messageBus.currentMessage?.message == "[NATI-2001] \(message) [Status: \(httpResponseCode)]") #expect(viewModel.isCreating) #expect(shopRepository.shops.count == createdShopsCount) #expect(viewModel.shouldDismiss == false) diff --git a/NativeAppTemplateTests/UI/Shop Settings/ShopBasicSettingsViewModelTest.swift b/NativeAppTemplateTests/UI/Shop Settings/ShopBasicSettingsViewModelTest.swift index 3705460..71a4284 100644 --- a/NativeAppTemplateTests/UI/Shop Settings/ShopBasicSettingsViewModelTest.swift +++ b/NativeAppTemplateTests/UI/Shop Settings/ShopBasicSettingsViewModelTest.swift @@ -129,7 +129,7 @@ struct ShopBasicSettingsViewModelTest { } await reloadTask.value - #expect(viewModel.messageBus.currentMessage?.message == "\(message) [Status: \(httpResponseCode)]") + #expect(viewModel.messageBus.currentMessage?.message == "[NATI-2001] \(message) [Status: \(httpResponseCode)]") #expect(viewModel.shouldDismiss) } @@ -214,7 +214,7 @@ struct ShopBasicSettingsViewModelTest { } await updateShopTask.value - #expect(viewModel.messageBus.currentMessage?.message == "\(message) [Status: \(httpResponseCode)]") + #expect(viewModel.messageBus.currentMessage?.message == "[NATI-2001] \(message) [Status: \(httpResponseCode)]") #expect(viewModel.isUpdating == false) #expect(viewModel.isBusy == false) #expect(viewModel.shouldDismiss) diff --git a/NativeAppTemplateTests/UI/Shop Settings/ShopSettingsViewModelTest.swift b/NativeAppTemplateTests/UI/Shop Settings/ShopSettingsViewModelTest.swift index fae0751..a255869 100644 --- a/NativeAppTemplateTests/UI/Shop Settings/ShopSettingsViewModelTest.swift +++ b/NativeAppTemplateTests/UI/Shop Settings/ShopSettingsViewModelTest.swift @@ -96,7 +96,7 @@ struct ShopSettingsViewModelTest { } await reloadTask.value - #expect(viewModel.messageBus.currentMessage?.message == "\(message) [Status: \(httpResponseCode)]") + #expect(viewModel.messageBus.currentMessage?.message == "[NATI-2001] \(message) [Status: \(httpResponseCode)]") #expect(viewModel.shouldDismiss) #expect(viewModel.isFetching == false) #expect(viewModel.isBusy == false) @@ -169,7 +169,7 @@ struct ShopSettingsViewModelTest { await resetShopTask.value #expect(viewModel.messageBus.currentMessage?.message == - "\(String.shopResetError) \(message) [Status: \(httpResponseCode)]") + "\(String.shopResetError) [NATI-2001] \(message) [Status: \(httpResponseCode)]") #expect(viewModel.isResetting) #expect(viewModel.isBusy) #expect(viewModel.shouldDismiss) @@ -241,7 +241,7 @@ struct ShopSettingsViewModelTest { await destroyShopTask.value #expect(viewModel.messageBus.currentMessage?.message == - "\(String.shopDeletedError) \(message) [Status: \(httpResponseCode)]") + "\(String.shopDeletedError) [NATI-2001] \(message) [Status: \(httpResponseCode)]") #expect(viewModel.isDeleting) #expect(viewModel.isBusy) #expect(sessionController.userState == .notLoggedIn) diff --git a/NativeAppTemplateTests/Utilities/AppErrorTest.swift b/NativeAppTemplateTests/Utilities/AppErrorTest.swift new file mode 100644 index 0000000..64403a6 --- /dev/null +++ b/NativeAppTemplateTests/Utilities/AppErrorTest.swift @@ -0,0 +1,49 @@ +// +// AppErrorTest.swift +// NativeAppTemplate +// + +@testable import NativeAppTemplate +import Testing + +@Suite +struct AppErrorTest { + @Test + func unexpectedErrorCode() { + let error = AppError.unexpected(description: "Something broke") + #expect(error.errorCode == "NATI-1001") + } + + @Test + func unexpectedErrorDescription() { + let error = AppError.unexpected(description: "Something broke") + #expect(error.errorDescription == "An unexpected error occurred. Something broke") + } + + @Test + func unexpectedFormattedDescription() { + let error = AppError.unexpected(description: "Something broke") + #expect(error.formattedDescription == "[NATI-1001] An unexpected error occurred. Something broke") + } + + @Test + func unexpectedDebugDescription() { + let error = AppError.unexpected( + description: "Something broke", + file: "TestFile.swift", + line: 42, + function: "testFunc()" + ) + #expect(error.debugDescription.contains("NATI-1001")) + #expect(error.debugDescription.contains("Something broke")) + #expect(error.debugDescription.contains("TestFile.swift")) + #expect(error.debugDescription.contains("42")) + #expect(error.debugDescription.contains("testFunc()")) + } + + @Test + func codedDescriptionViaErrorExtension() { + let error: Error = AppError.unexpected(description: "Test") + #expect(error.codedDescription == "[NATI-1001] An unexpected error occurred. Test") + } +} diff --git a/NativeAppTemplateTests/Utilities/CodedErrorTest.swift b/NativeAppTemplateTests/Utilities/CodedErrorTest.swift new file mode 100644 index 0000000..bce9ad5 --- /dev/null +++ b/NativeAppTemplateTests/Utilities/CodedErrorTest.swift @@ -0,0 +1,49 @@ +// +// CodedErrorTest.swift +// NativeAppTemplate +// + +import Foundation +@testable import NativeAppTemplate +import Testing + +@Suite +struct CodedErrorTest { + struct TestCodedError: CodedError { + var errorCode: String { "NATI-9999" } + var errorDescription: String? { "Test error description" } + } + + @Test + func formattedDescription() { + let error = TestCodedError() + #expect(error.formattedDescription == "[NATI-9999] Test error description") + } + + @Test + func codedDescriptionWithCodedError() { + let error: Error = TestCodedError() + #expect(error.codedDescription == "[NATI-9999] Test error description") + } + + @Test + func codedDescriptionWithNonCodedError() { + let error: Error = NSError( + domain: "TestDomain", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "Plain error"] + ) + #expect(error.codedDescription == "Plain error") + } + + struct NilDescriptionError: CodedError { + var errorCode: String { "NATI-0000" } + var errorDescription: String? { nil } + } + + @Test + func formattedDescriptionWithNilErrorDescription() { + let error = NilDescriptionError() + #expect(error.formattedDescription == "[NATI-0000] Unknown error") + } +} diff --git a/NativeAppTemplateTests/Utilities/MessageBusTest.swift b/NativeAppTemplateTests/Utilities/MessageBusTest.swift index 649c2d5..d0b5ba3 100644 --- a/NativeAppTemplateTests/Utilities/MessageBusTest.swift +++ b/NativeAppTemplateTests/Utilities/MessageBusTest.swift @@ -72,6 +72,38 @@ struct MessageBusTest { #expect(snackbarState.message == "Error occurred") } + @Test + func messageInitWithCodedError() { + let error = NativeAppTemplateAPIError.noData + let message = Message(error: error) + + #expect(message.level == .error) + #expect(message.message == "[NATI-2005] NativeAppTemplateAPIError::NoData") + #expect(message.autoDismiss == false) + } + + @Test + func messageInitWithNonCodedError() { + let error = NSError( + domain: "TestDomain", + code: 42, + userInfo: [NSLocalizedDescriptionKey: "Something went wrong"] + ) + let message = Message(error: error) + + #expect(message.level == .error) + #expect(message.message == "Something went wrong") + #expect(message.autoDismiss == false) + } + + @Test + func messageInitWithErrorAutoDismiss() { + let error = NativeAppTemplateAPIError.noData + let message = Message(error: error, autoDismiss: true) + + #expect(message.autoDismiss == true) + } + @Test func messageLevelSnackbarStatus() { #expect(Message.Level.error.snackbarStatus == .error) diff --git a/NativeAppTemplateTests/Utilities/NFCErrorTest.swift b/NativeAppTemplateTests/Utilities/NFCErrorTest.swift new file mode 100644 index 0000000..e6812b2 --- /dev/null +++ b/NativeAppTemplateTests/Utilities/NFCErrorTest.swift @@ -0,0 +1,34 @@ +// +// NFCErrorTest.swift +// NativeAppTemplate +// + +@testable import NativeAppTemplate +import Testing + +@Suite +struct NFCErrorTest { + @Test + func scanFailedErrorCode() { + let error = NFCError.scanFailed("Tag not valid") + #expect(error.errorCode == "NATI-3001") + } + + @Test + func scanFailedErrorDescription() { + let error = NFCError.scanFailed("Tag not valid") + #expect(error.errorDescription == "Tag not valid") + } + + @Test + func scanFailedFormattedDescription() { + let error = NFCError.scanFailed("Tag not valid") + #expect(error.formattedDescription == "[NATI-3001] Tag not valid") + } + + @Test + func codedDescriptionViaErrorExtension() { + let error: Error = NFCError.scanFailed("Scan failed message") + #expect(error.codedDescription == "[NATI-3001] Scan failed message") + } +} diff --git a/NativeAppTemplateTests/Utilities/NativeAppTemplateAPIErrorTest.swift b/NativeAppTemplateTests/Utilities/NativeAppTemplateAPIErrorTest.swift new file mode 100644 index 0000000..dce12bf --- /dev/null +++ b/NativeAppTemplateTests/Utilities/NativeAppTemplateAPIErrorTest.swift @@ -0,0 +1,95 @@ +// +// NativeAppTemplateAPIErrorTest.swift +// NativeAppTemplate +// + +@testable import NativeAppTemplate +import Testing + +@Suite +struct NativeAppTemplateAPIErrorTest { + @Test + func requestFailedErrorCode() { + let error = NativeAppTemplateAPIError.requestFailed(nil, 500, "Server error") + #expect(error.errorCode == "NATI-2001") + } + + @Test + func processingErrorErrorCode() { + let error = NativeAppTemplateAPIError.processingError(nil) + #expect(error.errorCode == "NATI-2002") + } + + @Test + func responseMissingRequiredMetaErrorCode() { + let error = NativeAppTemplateAPIError.responseMissingRequiredMeta(field: "total") + #expect(error.errorCode == "NATI-2003") + } + + @Test + func responseHasIncorrectNumberOfElementsErrorCode() { + let error = NativeAppTemplateAPIError.responseHasIncorrectNumberOfElements + #expect(error.errorCode == "NATI-2004") + } + + @Test + func noDataErrorCode() { + let error = NativeAppTemplateAPIError.noData + #expect(error.errorCode == "NATI-2005") + } + + @Test + func requestFailedWithMessage() { + let error = NativeAppTemplateAPIError.requestFailed(nil, 422, "Validation failed") + #expect(error.errorDescription == "Validation failed [Status: 422]") + #expect(error.formattedDescription == "[NATI-2001] Validation failed [Status: 422]") + } + + @Test + func requestFailedWithoutMessage() { + let error = NativeAppTemplateAPIError.requestFailed(nil, 500, nil) + let description = error.errorDescription! + #expect(description.contains("RequestFailed")) + #expect(description.contains("500")) + #expect(description.contains("UNKNOWN")) + } + + @Test + func processingErrorDescription() { + let error = NativeAppTemplateAPIError.processingError(nil) + #expect(error.errorDescription!.contains("ProcessingError")) + #expect(error.errorDescription!.contains("UNKNOWN")) + } + + @Test + func responseMissingRequiredMetaDescription() { + let error = NativeAppTemplateAPIError.responseMissingRequiredMeta(field: "count") + #expect(error.errorDescription!.contains("ResponseMissingRequiredMeta")) + #expect(error.errorDescription!.contains("count")) + } + + @Test + func responseMissingRequiredMetaDescriptionNilField() { + let error = NativeAppTemplateAPIError.responseMissingRequiredMeta(field: nil) + #expect(error.errorDescription!.contains("UNKNOWN")) + } + + @Test + func responseHasIncorrectNumberOfElementsDescription() { + let error = NativeAppTemplateAPIError.responseHasIncorrectNumberOfElements + #expect(error.errorDescription!.contains("ResponseHasIncorrectNumberOfElements")) + } + + @Test + func noDataDescription() { + let error = NativeAppTemplateAPIError.noData + #expect(error.errorDescription == "NativeAppTemplateAPIError::NoData") + #expect(error.formattedDescription == "[NATI-2005] NativeAppTemplateAPIError::NoData") + } + + @Test + func codedDescriptionViaErrorExtension() { + let error: Error = NativeAppTemplateAPIError.requestFailed(nil, 404, "Not found") + #expect(error.codedDescription == "[NATI-2001] Not found [Status: 404]") + } +} From 8e8338c049eb277ad756c3fed3446d6cb412051b Mon Sep 17 00:00:00 2001 From: dadachi Date: Thu, 2 Apr 2026 15:33:41 +0900 Subject: [PATCH 2/2] Add missing Foundation import to MessageBusTest for NSError Co-Authored-By: Claude Opus 4.6 (1M context) --- NativeAppTemplateTests/Utilities/MessageBusTest.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/NativeAppTemplateTests/Utilities/MessageBusTest.swift b/NativeAppTemplateTests/Utilities/MessageBusTest.swift index d0b5ba3..df158b8 100644 --- a/NativeAppTemplateTests/Utilities/MessageBusTest.swift +++ b/NativeAppTemplateTests/Utilities/MessageBusTest.swift @@ -3,6 +3,7 @@ // NativeAppTemplate // +import Foundation @testable import NativeAppTemplate import Testing