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
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ All errors use the `CodedError` protocol in `NativeAppTemplate/Common/Errors/`.

- New error types must conform to `CodedError` and be placed in `Common/Errors/`
- Use `error.codedDescription` (not `error.localizedDescription`) in all error messages
- `errorDescription` is what the user sees, so it is localized (`String(localized:)` in `Constants.swift`): the API's `error_message` as is, or a generic text ("Something went wrong while communicating with the server. Please try again later." when a request fails without one, "An unexpected error occurred." for developer-only errors such as malformed responses or Keychain failures). Developer details (status codes, field names, underlying errors) go to `debugDescription`; log errors with `error.debugCodedDescription`, never show it
- Use `Message(error: error)` convenience to post errors to `MessageBus`
- Error code numbers must match across iOS and Android

Expand All @@ -128,7 +129,8 @@ The app ships in English (development language) and Japanese. **Every new user-f
- Keep the keys in English: nativeapptemplate-agent renames the English nouns in code and catalog keys together.
- After building, Xcode adds new keys to `NativeAppTemplate/Localizable.xcstrings` (from the command line: `xcrun xcstringstool sync NativeAppTemplate/Localizable.xcstrings --stringsdata <each .stringsdata in DerivedData>`). Add the `ja` value with state `translated`, keeping every `%@`/`%lld` (use `%1$@`, `%2$@` to reorder). Permission prompts go in `InfoPlist.xcstrings`.
- Use the API's terms (`config/locales/ja.yml`), the same as the paid NativeAppTemplate-iOS: Shop is **店舗** (never ショップ/店/お店), ItemTag is **アイテムタグ** in full (never タグ or アイテム alone; the agent renames these Japanese nouns as substrings), Idled/Completed 未完了/完了. Don't translate the product name.
- Keep untranslated: product names, URLs, `NATIVEAPPTEMPLATE-xxxx` error codes and `CodedError` descriptions, log messages, the API's `error_message` (already localized by the server), and text the user typed.
- User-facing error text is localized, like any other UI text (`CodedError.errorDescription`); developer details go to logs (`debugDescription`).
- Keep untranslated: product names, URLs, `NATIVEAPPTEMPLATE-xxxx` error codes, log messages, the API's `error_message` (already localized by the server), and text the user typed.
- Tests run in English (the scheme's test action sets `language = "en"`).
- The app's language is set in iOS Settings, not in the app. Edit Profile shows it read-only; `SessionController` keeps `shopkeepers.locale` in step with it so emails match.

Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed
- Error messages are localized (English and Japanese). A request that fails without the API's `error_message` shows "Something went wrong while communicating with the server. Please try again later." (`NATIVEAPPTEMPLATE-2001`); developer-only errors (`NATIVEAPPTEMPLATE-1001`, `2002`–`2008`, `4001`–`4004`) show "An unexpected error occurred." The API's `error_message` is shown without the " [Status: nnn]" suffix. Error codes are unchanged; status codes, field names and underlying errors go to the log (`CodedError.debugDescription`)

### Fixed
- Debug builds lost the local API when launched other than from Xcode, e.g. from the Home Screen or when iOS relaunched the app after its language was changed in Settings: the endpoint came only from the scheme's environment variables, which only Xcode passes. It now comes from `API.xcconfig` (override it in the gitignored `API.local.xcconfig`) and is built into `Info.plist`; `NATIVEAPPTEMPLATE_API_*` environment variables still win. The scheme no longer sets them

Expand Down
12 changes: 6 additions & 6 deletions NativeAppTemplate.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@
017204D925AA847E008FD63B /* ShopRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017204A925AA8449008FD63B /* ShopRepository.swift */; };
017204FA25AA860F008FD63B /* MessageBus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017204F925AA85F3008FD63B /* MessageBus.swift */; };
0172051A25AAF6C0008FD63B /* SessionsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172051925AAF6BF008FD63B /* SessionsService.swift */; };
3DCDACF2A0069F085A61F98B /* RootPathService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF815E3C15C1BC7D878EE20 /* RootPathService.swift */; };
0172052525AAFA43008FD63B /* Shopkeeper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172052425AAFA43008FD63B /* Shopkeeper.swift */; };
0172052F25AC41A7008FD63B /* SessionRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172052E25AC41A7008FD63B /* SessionRequest.swift */; };
017278622D7D83E700CE424F /* ItemTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172785C2D7D83E700CE424F /* ItemTag.swift */; };
Expand All @@ -84,7 +83,6 @@
0172789D2D7D99D100CE424F /* ItemTagCreateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017278962D7D99D100CE424F /* ItemTagCreateView.swift */; };
0172789E2D7D99D100CE424F /* ItemTagEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017278942D7D99D100CE424F /* ItemTagEditView.swift */; };
0182D37825B277FA001E881D /* KeychainStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0182D37625B277D6001E881D /* KeychainStore.swift */; };
A1B2C3D4E5F6A7B8C9D0E1F2 /* KeychainReinstallCleanup.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F6A7B8C9D0E1F3 /* KeychainReinstallCleanup.swift */; };
0182D38225B296B9001E881D /* ShopkeeperAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0182D38125B296B9001E881D /* ShopkeeperAdapter.swift */; };
0182D39A25B4424B001E881D /* LoggedInShopkeeperKeychainStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0182D39925B4424B001E881D /* LoggedInShopkeeperKeychainStore.swift */; };
018E21CB2B36367F00FFD1F6 /* MeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018E21CA2B36367F00FFD1F6 /* MeRequest.swift */; };
Expand Down Expand Up @@ -128,7 +126,6 @@
01E0A5B725BD0FCD00298D35 /* OfflineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E0A5B225BD0FC700298D35 /* OfflineView.swift */; };
01E0A5B825BD0FCD00298D35 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E0A5B325BD0FC700298D35 /* ErrorView.swift */; };
01E0A60125BD149200298D35 /* MainButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E0A5F925BD148800298D35 /* MainButtonView.swift */; };
E6D6F71FD22DECE8CC643108 /* SaveThenDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99D40C223000EA09B764706B /* SaveThenDismiss.swift */; };
01E0A60C25BD440300298D35 /* SignInEmailAndPasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E0A60B25BD440300298D35 /* SignInEmailAndPasswordView.swift */; };
01E0A63025BD53FD00298D35 /* Shop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E0A62F25BD53FD00298D35 /* Shop.swift */; };
01E1CECC287787A700E724FC /* SignUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E1CECB287787A700E724FC /* SignUpView.swift */; };
Expand All @@ -138,15 +135,18 @@
01ED197B2A037B9E00CD4735 /* AppTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01ED197A2A037B9E00CD4735 /* AppTabView.swift */; };
01EE363E29A6DCEB009BCD9D /* ShopkeeperEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01EE363D29A6DCEB009BCD9D /* ShopkeeperEditView.swift */; };
01FC03E22B3329B700E6CD8E /* NeedAppUpdatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01FC03E12B3329B700E6CD8E /* NeedAppUpdatesView.swift */; };
3DCDACF2A0069F085A61F98B /* RootPathService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF815E3C15C1BC7D878EE20 /* RootPathService.swift */; };
4A8DA0DEF6F142C3A127058A /* PaginationMeta.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE8A6D1D27C458389C4F61A /* PaginationMeta.swift */; };
7249A60C06FE44338E16BC50 /* APISessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C597C0551370446BB931F19B /* APISessionDelegate.swift */; };
A1B2C3D401000003 /* GlassCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D401000004 /* GlassCard.swift */; };
A1B2C3D4E5F6A7B8C9D0E1F2 /* KeychainReinstallCleanup.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F6A7B8C9D0E1F3 /* KeychainReinstallCleanup.swift */; };
A2B3C4D500000002 /* CodedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2B3C4D500000001 /* CodedError.swift */; };
A2B3C4D500000004 /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2B3C4D500000003 /* AppError.swift */; };
A2B3C4D500000008 /* NativeAppTemplateAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2B3C4D500000007 /* NativeAppTemplateAPIError.swift */; };
A2B3C4D50000000C /* EntityAdapterError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2B3C4D50000000B /* EntityAdapterError.swift */; };
A2B3C4D50000000E /* KeychainStoreError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2B3C4D50000000D /* KeychainStoreError.swift */; };
A370BAECACECFAB76D851427 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 412B1F0A34D186286A11F085 /* Keychain.swift */; };
E6D6F71FD22DECE8CC643108 /* SaveThenDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99D40C223000EA09B764706B /* SaveThenDismiss.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -218,7 +218,6 @@
017204A925AA8449008FD63B /* ShopRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopRepository.swift; sourceTree = "<group>"; };
017204F925AA85F3008FD63B /* MessageBus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBus.swift; sourceTree = "<group>"; };
0172051925AAF6BF008FD63B /* SessionsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionsService.swift; sourceTree = "<group>"; };
DCF815E3C15C1BC7D878EE20 /* RootPathService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootPathService.swift; sourceTree = "<group>"; };
0172052425AAFA43008FD63B /* Shopkeeper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shopkeeper.swift; sourceTree = "<group>"; };
0172052E25AC41A7008FD63B /* SessionRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionRequest.swift; sourceTree = "<group>"; };
0172785C2D7D83E700CE424F /* ItemTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTag.swift; sourceTree = "<group>"; };
Expand All @@ -240,7 +239,6 @@
017278972D7D99D100CE424F /* ItemTagListCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTagListCardView.swift; sourceTree = "<group>"; };
017278982D7D99D100CE424F /* ItemTagListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTagListView.swift; sourceTree = "<group>"; };
0182D37625B277D6001E881D /* KeychainStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStore.swift; sourceTree = "<group>"; };
A1B2C3D4E5F6A7B8C9D0E1F3 /* KeychainReinstallCleanup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainReinstallCleanup.swift; sourceTree = "<group>"; };
0182D38125B296B9001E881D /* ShopkeeperAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopkeeperAdapter.swift; sourceTree = "<group>"; };
0182D39925B4424B001E881D /* LoggedInShopkeeperKeychainStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggedInShopkeeperKeychainStore.swift; sourceTree = "<group>"; };
018E21CA2B36367F00FFD1F6 /* MeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeRequest.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -286,7 +284,6 @@
01E0A5B325BD0FC700298D35 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
01E0A5B525BD0FC700298D35 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = "<group>"; };
01E0A5F925BD148800298D35 /* MainButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainButtonView.swift; sourceTree = "<group>"; };
99D40C223000EA09B764706B /* SaveThenDismiss.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveThenDismiss.swift; sourceTree = "<group>"; };
01E0A60B25BD440300298D35 /* SignInEmailAndPasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInEmailAndPasswordView.swift; sourceTree = "<group>"; };
01E0A62F25BD53FD00298D35 /* Shop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shop.swift; sourceTree = "<group>"; };
01E1CECB287787A700E724FC /* SignUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpView.swift; sourceTree = "<group>"; };
Expand All @@ -298,13 +295,16 @@
01FC03E12B3329B700E6CD8E /* NeedAppUpdatesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NeedAppUpdatesView.swift; sourceTree = "<group>"; };
2FE8A6D1D27C458389C4F61A /* PaginationMeta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationMeta.swift; sourceTree = "<group>"; };
412B1F0A34D186286A11F085 /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = "<group>"; };
99D40C223000EA09B764706B /* SaveThenDismiss.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveThenDismiss.swift; sourceTree = "<group>"; };
A1B2C3D401000004 /* GlassCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlassCard.swift; sourceTree = "<group>"; };
A1B2C3D4E5F6A7B8C9D0E1F3 /* KeychainReinstallCleanup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainReinstallCleanup.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>"; };
A2B3C4D500000007 /* NativeAppTemplateAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeAppTemplateAPIError.swift; sourceTree = "<group>"; };
A2B3C4D50000000B /* EntityAdapterError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntityAdapterError.swift; sourceTree = "<group>"; };
A2B3C4D50000000D /* KeychainStoreError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStoreError.swift; sourceTree = "<group>"; };
C597C0551370446BB931F19B /* APISessionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APISessionDelegate.swift; sourceTree = "<group>"; };
DCF815E3C15C1BC7D878EE20 /* RootPathService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootPathService.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFileSystemSynchronizedRootGroup section */
Expand Down
5 changes: 3 additions & 2 deletions NativeAppTemplate/Common/Errors/AppError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ enum AppError: CodedError {

var errorDescription: String? {
switch self {
case let .unexpected(description, _, _, _):
"An unexpected error occurred. \(description)"
case .unexpected:
// The description is developer detail: it is in debugDescription, for the log
Strings.unexpectedError
}
}

Expand Down
17 changes: 17 additions & 0 deletions NativeAppTemplate/Common/Errors/CodedError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,35 @@

import Foundation

/// `errorDescription` is what the user sees, so it is localized; developer details
/// (status codes, field names, underlying errors) go to `debugDescription`, which is
/// for the log only.
protocol CodedError: LocalizedError, Sendable {
var errorCode: String { get }

/// The error with its developer details, for the log only
var debugDescription: String { get }
}

extension CodedError {
var formattedDescription: String {
"[\(errorCode)] \(errorDescription ?? "Unknown error")"
}

/// Errors without developer details log what the user sees
var debugDescription: String {
formattedDescription
}
}

extension Error {
var codedDescription: String {
(self as? CodedError)?.formattedDescription ?? localizedDescription
}

/// Returns `debugDescription` if the error conforms to `CodedError`, otherwise
/// falls back to `localizedDescription`. For the log, not for the user.
var debugCodedDescription: String {
(self as? CodedError)?.debugDescription ?? localizedDescription
}
}
9 changes: 9 additions & 0 deletions NativeAppTemplate/Common/Errors/EntityAdapterError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,16 @@ enum EntityAdapterError: CodedError {
}
}

/// Developer-only errors: the user sees the generic text, the log the case
nonisolated var errorDescription: String? {
Strings.unexpectedError
}

nonisolated var debugDescription: String {
"[\(errorCode)] \(detail)"
}

private nonisolated var detail: String {
let prefix = "EntityAdapterError::"
switch self {
case .invalidResourceTypeForAdapter:
Expand Down
10 changes: 10 additions & 0 deletions NativeAppTemplate/Common/Errors/KeychainStoreError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,17 @@ enum KeychainStoreError: CodedError {
}
}

/// Developer-only errors: the user sees the generic text, the log the case and the
/// underlying error
nonisolated var errorDescription: String? {
Strings.unexpectedError
}

nonisolated var debugDescription: String {
"[\(errorCode)] \(detail)"
}

private nonisolated var detail: String {
switch self {
case let .secCallFailed(error):
"KeychainStoreError::SecCallFailed[Error: \(error.localizedDescription)]"
Expand Down
25 changes: 19 additions & 6 deletions NativeAppTemplate/Common/Errors/NativeAppTemplateAPIError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,28 @@ enum NativeAppTemplateAPIError: CodedError {
}
}

/// What the user sees: the API's error_message (already localized) as is, else a
/// localized generic text. The status and other developer details are in
/// `debugDescription`, for the log.
nonisolated var errorDescription: String? {
switch self {
case let .requestFailed(_, _, message):
message ?? Strings.serverCommunicationError
case .processingError, .responseMissingRequiredMeta, .responseHasIncorrectNumberOfElements, .noData:
Strings.unexpectedError
}
}

nonisolated var debugDescription: String {
"[\(errorCode)] \(detail)"
}

private nonisolated var detail: String {
switch self {
case let .requestFailed(error, statusCode, message):
if let message {
"\(message) [Status: \(statusCode)]"
} else {
"NativeAppTemplateAPIError::RequestFailed" +
"[Status: \(statusCode) | Error: \(error?.localizedDescription ?? "UNKNOWN")]"
}
"NativeAppTemplateAPIError::RequestFailed[Status: \(statusCode)"
+ " | Message: \(message ?? "NONE")"
+ " | Error: \(error?.localizedDescription ?? "UNKNOWN")]"
case let .processingError(error):
"NativeAppTemplateAPIError::ProcessingError[Error: \(error?.localizedDescription ?? "UNKNOWN")]"
case let .responseMissingRequiredMeta(field: field):
Expand Down
Loading
Loading