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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added
- Pagination support for item tags list
- CodedError system with `NATI-XXXX` prefixed error codes
- CodedError system with `NATIVEAPPTEMPLATE-XXXX` prefixed error codes
- App version display in settings
- Design system constants (spacing, animation, glass, layout, corner radius)
- GlassButtonStyle and GlassCard components with glassmorphism styling
Expand Down
9 changes: 4 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,18 +107,17 @@ NativeAppTemplate/
```

### 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`).
All errors use the `CodedError` protocol in `NativeAppTemplate/Common/Errors/`. Error codes share the `NATIVEAPPTEMPLATE-XXXX` prefix across iOS and Android.

| Range | Type | File |
|-------|------|------|
| NATI-1xxx | App/general errors | `AppError.swift` |
| NATI-2xxx | API/network errors | `NativeAppTemplateAPIError.swift` |
| NATI-3xxx | NFC/scan errors | `NFCError.swift` |
| NATIVEAPPTEMPLATE-1xxx | App/general errors | `AppError.swift` |
| NATIVEAPPTEMPLATE-2xxx | API/network errors | `NativeAppTemplateAPIError.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)
- Error code numbers must match across iOS and Android

### Dependencies (Swift Package Manager)
- KeychainAccess (4.2.2) - Secure credential storage
Expand Down
2 changes: 1 addition & 1 deletion NativeAppTemplate/Common/Errors/AppError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ enum AppError: CodedError {
var errorCode: String {
switch self {
case .unexpected:
"NATI-1001"
"NATIVEAPPTEMPLATE-1001"
}
}

Expand Down
3 changes: 1 addition & 2 deletions NativeAppTemplate/Common/Errors/CodedError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
// NativeAppTemplate
//

// Error codes use the `NATI-XXXX` prefix (NativeAppTemplate iOS).
// Android uses `NATA-XXXX`.
// Error codes share the `NATIVEAPPTEMPLATE-XXXX` prefix across iOS and Android.
// Ranges: 1xxx App errors, 2xxx API errors.

import Foundation
Expand Down
10 changes: 5 additions & 5 deletions NativeAppTemplate/Common/Errors/NativeAppTemplateAPIError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ enum NativeAppTemplateAPIError: CodedError {
nonisolated var errorCode: String {
switch self {
case .requestFailed:
"NATI-2001"
"NATIVEAPPTEMPLATE-2001"
case .processingError:
"NATI-2002"
"NATIVEAPPTEMPLATE-2002"
case .responseMissingRequiredMeta:
"NATI-2003"
"NATIVEAPPTEMPLATE-2003"
case .responseHasIncorrectNumberOfElements:
"NATI-2004"
"NATIVEAPPTEMPLATE-2004"
case .noData:
"NATI-2005"
"NATIVEAPPTEMPLATE-2005"
}
}

Expand Down
6 changes: 3 additions & 3 deletions NativeAppTemplate/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@ enum NativeAppTemplateConstants {
enum Strings {
#if DEBUG
private static let env = ProcessInfo.processInfo.environment
static let scheme: String = env["NATEMPLATE_API_SCHEME"] ?? "https"
static let domain: String = env["NATEMPLATE_API_DOMAIN"] ?? "api.nativeapptemplate.com"
static let port: String = env["NATEMPLATE_API_PORT"] ?? ""
static let scheme: String = env["NATIVEAPPTEMPLATE_API_SCHEME"] ?? "https"
static let domain: String = env["NATIVEAPPTEMPLATE_API_DOMAIN"] ?? "api.nativeapptemplate.com"
static let port: String = env["NATIVEAPPTEMPLATE_API_PORT"] ?? ""
#else
static let scheme: String = "https"
static let domain: String = "api.nativeapptemplate.com"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ struct ShopDetailViewModelTest { // swiftlint:disable:this type_body_length
}
await reloadTask.value

#expect(viewModel.messageBus.currentMessage?.message == "[NATI-2001] \(message) [Status: \(httpResponseCode)]")
#expect(viewModel.messageBus.currentMessage?.message == "[NATIVEAPPTEMPLATE-2001] \(message) [Status: \(httpResponseCode)]")
#expect(viewModel.shouldDismiss)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ struct ShopCreateViewModelTest {
}
await createShopTask.value

#expect(viewModel.messageBus.currentMessage?.message == "[NATI-2001] \(message) [Status: \(httpResponseCode)]")
#expect(viewModel.messageBus.currentMessage?.message == "[NATIVEAPPTEMPLATE-2001] \(message) [Status: \(httpResponseCode)]")
#expect(viewModel.isCreating)
#expect(shopRepository.shops.count == createdShopsCount)
#expect(viewModel.shouldDismiss)
Expand Down Expand Up @@ -215,7 +215,7 @@ struct ShopCreateViewModelTest {
}
await createShopTask.value

#expect(viewModel.messageBus.currentMessage?.message == "[NATI-2001] \(message) [Status: \(httpResponseCode)]")
#expect(viewModel.messageBus.currentMessage?.message == "[NATIVEAPPTEMPLATE-2001] \(message) [Status: \(httpResponseCode)]")
#expect(viewModel.isCreating)
#expect(shopRepository.shops.count == createdShopsCount)
#expect(viewModel.shouldDismiss == false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ struct ShopBasicSettingsViewModelTest {
}
await reloadTask.value

#expect(viewModel.messageBus.currentMessage?.message == "[NATI-2001] \(message) [Status: \(httpResponseCode)]")
#expect(viewModel.messageBus.currentMessage?.message == "[NATIVEAPPTEMPLATE-2001] \(message) [Status: \(httpResponseCode)]")
#expect(viewModel.shouldDismiss)
}

Expand Down Expand Up @@ -335,7 +335,7 @@ struct ShopBasicSettingsViewModelTest {
}
await updateShopTask.value

#expect(viewModel.messageBus.currentMessage?.message == "[NATI-2001] \(message) [Status: \(httpResponseCode)]")
#expect(viewModel.messageBus.currentMessage?.message == "[NATIVEAPPTEMPLATE-2001] \(message) [Status: \(httpResponseCode)]")
#expect(viewModel.isUpdating == false)
#expect(viewModel.isBusy == false)
#expect(viewModel.shouldDismiss)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ struct ShopSettingsViewModelTest {
}
await reloadTask.value

#expect(viewModel.messageBus.currentMessage?.message == "[NATI-2001] \(message) [Status: \(httpResponseCode)]")
#expect(viewModel.messageBus.currentMessage?.message == "[NATIVEAPPTEMPLATE-2001] \(message) [Status: \(httpResponseCode)]")
#expect(viewModel.shouldDismiss)
#expect(viewModel.isFetching == false)
#expect(viewModel.isBusy == false)
Expand Down Expand Up @@ -168,7 +168,7 @@ struct ShopSettingsViewModelTest {
await destroyShopTask.value

#expect(viewModel.messageBus.currentMessage?.message ==
"\(Strings.shopDeletedError) [NATI-2001] \(message) [Status: \(httpResponseCode)]")
"\(Strings.shopDeletedError) [NATIVEAPPTEMPLATE-2001] \(message) [Status: \(httpResponseCode)]")
#expect(viewModel.isDeleting)
#expect(viewModel.isBusy)
#expect(sessionController.userState == .notLoggedIn)
Expand Down
8 changes: 4 additions & 4 deletions NativeAppTemplateTests/Utilities/AppErrorTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ struct AppErrorTest {
@Test
func unexpectedErrorCode() {
let error = AppError.unexpected(description: "Something broke")
#expect(error.errorCode == "NATI-1001")
#expect(error.errorCode == "NATIVEAPPTEMPLATE-1001")
}

@Test
Expand All @@ -23,7 +23,7 @@ struct AppErrorTest {
@Test
func unexpectedFormattedDescription() {
let error = AppError.unexpected(description: "Something broke")
#expect(error.formattedDescription == "[NATI-1001] An unexpected error occurred. Something broke")
#expect(error.formattedDescription == "[NATIVEAPPTEMPLATE-1001] An unexpected error occurred. Something broke")
}

@Test
Expand All @@ -34,7 +34,7 @@ struct AppErrorTest {
line: 42,
function: "testFunc()"
)
#expect(error.debugDescription.contains("NATI-1001"))
#expect(error.debugDescription.contains("NATIVEAPPTEMPLATE-1001"))
#expect(error.debugDescription.contains("Something broke"))
#expect(error.debugDescription.contains("TestFile.swift"))
#expect(error.debugDescription.contains("42"))
Expand All @@ -44,6 +44,6 @@ struct AppErrorTest {
@Test
func codedDescriptionViaErrorExtension() {
let error: Error = AppError.unexpected(description: "Test")
#expect(error.codedDescription == "[NATI-1001] An unexpected error occurred. Test")
#expect(error.codedDescription == "[NATIVEAPPTEMPLATE-1001] An unexpected error occurred. Test")
}
}
10 changes: 5 additions & 5 deletions NativeAppTemplateTests/Utilities/CodedErrorTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,20 @@ import Testing
@Suite
struct CodedErrorTest {
struct TestCodedError: CodedError {
var errorCode: String { "NATI-9999" }
var errorCode: String { "NATIVEAPPTEMPLATE-9999" }
var errorDescription: String? { "Test error description" }
}

@Test
func formattedDescription() {
let error = TestCodedError()
#expect(error.formattedDescription == "[NATI-9999] Test error description")
#expect(error.formattedDescription == "[NATIVEAPPTEMPLATE-9999] Test error description")
}

@Test
func codedDescriptionWithCodedError() {
let error: Error = TestCodedError()
#expect(error.codedDescription == "[NATI-9999] Test error description")
#expect(error.codedDescription == "[NATIVEAPPTEMPLATE-9999] Test error description")
}

@Test
Expand All @@ -37,13 +37,13 @@ struct CodedErrorTest {
}

struct NilDescriptionError: CodedError {
var errorCode: String { "NATI-0000" }
var errorCode: String { "NATIVEAPPTEMPLATE-0000" }
var errorDescription: String? { nil }
}

@Test
func formattedDescriptionWithNilErrorDescription() {
let error = NilDescriptionError()
#expect(error.formattedDescription == "[NATI-0000] Unknown error")
#expect(error.formattedDescription == "[NATIVEAPPTEMPLATE-0000] Unknown error")
}
}
2 changes: 1 addition & 1 deletion NativeAppTemplateTests/Utilities/MessageBusTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ struct MessageBusTest {
let message = Message(error: error)

#expect(message.level == .error)
#expect(message.message == "[NATI-2005] NativeAppTemplateAPIError::NoData")
#expect(message.message == "[NATIVEAPPTEMPLATE-2005] NativeAppTemplateAPIError::NoData")
#expect(message.autoDismiss == false)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,38 +11,38 @@ struct NativeAppTemplateAPIErrorTest {
@Test
func requestFailedErrorCode() {
let error = NativeAppTemplateAPIError.requestFailed(nil, 500, "Server error")
#expect(error.errorCode == "NATI-2001")
#expect(error.errorCode == "NATIVEAPPTEMPLATE-2001")
}

@Test
func processingErrorErrorCode() {
let error = NativeAppTemplateAPIError.processingError(nil)
#expect(error.errorCode == "NATI-2002")
#expect(error.errorCode == "NATIVEAPPTEMPLATE-2002")
}

@Test
func responseMissingRequiredMetaErrorCode() {
let error = NativeAppTemplateAPIError.responseMissingRequiredMeta(field: "total")
#expect(error.errorCode == "NATI-2003")
#expect(error.errorCode == "NATIVEAPPTEMPLATE-2003")
}

@Test
func responseHasIncorrectNumberOfElementsErrorCode() {
let error = NativeAppTemplateAPIError.responseHasIncorrectNumberOfElements
#expect(error.errorCode == "NATI-2004")
#expect(error.errorCode == "NATIVEAPPTEMPLATE-2004")
}

@Test
func noDataErrorCode() {
let error = NativeAppTemplateAPIError.noData
#expect(error.errorCode == "NATI-2005")
#expect(error.errorCode == "NATIVEAPPTEMPLATE-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]")
#expect(error.formattedDescription == "[NATIVEAPPTEMPLATE-2001] Validation failed [Status: 422]")
}

@Test
Expand Down Expand Up @@ -84,12 +84,12 @@ struct NativeAppTemplateAPIErrorTest {
func noDataDescription() {
let error = NativeAppTemplateAPIError.noData
#expect(error.errorDescription == "NativeAppTemplateAPIError::NoData")
#expect(error.formattedDescription == "[NATI-2005] NativeAppTemplateAPIError::NoData")
#expect(error.formattedDescription == "[NATIVEAPPTEMPLATE-2005] NativeAppTemplateAPIError::NoData")
}

@Test
func codedDescriptionViaErrorExtension() {
let error: Error = NativeAppTemplateAPIError.requestFailed(nil, 404, "Not found")
#expect(error.codedDescription == "[NATI-2001] Not found [Status: 404]")
#expect(error.codedDescription == "[NATIVEAPPTEMPLATE-2001] Not found [Status: 404]")
}
}
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,12 @@ To run this app successfully, ensure you have:
To connect to a local API server, set these env vars on the Xcode scheme (Edit Scheme → Run → Arguments → Environment Variables):

```
NATEMPLATE_API_SCHEME = http
NATEMPLATE_API_DOMAIN = <your-lan-ip>
NATEMPLATE_API_PORT = 3000
NATIVEAPPTEMPLATE_API_SCHEME = http
NATIVEAPPTEMPLATE_API_DOMAIN = <your-lan-ip>
NATIVEAPPTEMPLATE_API_PORT = 3000
```

> **Note:** Never use `127.0.0.1`, `localhost`, or `0.0.0.0` for `NATEMPLATE_API_DOMAIN` — those resolve to the iOS Simulator/device itself, not your Mac. Use your Mac's LAN IP (e.g., `192.168.1.6`) so the simulator or a physical device can reach the API server.
> **Note:** Never use `127.0.0.1`, `localhost`, or `0.0.0.0` for `NATIVEAPPTEMPLATE_API_DOMAIN` — those resolve to the iOS Simulator/device itself, not your Mac. Use your Mac's LAN IP (e.g., `192.168.1.6`) so the simulator or a physical device can reach the API server.

Keep the scheme in `xcuserdata` (per-developer, gitignored), not `xcshareddata`. In Xcode, open **Product → Scheme → Manage Schemes…**, find `NativeAppTemplate`, and **uncheck "Shared"**. This moves the scheme (with your local env vars) to `xcuserdata/<user>.xcuserdatad/xcschemes/` so your API settings are not committed. If Xcode staged a deletion of the previously shared scheme, restore it with:

Expand Down
Loading