Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 32 additions & 4 deletions NativeAppTemplate.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -258,7 +261,6 @@
0172785E2D7D83E700CE424F /* ItemTagInfoFromNdefMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTagInfoFromNdefMessage.swift; sourceTree = "<group>"; };
0172785F2D7D83E700CE424F /* ItemTagState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTagState.swift; sourceTree = "<group>"; };
017278602D7D83E700CE424F /* ItemTagType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTagType.swift; sourceTree = "<group>"; };
017278662D7D83F600CE424F /* ScanResultError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanResultError.swift; sourceTree = "<group>"; };
017278672D7D83F600CE424F /* ScanState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanState.swift; sourceTree = "<group>"; };
0172786A2D7D840A00CE424F /* ShowTagInfoScanResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowTagInfoScanResult.swift; sourceTree = "<group>"; };
0172786C2D7D87D000CE424F /* Date+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -341,6 +343,10 @@
01EE363D29A6DCEB009BCD9D /* ShopkeeperEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopkeeperEditView.swift; sourceTree = "<group>"; };
01FC03E12B3329B700E6CD8E /* NeedAppUpdatesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NeedAppUpdatesView.swift; sourceTree = "<group>"; };
C597C0551370446BB931F19B /* CertificatePinningDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificatePinningDelegate.swift; sourceTree = "<group>"; };
A2B3C4D500000001 /* CodedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodedError.swift; sourceTree = "<group>"; };
A2B3C4D500000003 /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = "<group>"; };
A2B3C4D500000005 /* NFCError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NFCError.swift; sourceTree = "<group>"; };
A2B3C4D500000007 /* NativeAppTemplateAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeAppTemplateAPIError.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFileSystemSynchronizedRootGroup section */
Expand Down Expand Up @@ -411,6 +417,7 @@
011F6DF9259EF16600BED22E /* Info.plist */,
0172782B2D7D575900CE424F /* NativeAppTemplate.entitlements */,
015C78042B72DA2C00B6523C /* PrivacyInfo.xcprivacy */,
A2B3C4D50000000A /* Common */,
0172049125AA8449008FD63B /* Data */,
017203A725A96FBF008FD63B /* Extensions */,
017203E925AA6601008FD63B /* Logging */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -825,6 +831,25 @@
path = Shared;
sourceTree = "<group>";
};
A2B3C4D500000009 /* Errors */ = {
isa = PBXGroup;
children = (
A2B3C4D500000003 /* AppError.swift */,
A2B3C4D500000001 /* CodedError.swift */,
A2B3C4D500000007 /* NativeAppTemplateAPIError.swift */,
A2B3C4D500000005 /* NFCError.swift */,
);
path = Errors;
sourceTree = "<group>";
};
A2B3C4D50000000A /* Common */ = {
isa = PBXGroup;
children = (
A2B3C4D500000009 /* Errors */,
);
path = Common;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
Expand Down Expand Up @@ -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 */,
Expand Down
36 changes: 36 additions & 0 deletions NativeAppTemplate/Common/Errors/AppError.swift
Original file line number Diff line number Diff line change
@@ -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))"
}
}
}
26 changes: 26 additions & 0 deletions NativeAppTemplate/Common/Errors/CodedError.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
24 changes: 24 additions & 0 deletions NativeAppTemplate/Common/Errors/NFCError.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
49 changes: 49 additions & 0 deletions NativeAppTemplate/Common/Errors/NativeAppTemplateAPIError.swift
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
16 changes: 8 additions & 8 deletions NativeAppTemplate/Data/Repositories/ItemTagRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand Down
12 changes: 6 additions & 6 deletions NativeAppTemplate/Data/Repositories/ShopRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand Down
Loading
Loading