diff --git a/.gitignore b/.gitignore index 0acb818..fd9bdc7 100644 --- a/.gitignore +++ b/.gitignore @@ -89,3 +89,7 @@ secrets.*.xcconfig .claude/logs/ .claude/tmp/ .claude/*.log +.claude/scheduled_tasks.lock + +# Private docs +docs-private/ diff --git a/CHANGELOG.md b/CHANGELOG.md index e5911f3..282c580 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [3.2.0] - 2026-05-02 + +### Removed +- NFC, QR code, and Scan functionality (Phase 2A-2) +- `tagsScanned` count and `howToUse` fields from ItemTag +- `maximum_name_length` from server permissions; moved to client `Constants` +- Onboarding reload ceremony +- Dead code: `ImageSaver`, `composited()`, unused colors and fonts, dead `Date` helper + +### Changed +- Renamed "Number Tag" to "Item Tag" across labels and identifiers +- Ported ItemTag schema and generic CRUD UI from paid iOS (Phase 2A-1, 2A-3) +- Display `completedAt` as `yyyy/MM/dd HH:mm` regardless of locale +- Added client-side length caps and truncation for Shop name/description +- Wrapped String constants in `enum Strings` namespace +- Slimmed onboarding to 4 slides; introduced `ImageOrientation` enum +- Tightened ItemTag labels and silenced success toasts on complete/idle +- Read API endpoint from env vars in Debug builds +- Renamed error code prefix `NATI-` to `NATIVEAPPTEMPLATE-` and env var prefix `NATEMPLATE_API_` to `NATIVEAPPTEMPLATE_API_` +- Swiftier String predicates; replaced custom helpers with stdlib equivalents + +### Added +- Unit tests for previously untested Models and `Date` extension +- `CONTRIBUTING.md` and `CODE_OF_CONDUCT.md` + +### Fixed +- Onboarding flow and asset cleanup +- README: dropped stale NFC/QR feature lines and updated env var references + ## [3.1.1] - 2026-04-06 ### Changed @@ -17,7 +46,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 diff --git a/CLAUDE.md b/CLAUDE.md index 6c45041..58353cf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/NativeAppTemplate.xcodeproj/project.pbxproj b/NativeAppTemplate.xcodeproj/project.pbxproj index f968835..4c3ce40 100644 --- a/NativeAppTemplate.xcodeproj/project.pbxproj +++ b/NativeAppTemplate.xcodeproj/project.pbxproj @@ -31,9 +31,6 @@ 012009FC299F1E190078A1F9 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = 012009FB299F1E190078A1F9 /* OrderedCollections */; }; 012643372B3554AD00D4E9BD /* AcceptTermsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012643362B3554AD00D4E9BD /* AcceptTermsView.swift */; }; 013292BE262C3EA400690B75 /* LoggedInShopkeeper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 013292BD262C3EA400690B75 /* LoggedInShopkeeper.swift */; }; - 0135E7192D7E33F9004AD8FA /* CompleteScanResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0135E7152D7E33F9004AD8FA /* CompleteScanResultView.swift */; }; - 0135E71A2D7E33F9004AD8FA /* ShowTagInfoScanResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0135E7172D7E33F9004AD8FA /* ShowTagInfoScanResultView.swift */; }; - 0135E71B2D7E33F9004AD8FA /* ScanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0135E7162D7E33F9004AD8FA /* ScanView.swift */; }; 013DE735284E99DF00528CC5 /* ShopCreateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 013DE734284E99DF00528CC5 /* ShopCreateView.swift */; }; 01467357299902230005423D /* ShopSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01467356299902230005423D /* ShopSettingsView.swift */; }; 01482FA42B351E4100A56D43 /* AcceptPrivacyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01482FA32B351E4100A56D43 /* AcceptPrivacyView.swift */; }; @@ -78,32 +75,19 @@ 0172052F25AC41A7008FD63B /* SessionRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172052E25AC41A7008FD63B /* SessionRequest.swift */; }; 017278072D7D4F5800CE424F /* OnboardingRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017278062D7D4F5800CE424F /* OnboardingRepository.swift */; }; 017278092D7D4F7400CE424F /* Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017278082D7D4F7400CE424F /* Onboarding.swift */; }; - 0172785A2D7D83B600CE424F /* NFCManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017278592D7D83B600CE424F /* NFCManager.swift */; }; - 0172785B2D7D83B600CE424F /* AppSingletons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017278582D7D83B600CE424F /* AppSingletons.swift */; }; - 017278612D7D83E700CE424F /* ItemTagData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172785D2D7D83E700CE424F /* ItemTagData.swift */; }; 017278622D7D83E700CE424F /* ItemTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172785C2D7D83E700CE424F /* ItemTag.swift */; }; 017278632D7D83E700CE424F /* ItemTagState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172785F2D7D83E700CE424F /* ItemTagState.swift */; }; - 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 */; }; - 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 */; }; 017278712D7D87D000CE424F /* DateFormatter+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172786D2D7D87D000CE424F /* DateFormatter+Extensions.swift */; }; - 017278732D7D87EB00CE424F /* UIImage+Extentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017278722D7D87EB00CE424F /* UIImage+Extentions.swift */; }; 017278752D7D8FAC00CE424F /* ItemTagRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017278742D7D8FAC00CE424F /* ItemTagRepository.swift */; }; 017278772D7D8FF100CE424F /* ItemTagsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017278762D7D8FF100CE424F /* ItemTagsService.swift */; }; 017278792D7D900100CE424F /* ItemTagsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017278782D7D900100CE424F /* ItemTagsRequest.swift */; }; 0172787B2D7D903500CE424F /* ItemTagAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172787A2D7D903500CE424F /* ItemTagAdapter.swift */; }; - 0172787D2D7D92DF00CE424F /* CompleteScanResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172787C2D7D92DF00CE424F /* CompleteScanResult.swift */; }; 0172787F2D7D933000CE424F /* ShopDetailCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172787E2D7D933000CE424F /* ShopDetailCardView.swift */; }; - 017278822D7D935700CE424F /* QRCodeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017278812D7D935700CE424F /* QRCodeGenerator.swift */; }; - 017278832D7D935700CE424F /* ImageSaver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017278802D7D935700CE424F /* ImageSaver.swift */; }; 0172788B2D7D936E00CE424F /* CompletedTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017278842D7D936E00CE424F /* CompletedTag.swift */; }; 0172788C2D7D936E00CE424F /* IdlingTagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017278872D7D936E00CE424F /* IdlingTagView.swift */; }; - 0172788D2D7D936E00CE424F /* CustomerScannedTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017278862D7D936E00CE424F /* CustomerScannedTag.swift */; }; 017278902D7D936E00CE424F /* TagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017278892D7D936E00CE424F /* TagView.swift */; }; - 017278922D7D99B900CE424F /* NumberTagsWebpageListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017278912D7D99B900CE424F /* NumberTagsWebpageListView.swift */; }; 0172789A2D7D99D100CE424F /* ItemTagListCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017278972D7D99D100CE424F /* ItemTagListCardView.swift */; }; 0172789B2D7D99D100CE424F /* ItemTagListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017278982D7D99D100CE424F /* ItemTagListView.swift */; }; 0172789C2D7D99D100CE424F /* ItemTagDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017278932D7D99D100CE424F /* ItemTagDetailView.swift */; }; @@ -137,7 +121,6 @@ 01B6F5AB2601F84700397E66 /* PermissionsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B6F5AA2601F84700397E66 /* PermissionsRequest.swift */; }; 01B9E45228A5070D00CAC681 /* ShopkeeperSignInAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B9E45128A5070D00CAC681 /* ShopkeeperSignInAdapter.swift */; }; 01BE4F1D29CA6F8C002008BE /* TimeZoneData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01BE4F1C29CA6F8C002008BE /* TimeZoneData.swift */; }; - 01D85A962E07C78400A95798 /* NumberTagsWebpageListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D85A952E07C78400A95798 /* NumberTagsWebpageListViewModel.swift */; }; 01D85A9A2E07C85900A95798 /* ShopBasicSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D85A992E07C85900A95798 /* ShopBasicSettingsViewModel.swift */; }; 01D85A9E2E07C9BD00A95798 /* ShopSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D85A9D2E07C9BD00A95798 /* ShopSettingsViewModel.swift */; }; 01D85AE72E07CD4400A95798 /* ItemTagDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D85AE62E07CD4400A95798 /* ItemTagDetailViewModel.swift */; }; @@ -148,7 +131,6 @@ 01D85B462E07F15400A95798 /* PasswordEditViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D85B452E07F15400A95798 /* PasswordEditViewModel.swift */; }; 01D85B482E07F16100A95798 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D85B472E07F16100A95798 /* SettingsViewModel.swift */; }; 01D85B4A2E07F16900A95798 /* ShopkeeperEditViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D85B492E07F16900A95798 /* ShopkeeperEditViewModel.swift */; }; - 01D85BA72E081C6D00A95798 /* ScanViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D85BA62E081C6D00A95798 /* ScanViewModel.swift */; }; 01D8AE8B2AB453C1009AFFBA /* ShopBasicSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D8AE8A2AB453C1009AFFBA /* ShopBasicSettingsView.swift */; }; 01DCE23F298FA3B300BA311D /* ShopListCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01DCE23E298FA3B300BA311D /* ShopListCardView.swift */; }; 01E0A59C25BD088600298D35 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E0A59125BD087E00298D35 /* SettingsView.swift */; }; @@ -170,7 +152,6 @@ A1B2C3D401000003 /* GlassCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D401000004 /* GlassCard.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 */ @@ -208,9 +189,6 @@ 011F6DF9259EF16600BED22E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 012643362B3554AD00D4E9BD /* AcceptTermsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcceptTermsView.swift; sourceTree = ""; }; 013292BD262C3EA400690B75 /* LoggedInShopkeeper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggedInShopkeeper.swift; sourceTree = ""; }; - 0135E7152D7E33F9004AD8FA /* CompleteScanResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompleteScanResultView.swift; sourceTree = ""; }; - 0135E7162D7E33F9004AD8FA /* ScanView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanView.swift; sourceTree = ""; }; - 0135E7172D7E33F9004AD8FA /* ShowTagInfoScanResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowTagInfoScanResultView.swift; sourceTree = ""; }; 0135E8E22D7E4478004AD8FA /* SampleCode.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = SampleCode.xcconfig; sourceTree = ""; }; 013DE734284E99DF00528CC5 /* ShopCreateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopCreateView.swift; sourceTree = ""; }; 01467356299902230005423D /* ShopSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopSettingsView.swift; sourceTree = ""; }; @@ -256,33 +234,19 @@ 0172052E25AC41A7008FD63B /* SessionRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionRequest.swift; sourceTree = ""; }; 017278062D7D4F5800CE424F /* OnboardingRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingRepository.swift; sourceTree = ""; }; 017278082D7D4F7400CE424F /* Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Onboarding.swift; sourceTree = ""; }; - 0172782B2D7D575900CE424F /* NativeAppTemplate.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NativeAppTemplate.entitlements; sourceTree = ""; }; - 017278582D7D83B600CE424F /* AppSingletons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSingletons.swift; sourceTree = ""; }; - 017278592D7D83B600CE424F /* NFCManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NFCManager.swift; sourceTree = ""; }; 0172785C2D7D83E700CE424F /* ItemTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTag.swift; sourceTree = ""; }; - 0172785D2D7D83E700CE424F /* ItemTagData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTagData.swift; sourceTree = ""; }; - 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 = ""; }; - 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 = ""; }; 0172786D2D7D87D000CE424F /* DateFormatter+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateFormatter+Extensions.swift"; sourceTree = ""; }; 0172786E2D7D87D000CE424F /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; - 017278722D7D87EB00CE424F /* UIImage+Extentions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Extentions.swift"; sourceTree = ""; }; 017278742D7D8FAC00CE424F /* ItemTagRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTagRepository.swift; sourceTree = ""; }; 017278762D7D8FF100CE424F /* ItemTagsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTagsService.swift; sourceTree = ""; }; 017278782D7D900100CE424F /* ItemTagsRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTagsRequest.swift; sourceTree = ""; }; 0172787A2D7D903500CE424F /* ItemTagAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTagAdapter.swift; sourceTree = ""; }; - 0172787C2D7D92DF00CE424F /* CompleteScanResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompleteScanResult.swift; sourceTree = ""; }; 0172787E2D7D933000CE424F /* ShopDetailCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopDetailCardView.swift; sourceTree = ""; }; - 017278802D7D935700CE424F /* ImageSaver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSaver.swift; sourceTree = ""; }; - 017278812D7D935700CE424F /* QRCodeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeGenerator.swift; sourceTree = ""; }; 017278842D7D936E00CE424F /* CompletedTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletedTag.swift; sourceTree = ""; }; - 017278862D7D936E00CE424F /* CustomerScannedTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerScannedTag.swift; sourceTree = ""; }; 017278872D7D936E00CE424F /* IdlingTagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdlingTagView.swift; sourceTree = ""; }; 017278892D7D936E00CE424F /* TagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagView.swift; sourceTree = ""; }; - 017278912D7D99B900CE424F /* NumberTagsWebpageListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberTagsWebpageListView.swift; sourceTree = ""; }; 017278932D7D99D100CE424F /* ItemTagDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTagDetailView.swift; sourceTree = ""; }; 017278942D7D99D100CE424F /* ItemTagEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTagEditView.swift; sourceTree = ""; }; 017278962D7D99D100CE424F /* ItemTagCreateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTagCreateView.swift; sourceTree = ""; }; @@ -316,7 +280,6 @@ 01B9E45128A5070D00CAC681 /* ShopkeeperSignInAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopkeeperSignInAdapter.swift; sourceTree = ""; }; 01BE4F1C29CA6F8C002008BE /* TimeZoneData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeZoneData.swift; sourceTree = ""; }; 01D19B432D4DE33500BDEAB7 /* NativeAppTemplateTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NativeAppTemplateTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 01D85A952E07C78400A95798 /* NumberTagsWebpageListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberTagsWebpageListViewModel.swift; sourceTree = ""; }; 01D85A992E07C85900A95798 /* ShopBasicSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopBasicSettingsViewModel.swift; sourceTree = ""; }; 01D85A9D2E07C9BD00A95798 /* ShopSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopSettingsViewModel.swift; sourceTree = ""; }; 01D85AE62E07CD4400A95798 /* ItemTagDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTagDetailViewModel.swift; sourceTree = ""; }; @@ -327,7 +290,6 @@ 01D85B452E07F15400A95798 /* PasswordEditViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordEditViewModel.swift; sourceTree = ""; }; 01D85B472E07F16100A95798 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; 01D85B492E07F16900A95798 /* ShopkeeperEditViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopkeeperEditViewModel.swift; sourceTree = ""; }; - 01D85BA62E081C6D00A95798 /* ScanViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanViewModel.swift; sourceTree = ""; }; 01D8AE8A2AB453C1009AFFBA /* ShopBasicSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopBasicSettingsView.swift; sourceTree = ""; }; 01DCE23E298FA3B300BA311D /* ShopListCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopListCardView.swift; sourceTree = ""; }; 01E0A59125BD087E00298D35 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; @@ -348,7 +310,6 @@ A1B2C3D401000004 /* GlassCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlassCard.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 = ""; }; C597C0551370446BB931F19B /* CertificatePinningDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificatePinningDelegate.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -414,11 +375,8 @@ children = ( 011F6DF0259EF16400BED22E /* App.swift */, 017203A225A96F7A008FD63B /* Constants.swift */, - 017278582D7D83B600CE424F /* AppSingletons.swift */, - 017278592D7D83B600CE424F /* NFCManager.swift */, 01BE4F1C29CA6F8C002008BE /* TimeZoneData.swift */, 011F6DF9259EF16600BED22E /* Info.plist */, - 0172782B2D7D575900CE424F /* NativeAppTemplate.entitlements */, 015C78042B72DA2C00B6523C /* PrivacyInfo.xcprivacy */, A2B3C4D50000000A /* Common */, 0172049125AA8449008FD63B /* Data */, @@ -453,17 +411,6 @@ name = Frameworks; sourceTree = ""; }; - 0135E7182D7E33F9004AD8FA /* Scan */ = { - isa = PBXGroup; - children = ( - 0135E7152D7E33F9004AD8FA /* CompleteScanResultView.swift */, - 0135E7162D7E33F9004AD8FA /* ScanView.swift */, - 01D85BA62E081C6D00A95798 /* ScanViewModel.swift */, - 0135E7172D7E33F9004AD8FA /* ShowTagInfoScanResultView.swift */, - ); - path = Scan; - sourceTree = ""; - }; 0135E8E32D7E4478004AD8FA /* Configuration */ = { isa = PBXGroup; children = ( @@ -477,8 +424,6 @@ children = ( 017278952D7D99D100CE424F /* ItemTag Detail */, 017278992D7D99D100CE424F /* ItemTag List */, - 017278912D7D99B900CE424F /* NumberTagsWebpageListView.swift */, - 01D85A952E07C78400A95798 /* NumberTagsWebpageListViewModel.swift */, 01D8AE8A2AB453C1009AFFBA /* ShopBasicSettingsView.swift */, 01D85A992E07C85900A95798 /* ShopBasicSettingsViewModel.swift */, 01467356299902230005423D /* ShopSettingsView.swift */, @@ -593,22 +538,16 @@ 0172036325A96E04008FD63B /* Models */ = { isa = PBXGroup; children = ( - 0172787C2D7D92DF00CE424F /* CompleteScanResult.swift */, 0172785C2D7D83E700CE424F /* ItemTag.swift */, - 0172785D2D7D83E700CE424F /* ItemTagData.swift */, - 0172785E2D7D83E700CE424F /* ItemTagInfoFromNdefMessage.swift */, 0172785F2D7D83E700CE424F /* ItemTagState.swift */, - 017278602D7D83E700CE424F /* ItemTagType.swift */, 01B526532AF4E36400655131 /* MainTab.swift */, 2FE8A6D1D27C458389C4F61A /* PaginationMeta.swift */, 017278082D7D4F7400CE424F /* Onboarding.swift */, - 017278672D7D83F600CE424F /* ScanState.swift */, 01B526552AF4E82A00655131 /* ScrollToTopID.swift */, 0110A15E2AC816F5003EDCBA /* SendConfirmation.swift */, 0150A36529B14BB300907F96 /* SendResetPassword.swift */, 01E0A62F25BD53FD00298D35 /* Shop.swift */, 0172052425AAFA43008FD63B /* Shopkeeper.swift */, - 0172786A2D7D840A00CE424F /* ShowTagInfoScanResult.swift */, 01E2476F29A570D300D4B00D /* SignUp.swift */, 0106413D29A9F1C300B46FED /* UpdatePassword.swift */, ); @@ -623,7 +562,6 @@ 0172786D2D7D87D000CE424F /* DateFormatter+Extensions.swift */, 0172786E2D7D87D000CE424F /* String+Extensions.swift */, 017203A825A96FBF008FD63B /* UIApplication+DismissKeyboard.swift */, - 017278722D7D87EB00CE424F /* UIImage+Extentions.swift */, 017203AB25A96FBF008FD63B /* View+Extensions.swift */, ); path = Extensions; @@ -676,7 +614,6 @@ children = ( 0172045625AA8275008FD63B /* App Root */, 01E0A5B125BD0FC600298D35 /* Empty States */, - 0135E7182D7E33F9004AD8FA /* Scan */, 01E0A59025BD087E00298D35 /* Settings */, 01E0A5DF25BD148800298D35 /* Shared */, 01011B542864434900B70D04 /* Shop Detail */, @@ -746,9 +683,7 @@ 017204F825AA85F3008FD63B /* Utilities */ = { isa = PBXGroup; children = ( - 017278802D7D935700CE424F /* ImageSaver.swift */, 017204F925AA85F3008FD63B /* MessageBus.swift */, - 017278812D7D935700CE424F /* QRCodeGenerator.swift */, 01E2477129A5E30400D4B00D /* Utility.swift */, ); path = Utilities; @@ -758,7 +693,6 @@ isa = PBXGroup; children = ( 017278842D7D936E00CE424F /* CompletedTag.swift */, - 017278862D7D936E00CE424F /* CustomerScannedTag.swift */, 017278872D7D936E00CE424F /* IdlingTagView.swift */, 017278892D7D936E00CE424F /* TagView.swift */, ); @@ -851,7 +785,6 @@ A2B3C4D500000003 /* AppError.swift */, A2B3C4D500000001 /* CodedError.swift */, A2B3C4D500000007 /* NativeAppTemplateAPIError.swift */, - A2B3C4D500000005 /* NFCError.swift */, ); path = Errors; sourceTree = ""; @@ -976,9 +909,6 @@ 0172047925AA8335008FD63B /* UIFont+Extensions.swift in Sources */, 01E2477029A570D300D4B00D /* SignUp.swift in Sources */, 01D85B4A2E07F16900A95798 /* ShopkeeperEditViewModel.swift in Sources */, - 0172785A2D7D83B600CE424F /* NFCManager.swift in Sources */, - 0172785B2D7D83B600CE424F /* AppSingletons.swift in Sources */, - 017278732D7D87EB00CE424F /* UIImage+Extentions.swift in Sources */, 0172052F25AC41A7008FD63B /* SessionRequest.swift in Sources */, 017278772D7D8FF100CE424F /* ItemTagsService.swift in Sources */, 017204C025AA846D008FD63B /* TabViewModel.swift in Sources */, @@ -987,7 +917,6 @@ 0172040025AA6775008FD63B /* LoginRepository.swift in Sources */, 0172034B25A9642E008FD63B /* EntityAdapter.swift in Sources */, 010A30A528A4A285001D6BD1 /* DataCacheUpdate.swift in Sources */, - 017278922D7D99B900CE424F /* NumberTagsWebpageListView.swift in Sources */, 01E0A60C25BD440300298D35 /* SignInEmailAndPasswordView.swift in Sources */, 0172033925A9642E008FD63B /* JSONAPIRelationship.swift in Sources */, 01B526562AF4E82A00655131 /* ScrollToTopID.swift in Sources */, @@ -1030,54 +959,39 @@ 01E0A5B625BD0FCD00298D35 /* LoadingView.swift in Sources */, 0114F3AC2E079BD100F4A1DD /* ShopListViewModel.swift in Sources */, 0172051A25AAF6C0008FD63B /* SessionsService.swift in Sources */, - 01D85A962E07C78400A95798 /* NumberTagsWebpageListViewModel.swift in Sources */, 017204D125AA8479008FD63B /* DataState.swift in Sources */, 012643372B3554AD00D4E9BD /* AcceptTermsView.swift in Sources */, 0172033E25A9642E008FD63B /* Parameters.swift in Sources */, 0150A36629B14BB300907F96 /* SendResetPassword.swift in Sources */, 017204B625AA8467008FD63B /* DataManager.swift in Sources */, - 017278682D7D83F600CE424F /* ScanState.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 */, 01D8AE8B2AB453C1009AFFBA /* ShopBasicSettingsView.swift in Sources */, 01E0A60125BD149200298D35 /* MainButtonView.swift in Sources */, A1B2C3D401000003 /* GlassCard.swift in Sources */, 0182D39A25B4424B001E881D /* LoggedInShopkeeperKeychainStore.swift in Sources */, 01ED197B2A037B9E00CD4735 /* AppTabView.swift in Sources */, 0110A1612AC81978003EDCBA /* ResendConfirmationInstructionsView.swift in Sources */, - 01D85BA72E081C6D00A95798 /* ScanViewModel.swift in Sources */, 0106413C29A9EDFF00B46FED /* AccountPasswordRequest.swift in Sources */, 0172035625A9642E008FD63B /* ShopAdapter.swift in Sources */, 0172788B2D7D936E00CE424F /* CompletedTag.swift in Sources */, 0172788C2D7D936E00CE424F /* IdlingTagView.swift in Sources */, 01A1339F2E08B2FD000AD24A /* AcceptTermsViewModel.swift in Sources */, - 0172788D2D7D936E00CE424F /* CustomerScannedTag.swift in Sources */, 017278902D7D936E00CE424F /* TagView.swift in Sources */, 0106414429AA061100B46FED /* PasswordEditView.swift in Sources */, 0114F4032E07A88000F4A1DD /* ShopCreateViewModel.swift in Sources */, - 0172786B2D7D840A00CE424F /* ShowTagInfoScanResult.swift in Sources */, 017204D925AA847E008FD63B /* ShopRepository.swift in Sources */, - 017278612D7D83E700CE424F /* ItemTagData.swift in Sources */, 017278622D7D83E700CE424F /* ItemTag.swift in Sources */, 0199CD242E07510200109DC6 /* AccountPasswordRepositoryProtocol.swift in Sources */, 0199CD252E07510200109DC6 /* ItemTagRepositoryProtocol.swift in Sources */, 0199CD262E07510200109DC6 /* ShopRepositoryProtocol.swift in Sources */, 017278632D7D83E700CE424F /* ItemTagState.swift in Sources */, 4A8DA0DEF6F142C3A127058A /* PaginationMeta.swift in Sources */, - 017278642D7D83E700CE424F /* ItemTagInfoFromNdefMessage.swift in Sources */, - 017278652D7D83E700CE424F /* ItemTagType.swift in Sources */, 0172046625AA82BF008FD63B /* MessageBarView.swift in Sources */, 01E0A63025BD53FD00298D35 /* Shop.swift in Sources */, 017278072D7D4F5800CE424F /* OnboardingRepository.swift in Sources */, - 0135E7192D7E33F9004AD8FA /* CompleteScanResultView.swift in Sources */, 01D85A9A2E07C85900A95798 /* ShopBasicSettingsViewModel.swift in Sources */, - 0135E71A2D7E33F9004AD8FA /* ShowTagInfoScanResultView.swift in Sources */, - 0135E71B2D7E33F9004AD8FA /* ScanView.swift in Sources */, 013292BE262C3EA400690B75 /* LoggedInShopkeeper.swift in Sources */, 01D85A9E2E07C9BD00A95798 /* ShopSettingsViewModel.swift in Sources */, 0172035825A9642E008FD63B /* ShopsService.swift in Sources */, @@ -1288,10 +1202,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = ""; BUNDLE_ID_SUFFIX = .dev; - CODE_SIGN_ENTITLEMENTS = NativeAppTemplate/NativeAppTemplate.entitlements; + CODE_SIGN_ENTITLEMENTS = ""; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_ASSET_PATHS = "\"NativeAppTemplate/Preview Content\""; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -1303,7 +1217,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.1.1; + MARKETING_VERSION = 3.2.0; PRODUCT_BUNDLE_IDENTIFIER = "com.nativeapptemplate.NativeAppTemplateFree.ios${SAMPLE_CODE_DISAMBIGUATOR}"; "PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = "com.nativeapptemplate.NativeAppTemplateFree.ios${SAMPLE_CODE_DISAMBIGUATOR}"; PRODUCT_NAME = NativeAppTemplate; @@ -1324,10 +1238,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = ""; BUNDLE_ID_SUFFIX = ""; - CODE_SIGN_ENTITLEMENTS = NativeAppTemplate/NativeAppTemplate.entitlements; + CODE_SIGN_ENTITLEMENTS = ""; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_ASSET_PATHS = "\"NativeAppTemplate/Preview Content\""; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -1339,7 +1253,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.1.1; + MARKETING_VERSION = 3.2.0; PRODUCT_BUNDLE_IDENTIFIER = "com.nativeapptemplate.NativeAppTemplateFree.ios${SAMPLE_CODE_DISAMBIGUATOR}"; "PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = "com.nativeapptemplate.NativeAppTemplateFree.ios${SAMPLE_CODE_DISAMBIGUATOR}"; PRODUCT_NAME = NativeAppTemplate; @@ -1354,103 +1268,6 @@ }; name = Release; }; - 016595AF2824E3D800203F7F /* Beta configuration for PBXProject "NativeAppTemplate" */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = NNYDL5U3V3; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.2; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - STRING_CATALOG_GENERATE_SYMBOLS = YES; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 6.0; - VALIDATE_PRODUCT = YES; - }; - name = Beta; - }; - 016595B02824E3D800203F7F /* Beta configuration for PBXNativeTarget "NativeAppTemplate" */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = ""; - BUNDLE_ID_SUFFIX = .beta; - CODE_SIGN_ENTITLEMENTS = NativeAppTemplate/NativeAppTemplate.entitlements; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; - DEVELOPMENT_ASSET_PATHS = "\"NativeAppTemplate/Preview Content\""; - ENABLE_PREVIEWS = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - INFOPLIST_FILE = NativeAppTemplate/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "NativeAppTemplate Free"; - INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.business"; - IPHONEOS_DEPLOYMENT_TARGET = 26.2; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 3.1.1; - PRODUCT_BUNDLE_IDENTIFIER = "com.nativeapptemplate.NativeAppTemplateFree.ios${SAMPLE_CODE_DISAMBIGUATOR}$(BUNDLE_ID_SUFFIX)"; - PRODUCT_NAME = NativeAppTemplate; - SAMPLE_CODE_DISAMBIGUATOR = "${DEVELOPMENT_TEAM}"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; - SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_VERSION = 6.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Beta; - }; 01D19B4A2D4DE33500BDEAB7 /* Debug configuration for PBXNativeTarget "NativeAppTemplateTests" */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1492,26 +1309,6 @@ }; name = Release; }; - 01D19B4C2D4DE33500BDEAB7 /* Beta configuration for PBXNativeTarget "NativeAppTemplateTests" */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GCC_C_LANGUAGE_STANDARD = gnu17; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 26.2; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.nativeapptemplate.NativeAppTemplateTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/NativeAppTemplate.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/NativeAppTemplate"; - }; - name = Beta; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -1520,7 +1317,6 @@ buildConfigurations = ( 011F6E10259EF16600BED22E /* Debug configuration for PBXProject "NativeAppTemplate" */, 011F6E11259EF16600BED22E /* Release configuration for PBXProject "NativeAppTemplate" */, - 016595AF2824E3D800203F7F /* Beta configuration for PBXProject "NativeAppTemplate" */, ); defaultConfigurationName = Release; }; @@ -1529,7 +1325,6 @@ buildConfigurations = ( 011F6E13259EF16600BED22E /* Debug configuration for PBXNativeTarget "NativeAppTemplate" */, 011F6E14259EF16600BED22E /* Release configuration for PBXNativeTarget "NativeAppTemplate" */, - 016595B02824E3D800203F7F /* Beta configuration for PBXNativeTarget "NativeAppTemplate" */, ); defaultConfigurationName = Release; }; @@ -1538,7 +1333,6 @@ buildConfigurations = ( 01D19B4A2D4DE33500BDEAB7 /* Debug configuration for PBXNativeTarget "NativeAppTemplateTests" */, 01D19B4B2D4DE33500BDEAB7 /* Release configuration for PBXNativeTarget "NativeAppTemplateTests" */, - 01D19B4C2D4DE33500BDEAB7 /* Beta configuration for PBXNativeTarget "NativeAppTemplateTests" */, ); defaultConfigurationName = Release; }; diff --git a/NativeAppTemplate.xcodeproj/xcshareddata/xcschemes/NativeAppTemplate.xcscheme b/NativeAppTemplate.xcodeproj/xcshareddata/xcschemes/NativeAppTemplate.xcscheme deleted file mode 100644 index 4bd14a0..0000000 --- a/NativeAppTemplate.xcodeproj/xcshareddata/xcschemes/NativeAppTemplate.xcscheme +++ /dev/null @@ -1,95 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/NativeAppTemplate/App.swift b/NativeAppTemplate/App.swift index e24f728..5728066 100644 --- a/NativeAppTemplate/App.swift +++ b/NativeAppTemplate/App.swift @@ -33,13 +33,9 @@ private final class NullSessionController: SessionControllerProtocol { } var shouldPopToRootView: Bool = false - var didBackgroundTagReading: Bool = false - var completeScanResult = CompleteScanResult() - var showTagInfoScanResult = ShowTagInfoScanResult() var shouldUpdateApp: Bool = false var shouldUpdatePrivacy: Bool = false var shouldUpdateTerms: Bool = false - var maximumQueueNumberLength: Int = 0 var shopLimitCount: Int = 0 var shopkeeper: Shopkeeper? var hasPermissions: Bool { diff --git a/NativeAppTemplate/AppSingletons.swift b/NativeAppTemplate/AppSingletons.swift deleted file mode 100644 index 5852516..0000000 --- a/NativeAppTemplate/AppSingletons.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// AppSingletons.swift -// NativeAppTemplate -// - -import Foundation - -@MainActor -struct AppSingletons { - var nfcManager: NFCManager - - init(nfcManager: NFCManager? = nil) { - self.nfcManager = nfcManager ?? NFCManager.shared - } -} - -@MainActor var appSingletons = AppSingletons() diff --git a/NativeAppTemplate/Common/Errors/AppError.swift b/NativeAppTemplate/Common/Errors/AppError.swift index 7a1de92..b8b7c7c 100644 --- a/NativeAppTemplate/Common/Errors/AppError.swift +++ b/NativeAppTemplate/Common/Errors/AppError.swift @@ -16,7 +16,7 @@ enum AppError: CodedError { var errorCode: String { switch self { case .unexpected: - "NATI-1001" + "NATIVEAPPTEMPLATE-1001" } } diff --git a/NativeAppTemplate/Common/Errors/CodedError.swift b/NativeAppTemplate/Common/Errors/CodedError.swift index 1cd9c64..5fa12d9 100644 --- a/NativeAppTemplate/Common/Errors/CodedError.swift +++ b/NativeAppTemplate/Common/Errors/CodedError.swift @@ -3,9 +3,8 @@ // NativeAppTemplate // -// Error codes use the `NATI-XXXX` prefix (NativeAppTemplate iOS). -// Android uses `NATA-XXXX`. -// Ranges: 1xxx App errors, 2xxx API errors, 3xxx NFC errors. +// Error codes share the `NATIVEAPPTEMPLATE-XXXX` prefix across iOS and Android. +// Ranges: 1xxx App errors, 2xxx API errors. import Foundation diff --git a/NativeAppTemplate/Common/Errors/NFCError.swift b/NativeAppTemplate/Common/Errors/NFCError.swift deleted file mode 100644 index 024b15e..0000000 --- a/NativeAppTemplate/Common/Errors/NFCError.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// 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 index 7c90fd8..9f64207 100644 --- a/NativeAppTemplate/Common/Errors/NativeAppTemplateAPIError.swift +++ b/NativeAppTemplate/Common/Errors/NativeAppTemplateAPIError.swift @@ -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" } } diff --git a/NativeAppTemplate/Constants.swift b/NativeAppTemplate/Constants.swift index 523d2ee..3de608c 100644 --- a/NativeAppTemplate/Constants.swift +++ b/NativeAppTemplate/Constants.swift @@ -72,6 +72,16 @@ enum NativeAppTemplateConstants { static let shadowRadius: CGFloat = 8 } + // MARK: - Shop + + static let maximumShopNameLength = 100 + static let maximumShopDescriptionLength = 1_000 + + // MARK: - ItemTag + + static let maximumItemTagNameLength = 100 + static let maximumItemTagDescriptionLength = 1_000 + // MARK: - Corner Radius enum CornerRadius { @@ -92,27 +102,21 @@ enum NativeAppTemplateConstants { } } -extension String { +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" static let port: String = "" #endif - static let androidAar: String = "com.nativeapptemplate.nativeapptemplatefree" - static let androidAarNfcndefPayloadType: String = "android.com:pkg" - /// This is for MyTurnTag Creator. Replace this. static let appStoreUrl: String = "https://apps.apple.com/app/myturntag-creator/id1516198303" - static let scanPath: String = "scan" - static let scanPathCustomer: String = "scan_customer" - static let placeholderFullName: String = "John Smith" static let placeholderEmail: String = "you@example.com" static let placeholderPassword: String = "password" @@ -125,7 +129,6 @@ extension String { "com.nativeapptemplate.NativeAppTemplateFree.LoggedInShopkeeperService" static let shops = "Shops" - static let scan = "Scan" static let settings = "Settings" static let loading = "Loading..." @@ -148,70 +151,57 @@ extension String { static let addShopDescription = "Add a new shop." static let deleteShop = "Delete Shop" static let shopNameIsRequired = "Shop name is required." + static let shopNameIsInvalid = "Shop name is invalid." + static let shopDescriptionIsInvalid = "Shop description is too long." + + static func shopNameHelp(maximumLength: Int) -> String { + "Name must be 1–\(maximumLength) characters." + } + + static func shopDescriptionHelp(maximumLength: Int) -> String { + "Description can be up to \(maximumLength) characters." + } + static let timeZone = "Time Zone" static let createShopsLabel = "Create shops" static let tapShopBelow = "Tap a shop below." static let haveFun = "Have fun!" - - // MARK: Shop Detail View - - static let swipeNumberTagBelow = "Swipe a number tag below." - static let tapDisplayedButton = "Tap the displayed button." - static let serverNumberTagsWebpageWillBeUpdated = "The Server Number Tags Webpage will be updated." - static let readInstructions = "Read Instructions" - static let serverNumberTagsWebpage = "Server Number Tags Webpage" + static let shopDetailInstruction = "Swipe an item tag to change its status." // MARK: Shop Settings View static let shopSettingsLabel = "Shop Settings" static let shopSettingsBasicSettingsLabel = "Basic Settings" static let shopSettingsManageItemTagsLabel = "Manage Item Tags" - static let shopSettingsNumberTagsWebpageLabel = "Number Tags Webpage" - static let resetNumberTagsDescription = "Reset all number tag statuses." - static let resetNumberTags = "Reset Number Tags" - - // MARK: Number Tags Web Pages - - static let copyWebpageUrl = "Copy the above webpage URL" - static let webpageUrlCopied = "Webpage URL copied." // MARK: Item Tag View - static let tagNumber = "Name" - static let editTag = "Edit Tag" - static let addTag = "Add Tag" - static let addTagDescription = "Add a new item tag and start changing the tag status." - static let deleteTag = "Delete tag" - static let buttonDeleteTag = "Delete Tag" - static let tagNumberIsInvalid = "Item tag name is invalid." - static let writeServerTag = "Write Server Tag" - static let writeCustomerTag = "Write Customer Tag" - static let youCannotUndoAfterLockingTag = - "You cannot undo. After locking the tag, you can no longer write data to it." - static let zeroPadding = "Zero padding(e.g. 07)." - static let writingSucceeded = "Writing succeeded!" - - // MARK: Scan View - - static let completeScan = "Complete Scan" - static let showTagInfoScan = "Show Tag Info Scan" - static let tagInfo = "Tag info" - static let readOnly = "Read Only" - static let writable = "Writable" - static let completeScanHelp = "Read a NFC Number Tag for changing the Number Tag status." - static let showTagInfoScanHelp = "Read a NFC Number Tag for showing the Number Tag information." - static let deviceDoesNotSupportScan = "This device doesn't support tag scanning." - static let holdYourIPhoneNearTheItem = "Hold your iPhone near the item to learn more about it." - static let tagNotValid = "Tag not valid." - static let moreThan1TagsWasFound = "More than 1 tags was found. Please present only 1 tag." - static let tagIsNotWritable = "Tag is not writable." - static let tagIsNotNdefFormatted = "Tag is not NDEF formatted." + static let nameLabel = "Name" + static let descriptionLabel = "Description" + static let itemTagNamePlaceholder = "Name" + static let editItemTag = "Edit Item Tag" + static let addItemTag = "Add Item Tag" + static let addItemTagDescription = "Add a new item tag and start changing the item tag status." + static let deleteItemTag = "Delete item tag" + static let buttonDeleteItemTag = "Delete Item Tag" + static let itemTagNameIsInvalid = "Item tag name is invalid." + static let itemTagDescriptionIsInvalid = "Item tag description is too long." + static let completedAtLabel = "Completed at" + static let markAsCompleted = "Mark as completed" + static let markAsIdled = "Mark as idled" + + static func itemTagNameHelp(maximumLength: Int) -> String { + "Name must be 1–\(maximumLength) characters." + } + + static func itemTagDescriptionHelp(maximumLength: Int) -> String { + "Description can be up to \(maximumLength) characters." + } // MARK: Settings View static let supportMail: String = "support@nativeapptemplate.com" static let supportWebsiteUrl: String = "https://nativeapptemplate.com" - static let howToUseUrl: String = "https://myturntag.com/how" static let faqsUrl: String = "https://nativeapptemplate.com/faqs" static let privacyPolicyUrl: String = "https://nativeapptemplate.com/privacy" static let termsOfUseUrl: String = "https://nativeapptemplate.com/terms" @@ -244,26 +234,13 @@ extension String { static let basicSettingsUpdated = "Basic settings updated successfully." static let shopDeleted = "Shop deleted successfully." static let shopDeletedError = "There was a problem deleting the shop." - static let shopReset = "All number tags reset." - static let shopResetError = "There was a problem resetting number tags." - - static let itemTagCreated = "Tag created successfully." - static let itemTagUpdated = "Tag updated successfully." - static let itemTagDeleted = "Tag deleted successfully." - static let itemTagDeletedError = "There was a problem deleting the tag." - static let itemTagCompleted = "Tag completed successfully." - static let itemTagCompletedError = "There was a problem completing the tag." - static let itemTagReset = "Tag reset successfully." - static let itemTagResetError = "There was a problem resetting the tag." - static let itemTagAlreadyCompleted = "Tag already completed." - static let messageWrittenOnTagIsWrong = "The message written on the tag is wrong." - static let scanServerTag = "This tag is a \"CUSTOMER\" tag. Scan a \"SERVER\" tag!" - - static let customerQrCodeImageSavedToPhotoAlbum = "Customer QR code image saved to Photo Album successfully." - static let customerQrCodeImageSavedToPhotoAlbumError = - "There was a problem saving Customer QR code image to Photo Album." - static let saveToPhotoAlbum = "Save to Photo Album" - static let generateCustomerQrCode = "Generate Customer QR code" + + static let itemTagCreated = "Item tag created successfully." + static let itemTagUpdated = "Item tag updated successfully." + static let itemTagDeleted = "Item tag deleted successfully." + static let itemTagDeletedError = "There was a problem deleting the item tag." + static let itemTagCompletedError = "There was a problem completing the item tag." + static let itemTagIdledError = "There was a problem idling the item tag." static let shopkeeperCreated = "Account created successfully." static let shopkeeperCreatedError = "There was a problem creating the account." @@ -308,24 +285,10 @@ extension String { static let email = "Email" static let password = "Password" - static let onboardingDescription1 = String(localized: "A **Server Tag** and a **Customer Tag** are NFCs.") - static let onboardingDescription2 = String(localized: "The staff gives the **Customer Tag** to the customer.") - static let onboardingDescription3 = - String(localized: "The customer scans the **Customer Tag** or the **Customer QR code**.") - static let onboardingDescription4 = - String(localized: "The customer can view the **Number Tags Webpage** on his mobile browser.") - static let onboardingDescription5 = String(localized: "The staff is cooking KILITANPOs.") - static let onboardingDescription6 = - String(localized: "The staff completed cooking KILITANPOs. The staff scans the **Server Tag**.") - static let onboardingDescription7 = String(localized: "Tag completed with Background Tag Reading.") - static let onboardingDescription8 = - String(localized: "If you do not want to scan, you can complete the tag by swiping the tag(Shops > [Shop]).") - static let onboardingDescription9 = String(localized: "**Number Tags Webpage** displays the completed number tag.") - static let onboardingDescription10 = String(localized: "The customer's **Number Tags Webpage** updated.") - static let onboardingDescription11 = - String(localized: "The customer\'s **Number Tags Webpage** displays the completed **Customer Tag**(A07).") - static let onboardingDescription12 = String(localized: "The customer returns the **Customer Tag**.") - static let onboardingDescription13 = String(localized: "The customer finally got the delicious KILITANPO!") + static let onboardingDescription1 = "Onboarding description 1." + static let onboardingDescription2 = "Onboarding description 2." + static let onboardingDescription3 = "Onboarding description 3." + static let onboardingDescription4 = "Onboarding description 4." // MARK: Other @@ -348,11 +311,8 @@ extension String { static let passwordIsRequired = "Password is required." static let passwordIsInvalid = "Password is invalid." static let role = "Role" - static let createShops = "Create shops." - static let createTags = "Create tags." static let complete = "Complete" static let open = "Open" - static let learnMore = "Learn More" static let instructions = "Instructions" static let forceSignOut = "Force Sign Out?" static let signOut = "Sign Out" @@ -363,10 +323,7 @@ extension String { static let backToStartScreen = "Back to Start Screen" static let fullName = "Full Name" static let fullNameIsRequired = "Full name is required." - static let reset = "Reset" - static let unknownNdefStatus = "Unknown NDEF status" - static let noRecrodsFound = "No recrods found" - static let thisDeviceDoesNotSupportTagScanning = "This device doesn't support tag scanning." + static let idle = "Idle" } extension TimeInterval { diff --git a/NativeAppTemplate/Data/Repositories/ItemTagRepository.swift b/NativeAppTemplate/Data/Repositories/ItemTagRepository.swift index 9b5ac78..c6261d4 100644 --- a/NativeAppTemplate/Data/Repositories/ItemTagRepository.swift +++ b/NativeAppTemplate/Data/Repositories/ItemTagRepository.swift @@ -191,15 +191,15 @@ import SwiftUI } } - func reset(id: String) async throws -> ItemTag { + func idle(id: String) async throws -> ItemTag { do { - let resetItemTag = try await itemTagsService.resetItemTag(id: id) - let itemTagIndex = (itemTags.firstIndex { $0.id == resetItemTag.id }) + let idledItemTag = try await itemTagsService.idleItemTag(id: id) + let itemTagIndex = (itemTags.firstIndex { $0.id == idledItemTag.id }) if itemTagIndex != nil { - itemTags[itemTagIndex!] = resetItemTag + itemTags[itemTagIndex!] = idledItemTag } - return resetItemTag + return idledItemTag } catch { state = .failed Failure diff --git a/NativeAppTemplate/Data/Repositories/ItemTagRepositoryProtocol.swift b/NativeAppTemplate/Data/Repositories/ItemTagRepositoryProtocol.swift index c29e012..cb8bf29 100644 --- a/NativeAppTemplate/Data/Repositories/ItemTagRepositoryProtocol.swift +++ b/NativeAppTemplate/Data/Repositories/ItemTagRepositoryProtocol.swift @@ -24,5 +24,5 @@ import SwiftUI func update(id: String, itemTag: ItemTag) async throws -> ItemTag func destroy(id: String) async throws func complete(id: String) async throws -> ItemTag - func reset(id: String) async throws -> ItemTag + func idle(id: String) async throws -> ItemTag } diff --git a/NativeAppTemplate/Extensions/Date+Extensions.swift b/NativeAppTemplate/Extensions/Date+Extensions.swift index d72625a..a4f6bcc 100644 --- a/NativeAppTemplate/Extensions/Date+Extensions.swift +++ b/NativeAppTemplate/Extensions/Date+Extensions.swift @@ -21,8 +21,7 @@ extension Date { return formatter.string(from: self) } - var cardTimeAgoInWordsDateString: String { - let formatter = DateFormatter.timeAgoInWordsDateFormatter - return formatter.string(from: self) + var cardDateTimeString: String { + "\(cardDateString) \(cardTimeString)" } } diff --git a/NativeAppTemplate/Extensions/DateFormatter+Extensions.swift b/NativeAppTemplate/Extensions/DateFormatter+Extensions.swift index 4afd9e0..2913614 100644 --- a/NativeAppTemplate/Extensions/DateFormatter+Extensions.swift +++ b/NativeAppTemplate/Extensions/DateFormatter+Extensions.swift @@ -5,11 +5,6 @@ import Foundation -extension String { - static let cardDateString: String = "MMM dd yyyy" - static let cardTimeString: String = "HH:mm" -} - extension ISO8601DateFormatter { convenience init(_ formatOptions: Options, timeZone: TimeZone = TimeZone(secondsFromGMT: 0)!) { self.init() @@ -34,21 +29,14 @@ extension String { } extension DateFormatter { - static let cardDateFormatter: DateFormatter = .formatter(for: .cardDateString) - - static let cardTimeFormatter: DateFormatter = .formatter(for: .cardTimeString) + static let cardDateFormatter: DateFormatter = .formatter(for: "yyyy/MM/dd") - static let timeAgoInWordsDateFormatter: DateFormatter = { - let dateFormatter = DateFormatter() - dateFormatter.dateStyle = .short - dateFormatter.timeStyle = .medium - dateFormatter.doesRelativeDateFormatting = true - return dateFormatter - }() + static let cardTimeFormatter: DateFormatter = .formatter(for: "HH:mm") static func formatter(for dateString: String) -> DateFormatter { let dateFormatter = DateFormatter() dateFormatter.dateFormat = dateString + dateFormatter.locale = Locale(identifier: "en_US_POSIX") return dateFormatter } } diff --git a/NativeAppTemplate/Extensions/String+Extensions.swift b/NativeAppTemplate/Extensions/String+Extensions.swift index 1273346..644a1ef 100644 --- a/NativeAppTemplate/Extensions/String+Extensions.swift +++ b/NativeAppTemplate/Extensions/String+Extensions.swift @@ -3,36 +3,15 @@ // NativeAppTemplate // -import UIKit +import Foundation extension String { - /// Generates a `UIImage` instance from this string using a specified - /// attributes and size. - /// - /// - Parameters: - /// - attributes: to draw this string with. Default is `nil`. - /// - size: of the image to return. - /// - Returns: a `UIImage` instance from this string using a specified - /// attributes and size, or `nil` if the operation fails. - func image(withAttributes attributes: [NSAttributedString.Key: Any]? = nil, size: CGSize? = nil) -> UIImage? { - let size = size ?? (self as NSString).size(withAttributes: attributes) - return UIGraphicsImageRenderer(size: size).image { _ in - (self as NSString).draw( - in: CGRect(origin: .zero, size: size), - withAttributes: attributes - ) - } + var isBlank: Bool { + trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } - func isAlphanumeric() -> Bool { - rangeOfCharacter(from: CharacterSet.alphanumerics.inverted) == nil && !isEmpty - } - - func isAlphanumeric(ignoreDiacritics: Bool = false) -> Bool { - if ignoreDiacritics { - range(of: "[^a-zA-Z0-9]", options: .regularExpression) == nil && !isEmpty - } else { - isAlphanumeric() - } + var isValidEmail: Bool { + let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" + return NSPredicate(format: "SELF MATCHES %@", emailRegex).evaluate(with: self) } } diff --git a/NativeAppTemplate/Extensions/UIImage+Extentions.swift b/NativeAppTemplate/Extensions/UIImage+Extentions.swift deleted file mode 100644 index cee022d..0000000 --- a/NativeAppTemplate/Extensions/UIImage+Extentions.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// UIImage+Extentions.swift -// NativeAppTemplate -// - -import UIKit - -extension UIImage { - func composited(withSmallCenterImage centerImage: UIImage) -> UIImage { - UIGraphicsImageRenderer(size: size).image { context in - let imageWidth = context.format.bounds.width - let imageHeight = context.format.bounds.height - let centerImageLength = imageWidth < imageHeight ? imageWidth / 5 : imageHeight / 5 - let centerImageRadius = centerImageLength * 0.2 - - draw(in: CGRect( - origin: CGPoint(x: 0, y: 0), - size: context.format.bounds.size - )) - - let centerImageRect = CGRect( - x: (imageWidth - centerImageLength) / 2, - y: (imageHeight - centerImageLength) / 2, - width: centerImageLength, - height: centerImageLength - ) - - let roundedRectPath = UIBezierPath( - roundedRect: centerImageRect, - cornerRadius: centerImageRadius - ) - roundedRectPath.addClip() - - centerImage.draw(in: centerImageRect) - } - } -} diff --git a/NativeAppTemplate/Info.plist b/NativeAppTemplate/Info.plist index 8a3667c..d26d8e8 100644 --- a/NativeAppTemplate/Info.plist +++ b/NativeAppTemplate/Info.plist @@ -24,10 +24,6 @@ LSRequiresIPhoneOS - NFCReaderUsageDescription - This app uses a NFC to write the application info to the NFC number tag or to read the NFC number tag into the application. - NSPhotoLibraryAddUsageDescription - Save a QR code image including web page link used by this app. UIApplicationSceneManifest UIApplicationSupportsMultipleScenes @@ -55,13 +51,5 @@ UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown - com.apple.developer.nfc.readersession.felica.systemcodes - - 12FC - - com.apple.developer.nfc.readersession.iso7816.select-identifiers - - D2760000850101 - diff --git a/NativeAppTemplate/Login/OnboardingRepository.swift b/NativeAppTemplate/Login/OnboardingRepository.swift index 102ca59..7f9b25a 100644 --- a/NativeAppTemplate/Login/OnboardingRepository.swift +++ b/NativeAppTemplate/Login/OnboardingRepository.swift @@ -4,29 +4,12 @@ // import Foundation -import OrderedCollections @MainActor @Observable class OnboardingRepository: OnboardingRepositoryProtocol { - var onboardings: [Onboarding] = [] - let onboardingsDictionary: OrderedDictionary = [ - 1: false, - 2: false, - 3: false, - 4: true, - 5: false, - 6: false, - 7: true, - 8: true, - 9: false, - 10: false, - 11: true, - 12: false, - 13: false + var onboardings: [Onboarding] = [ + Onboarding(id: 1, imageOrientation: .landscape), + Onboarding(id: 2, imageOrientation: .landscape), + Onboarding(id: 3, imageOrientation: .portrait), + Onboarding(id: 4, imageOrientation: .portrait) ] - - func reload() { - onboardings = onboardingsDictionary.map { key, value in - Onboarding(id: key, isPortraitImage: value) - } - } } diff --git a/NativeAppTemplate/Login/OnboardingRepositoryProtocol.swift b/NativeAppTemplate/Login/OnboardingRepositoryProtocol.swift index 1e91059..5a73bf8 100644 --- a/NativeAppTemplate/Login/OnboardingRepositoryProtocol.swift +++ b/NativeAppTemplate/Login/OnboardingRepositoryProtocol.swift @@ -4,11 +4,8 @@ // import Foundation -import OrderedCollections -@MainActor protocol OnboardingRepositoryProtocol: AnyObject, Observable, Sendable { +@MainActor +protocol OnboardingRepositoryProtocol: AnyObject, Observable, Sendable { var onboardings: [Onboarding] { get set } - var onboardingsDictionary: OrderedDictionary { get } - - func reload() } diff --git a/NativeAppTemplate/Models/CompleteScanResult.swift b/NativeAppTemplate/Models/CompleteScanResult.swift deleted file mode 100644 index b168fde..0000000 --- a/NativeAppTemplate/Models/CompleteScanResult.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// CompleteScanResult.swift -// NativeAppTemplate -// - -import Foundation - -enum CompleteScanResultType { - case idled - case completed - case reset - case failed - - var displayString: String { - switch self { - case .idled: - "Idling" - case .completed: - "Completed!" - case .reset: - "Reset!" - case .failed: - "Failed" - } - } -} - -struct CompleteScanResult { - var itemTag: ItemTag? - var type: CompleteScanResultType = .idled - var message = "" - var scannedAt = Date.now -} diff --git a/NativeAppTemplate/Models/ItemTag.swift b/NativeAppTemplate/Models/ItemTag.swift index 8a9895e..75906b6 100644 --- a/NativeAppTemplate/Models/ItemTag.swift +++ b/NativeAppTemplate/Models/ItemTag.swift @@ -8,25 +8,21 @@ import Foundation struct ItemTag: Codable, Hashable, Identifiable, Sendable { var id: String = "" var shopId: String = "" - var queueNumber: String = "" + var name: String = "" + var description: String = "" + var position: Int = 0 var state = ItemTagState.idled - var scanState = ScanState.unscanned var createdAt = Date.now - var customerReadAt: Date? var completedAt: Date? var shopName: String = "" - var alreadyCompleted: Bool? } extension ItemTag { - func scanUrl(itemTagType: ItemTagType) -> URL { - Utility.scanUrl(itemTagId: id, itemTagType: itemTagType.toJson()) - } - func toJson() -> [String: Any] { ["item_tag": [ - "queue_number": queueNumber + "name": name, + "description": description ] ] } diff --git a/NativeAppTemplate/Models/ItemTagData.swift b/NativeAppTemplate/Models/ItemTagData.swift deleted file mode 100644 index e91e1b6..0000000 --- a/NativeAppTemplate/Models/ItemTagData.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// ItemTagData.swift -// NativeAppTemplate -// - -import Foundation - -struct ItemTagData: Identifiable { - var id: String { - itemTagId - } - - var itemTagId: String - var itemTagType: ItemTagType - var isReadOnly: Bool - var scannedAt: Date -} - -// MARK: - Equatable - -extension ItemTagData: Equatable {} diff --git a/NativeAppTemplate/Models/ItemTagInfoFromNdefMessage.swift b/NativeAppTemplate/Models/ItemTagInfoFromNdefMessage.swift deleted file mode 100644 index 7f79f13..0000000 --- a/NativeAppTemplate/Models/ItemTagInfoFromNdefMessage.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// ItemTagInfoFromNdefMessage.swift -// NativeAppTemplate -// - -import Foundation - -struct ItemTagInfoFromNdefMessage { - var id: String - var type: String - var success: Bool - var message: String - - init() { - id = "" - type = "" - success = false - message = .messageWrittenOnTagIsWrong - } -} diff --git a/NativeAppTemplate/Models/ItemTagType.swift b/NativeAppTemplate/Models/ItemTagType.swift deleted file mode 100644 index 65d19f1..0000000 --- a/NativeAppTemplate/Models/ItemTagType.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// ItemTagType.swift -// NativeAppTemplate -// - -import Foundation - -enum ItemTagType: String, CaseIterable, Identifiable, Codable { - case server - case customer - - var id: Self { - self - } - - init(string: String) { - switch string { - case "server": - self = .server - case "customer": - self = .customer - default: - self = .server - } - } - - func toJson() -> String { - switch self { - case .server: - "server" - case .customer: - "customer" - } - } - - var displayString: String { - switch self { - case .server: - "Server" - case .customer: - "Customer" - } - } -} diff --git a/NativeAppTemplate/Models/MainTab.swift b/NativeAppTemplate/Models/MainTab.swift index 5075775..2303cca 100644 --- a/NativeAppTemplate/Models/MainTab.swift +++ b/NativeAppTemplate/Models/MainTab.swift @@ -8,7 +8,6 @@ import SwiftUI enum MainTab { case shops - case scan case settings } diff --git a/NativeAppTemplate/Models/Onboarding.swift b/NativeAppTemplate/Models/Onboarding.swift index ab71ea2..e3070b2 100644 --- a/NativeAppTemplate/Models/Onboarding.swift +++ b/NativeAppTemplate/Models/Onboarding.swift @@ -3,7 +3,12 @@ // NativeAppTemplate // +enum ImageOrientation: String, Hashable, Codable { + case portrait + case landscape +} + struct Onboarding: Hashable, Codable, Identifiable { var id: Int - var isPortraitImage: Bool = false + var imageOrientation: ImageOrientation = .landscape } diff --git a/NativeAppTemplate/Models/ScanState.swift b/NativeAppTemplate/Models/ScanState.swift deleted file mode 100644 index ecb003b..0000000 --- a/NativeAppTemplate/Models/ScanState.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// ScanState.swift -// NativeAppTemplate -// - -enum ScanState: String, Identifiable, CaseIterable, Codable { - case unscanned, - scanned - - var id: Self { - self - } - - init(string: String) { - switch string { - case "unscanned": - self = .unscanned - case "scanned": - self = .scanned - default: - self = .unscanned - } - } - - func toJson() -> String { - switch self { - case .unscanned: - "unscanned" - case .scanned: - "scanned" - } - } - - var displayString: String { - switch self { - case .unscanned: - "Unscanned" - case .scanned: - "Scanned" - } - } -} diff --git a/NativeAppTemplate/Models/Shop.swift b/NativeAppTemplate/Models/Shop.swift index b6b4463..c0bacb7 100644 --- a/NativeAppTemplate/Models/Shop.swift +++ b/NativeAppTemplate/Models/Shop.swift @@ -11,16 +11,10 @@ struct Shop: Codable, Identifiable, Sendable { var description: String var timeZone: String var itemTagsCount: Int = 0 - var scannedItemTagsCount: Int = 0 var completedItemTagsCount: Int = 0 - var displayShopServerPath: String = "" } extension Shop { - var displayShopServerUrl: URL { - URL(string: "\(NativeAppTemplateEnvironment.prod.baseURL.absoluteString)\(displayShopServerPath)")! - } - func toJsonForCreate() -> [String: Any] { [ "shop": [ diff --git a/NativeAppTemplate/Models/ShowTagInfoScanResult.swift b/NativeAppTemplate/Models/ShowTagInfoScanResult.swift deleted file mode 100644 index 15d0226..0000000 --- a/NativeAppTemplate/Models/ShowTagInfoScanResult.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// ShowTagInfoScanResult.swift -// NativeAppTemplate -// - -import Foundation - -enum ShowTagInfoScanResultType { - case idled - case succeeded - case failed -} - -struct ShowTagInfoScanResult { - var itemTag: ItemTag? - var itemTagType: ItemTagType = .server - var isReadOnly = false - var type: ShowTagInfoScanResultType = .idled - var message = "" - var scannedAt = Date.now -} diff --git a/NativeAppTemplate/NFCManager.swift b/NativeAppTemplate/NFCManager.swift deleted file mode 100644 index a16562e..0000000 --- a/NativeAppTemplate/NFCManager.swift +++ /dev/null @@ -1,269 +0,0 @@ -// -// NFCManager.swift -// NativeAppTemplate -// - -@preconcurrency import CoreNFC -import Foundation -import os - -protocol NFCManagerProtocol: Sendable { - @MainActor var scanResult: Result? { get } - @MainActor var isScanResultChanged: Bool { get } - @MainActor var isScanResultChangedForTesting: Bool { get } - - func startReading() async - func startReadingForTesting() async - - func startWriting(ndefMessage: NFCNDEFMessage, isLock: Bool) async -} - -final class NFCManager: NSObject, ObservableObject, @unchecked Sendable { - @MainActor static let shared = NFCManager() - - @MainActor @Published var scanResult: Result? - @MainActor @Published var isScanResultChanged = false - @MainActor @Published var isScanResultChangedForTesting = false - - private var internalScanResult: Result? { - @Sendable didSet { - Task { [internalScanResult] in - await MainActor.run { - self.scanResult = internalScanResult - } - } - } - } - - private var internalIsScanResultChanged: Bool = false { - @Sendable didSet { - Task { [internalIsScanResultChanged] in - await MainActor.run { - self.isScanResultChanged = internalIsScanResultChanged - } - } - } - } - - private var internalIsScanResultChangedForTesting: Bool = false { - @Sendable didSet { - Task { [internalIsScanResultChangedForTesting] in - await MainActor.run { - self.isScanResultChangedForTesting = internalIsScanResultChangedForTesting - } - } - } - } - - enum NFCOperation { - case read - case readForTesting - case write - } - - var nfcSession: NFCNDEFReaderSession? - var nfcOperation = NFCOperation.read - private var userNdefMessage: NFCNDEFMessage? - private var isLock = false - - @MainActor override init() {} -} - -extension NFCManager: NFCManagerProtocol { - func startReading() async { - internalScanResult = nil - internalIsScanResultChanged = false - nfcOperation = .read - startSession() - } - - func startReadingForTesting() async { - internalScanResult = nil - internalIsScanResultChangedForTesting = false - nfcOperation = .readForTesting - startSession() - } - - func startWriting(ndefMessage: NFCNDEFMessage, isLock: Bool) async { - nfcOperation = .write - userNdefMessage = ndefMessage - self.isLock = isLock - startSession() - } - - private func startSession() { - nfcSession = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: false) - nfcSession?.begin() - } -} - -extension NFCManager: NFCNDEFReaderSessionDelegate { - func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {} - - func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag]) { - nonisolated(unsafe) let session = session - guard let nfcTag = tags.first else { return } - nonisolated(unsafe) let tag = nfcTag - - session.connect(to: tag) { error in - if let error { - 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.codedDescription)") - return - } - - switch status { - case .notSupported: - session.invalidate(errorMessage: String.tagIsNotNdefFormatted) - case .readOnly: - switch self.nfcOperation { - case .read: - self.read(session: session, tag: tag, status: status) - case .readForTesting: - self.read(session: session, tag: tag, status: status, test: true) - case .write: - session.invalidate(errorMessage: String.tagIsNotWritable) - } - case .readWrite: - switch self.nfcOperation { - case .read: - self.read(session: session, tag: tag, status: status) - case .readForTesting: - self.read(session: session, tag: tag, status: status, test: true) - case .write: - if capacity < self.userNdefMessage!.length { - let errorMessage = "Tag capacity is too small. " + - "Minimum size requirement is \(self.userNdefMessage!.length) bytes." - session.invalidate(errorMessage: errorMessage) - return - } - - self.write(session: session, tag: tag) - } - @unknown default: - session.invalidate(errorMessage: String.unknownNdefStatus) - } - } - } - } - - private func read( - session: NFCNDEFReaderSession, - tag: NFCNDEFTag, - status: NFCNDEFStatus, - test: Bool = false - ) { - nonisolated(unsafe) let session = session - nonisolated(unsafe) let tag = tag - tag.readNDEF { [weak self] message, error in - if let error { - session.invalidate(errorMessage: "Reading error: \(error.codedDescription)") - if test { - self?.internalIsScanResultChangedForTesting = true - } else { - self?.internalIsScanResultChanged = true - } - return - } - - guard let message else { - session.invalidate(errorMessage: String.noRecrodsFound) - self?.internalScanResult = .failure(NFCError.scanFailed(String.tagNotValid)) - - if test { - self?.internalIsScanResultChangedForTesting = true - } else { - self?.internalIsScanResultChanged = true - } - return - } - - let isReadOnly = status == .readOnly - self?.setResultExtractedFrom(message: message, isReadOnly: isReadOnly, test: test) - - if test { - self?.internalIsScanResultChangedForTesting = true - } else { - self?.internalIsScanResultChanged = true - } - - session.invalidate() - } - } - - private func write(session: NFCNDEFReaderSession, tag: NFCNDEFTag) { - guard let userNdefMessage else { return } - - write( - session: session, - tag: tag, - ndefMessage: userNdefMessage, - isLock: isLock - ) { error in - guard error == nil else { return } - appLogger.debug("NFC Write: \(userNdefMessage, privacy: .private)") - } - } - - private func write( - session: NFCNDEFReaderSession, - tag: NFCNDEFTag, - ndefMessage: NFCNDEFMessage, - isLock: Bool = false, - completion: @escaping ((Error?) -> Void) - ) { - nonisolated(unsafe) let session = session - nonisolated(unsafe) let tag = tag - nonisolated(unsafe) let completion = completion - tag.writeNDEF(ndefMessage) { error in - if let error { - 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.codedDescription)") - completion(error) - } else { - session.alertMessage = String.writingSucceeded - session.invalidate() - completion(nil) - } - } - } else { - session.alertMessage = String.writingSucceeded - session.invalidate() - completion(nil) - } - } - } - } - - private func setResultExtractedFrom(message: NFCNDEFMessage, isReadOnly: Bool, test: Bool) { - let itemTagInfo = Utility.extractItemTagInfoFrom(message: message, test: test) - - if itemTagInfo.success { - let itemTagData = ItemTagData( - itemTagId: itemTagInfo.id, - itemTagType: ItemTagType(string: itemTagInfo.type), - isReadOnly: isReadOnly, - scannedAt: Date.now - ) - internalScanResult = .success(itemTagData) - } else { - internalScanResult = .failure(NFCError.scanFailed(itemTagInfo.message)) - } - } - - func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) {} - - func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) { - appLogger.debug("readerSession error: \(error.codedDescription, privacy: .private)") - } -} diff --git a/NativeAppTemplate/NativeAppTemplate.entitlements b/NativeAppTemplate/NativeAppTemplate.entitlements deleted file mode 100644 index 3b79e0d..0000000 --- a/NativeAppTemplate/NativeAppTemplate.entitlements +++ /dev/null @@ -1,14 +0,0 @@ - - - - - com.apple.developer.associated-domains - - applinks:api.nativeapptemplate.com - - com.apple.developer.nfc.readersession.formats - - TAG - - - diff --git a/NativeAppTemplate/Networking/Adapters/EntityAdapters/ItemTagAdapter.swift b/NativeAppTemplate/Networking/Adapters/EntityAdapters/ItemTagAdapter.swift index 3bac0ae..95f1599 100644 --- a/NativeAppTemplate/Networking/Adapters/EntityAdapters/ItemTagAdapter.swift +++ b/NativeAppTemplate/Networking/Adapters/EntityAdapters/ItemTagAdapter.swift @@ -14,9 +14,9 @@ struct ItemTagAdapter: EntityAdapter { guard resource.entityType == .itemTag else { throw EntityAdapterError.invalidResourceTypeForAdapter } guard let shopId = resource.attributes["shop_id"] as? String, - let queueNumber = resource.attributes["queue_number"] as? String, + let name = resource.attributes["name"] as? String, + let position = resource.attributes["position"] as? Int, let state = resource.attributes["state"] as? String, - let scanState = resource.attributes["scan_state"] as? String, let createdAtString = resource.attributes["created_at"] as? String, let shopName = resource.attributes["shop_name"] as? String else { @@ -25,25 +25,21 @@ struct ItemTagAdapter: EntityAdapter { let createdAt = createdAtString.iso8601! - let customerReadAtString = resource.attributes["customer_read_at"] as? String - let customerReadAt = customerReadAtString?.iso8601 + let description = resource.attributes["description"] as? String ?? "" let completedAtString = resource.attributes["completed_at"] as? String let completedAt = completedAtString?.iso8601 - let alreadyCompleted = resource.attributes["already_completed"] as? Bool - return ItemTag( id: resource.id, shopId: shopId, - queueNumber: queueNumber, + name: name, + description: description, + position: position, state: ItemTagState(string: state), - scanState: ScanState(string: scanState), createdAt: createdAt, - customerReadAt: customerReadAt, completedAt: completedAt, - shopName: shopName, - alreadyCompleted: alreadyCompleted + shopName: shopName ) } } diff --git a/NativeAppTemplate/Networking/Adapters/EntityAdapters/ShopAdapter.swift b/NativeAppTemplate/Networking/Adapters/EntityAdapters/ShopAdapter.swift index 85869d5..a8a0e9b 100644 --- a/NativeAppTemplate/Networking/Adapters/EntityAdapters/ShopAdapter.swift +++ b/NativeAppTemplate/Networking/Adapters/EntityAdapters/ShopAdapter.swift @@ -14,8 +14,7 @@ struct ShopAdapter: EntityAdapter { guard resource.entityType == .shop else { throw EntityAdapterError.invalidResourceTypeForAdapter } guard let name = resource.attributes["name"] as? String, - let timeZone = resource.attributes["time_zone"] as? String, - let displayShopServerPath = resource.attributes["display_shop_server_path"] as? String + let timeZone = resource.attributes["time_zone"] as? String else { throw EntityAdapterError.invalidOrMissingAttributes } @@ -26,9 +25,7 @@ struct ShopAdapter: EntityAdapter { description: resource.attributes["description"] as? String ?? "", timeZone: timeZone, itemTagsCount: resource.attributes["item_tags_count"] as? Int ?? 0, - scannedItemTagsCount: resource.attributes["scanned_item_tags_count"] as? Int ?? 0, - completedItemTagsCount: resource.attributes["completed_item_tags_count"] as? Int ?? 0, - displayShopServerPath: displayShopServerPath + completedItemTagsCount: resource.attributes["completed_item_tags_count"] as? Int ?? 0 ) } } diff --git a/NativeAppTemplate/Networking/Network/CertificatePinningDelegate.swift b/NativeAppTemplate/Networking/Network/CertificatePinningDelegate.swift index 1cbe7ec..2826410 100644 --- a/NativeAppTemplate/Networking/Network/CertificatePinningDelegate.swift +++ b/NativeAppTemplate/Networking/Network/CertificatePinningDelegate.swift @@ -17,7 +17,7 @@ final class CertificatePinningDelegate: NSObject, URLSessionDelegate { "kIdp6NNEd8wsugYyyIYFsi1ylMCED3hZbSR8ZFsa/A4=" ] - static let pinnedDomain = String.domain + static let pinnedDomain = Strings.domain /// ASN.1 header for EC 256-bit public key (SPKI prefix) private static let ecDsaSecp256r1Asn1Header: [UInt8] = [ diff --git a/NativeAppTemplate/Networking/Network/NativeAppTemplateEnvironment.swift b/NativeAppTemplate/Networking/Network/NativeAppTemplateEnvironment.swift index 05d3a9d..9fd00d2 100644 --- a/NativeAppTemplate/Networking/Network/NativeAppTemplateEnvironment.swift +++ b/NativeAppTemplate/Networking/Network/NativeAppTemplateEnvironment.swift @@ -13,10 +13,10 @@ struct NativeAppTemplateEnvironment: Equatable { } extension NativeAppTemplateEnvironment { - static let urlString = if String.port.isEmpty { - "\(String.scheme)://\(String.domain)" + static let urlString = if Strings.port.isEmpty { + "\(Strings.scheme)://\(Strings.domain)" } else { - "\(String.scheme)://\(String.domain):\(String.port)" + "\(Strings.scheme)://\(Strings.domain):\(Strings.port)" } static let prod = NativeAppTemplateEnvironment(baseURL: URL(string: urlString)!) diff --git a/NativeAppTemplate/Networking/Requests/ItemTagsRequest.swift b/NativeAppTemplate/Networking/Requests/ItemTagsRequest.swift index 9c41dc2..56ab9f1 100644 --- a/NativeAppTemplate/Networking/Requests/ItemTagsRequest.swift +++ b/NativeAppTemplate/Networking/Requests/ItemTagsRequest.swift @@ -230,7 +230,7 @@ struct CompleteItemTagRequest: Request { } } -struct ResetItemTagRequest: Request { +struct IdleItemTagRequest: Request { typealias Response = ItemTag // MARK: - Properties @@ -240,7 +240,7 @@ struct ResetItemTagRequest: Request { } var path: String { - "/shopkeeper/item_tags/\(id)/reset" + "/shopkeeper/item_tags/\(id)/idle" } var additionalHeaders: [String: String] = [:] diff --git a/NativeAppTemplate/Networking/Requests/PermissionsRequest.swift b/NativeAppTemplate/Networking/Requests/PermissionsRequest.swift index e4dded1..256e91a 100644 --- a/NativeAppTemplate/Networking/Requests/PermissionsRequest.swift +++ b/NativeAppTemplate/Networking/Requests/PermissionsRequest.swift @@ -9,7 +9,6 @@ struct PermissionsResponse: Sendable { var iosAppVersion: Int var shouldUpdatePrivacy: Bool var shouldUpdateTerms: Bool - var maximumQueueNumberLength: Int var shopLimitCount: Int } @@ -49,10 +48,6 @@ struct PermissionsRequest: Request { throw NativeAppTemplateAPIError.responseMissingRequiredMeta(field: "should_update_terms") } - guard let maximumQueueNumberLength = doc.meta["maximum_queue_number_length"] as? Int else { - throw NativeAppTemplateAPIError.responseMissingRequiredMeta(field: "maximum_queue_number_length") - } - guard let shopLimitCount = doc.meta["shop_limit_count"] as? Int else { throw NativeAppTemplateAPIError.responseMissingRequiredMeta(field: "shop_limit_count") } @@ -61,7 +56,6 @@ struct PermissionsRequest: Request { iosAppVersion: iosAppVersion, shouldUpdatePrivacy: shouldUpdatePrivacy, shouldUpdateTerms: shouldUpdateTerms, - maximumQueueNumberLength: maximumQueueNumberLength, shopLimitCount: shopLimitCount ) } diff --git a/NativeAppTemplate/Networking/Services/ItemTagsService.swift b/NativeAppTemplate/Networking/Services/ItemTagsService.swift index b144ac8..d1a0bdb 100644 --- a/NativeAppTemplate/Networking/Services/ItemTagsService.swift +++ b/NativeAppTemplate/Networking/Services/ItemTagsService.swift @@ -37,7 +37,7 @@ extension ItemTagsService { try await makeRequest(request: CompleteItemTagRequest(id: id)) } - func resetItemTag(id: String) async throws -> ResetItemTagRequest.Response { - try await makeRequest(request: ResetItemTagRequest(id: id)) + func idleItemTag(id: String) async throws -> IdleItemTagRequest.Response { + try await makeRequest(request: IdleItemTagRequest(id: id)) } } diff --git a/NativeAppTemplate/Persistence/KeychainStore/LoggedInShopkeeperKeychainStore.swift b/NativeAppTemplate/Persistence/KeychainStore/LoggedInShopkeeperKeychainStore.swift index 15d8084..1f13b34 100644 --- a/NativeAppTemplate/Persistence/KeychainStore/LoggedInShopkeeperKeychainStore.swift +++ b/NativeAppTemplate/Persistence/KeychainStore/LoggedInShopkeeperKeychainStore.swift @@ -7,8 +7,8 @@ import Foundation struct LoggedInShopkeeperKeychainStore: KeychainStore { // Make sure the account name doesn't match the bundle identifier! - var account = String.keychainAccountLoggedInShopkeeper - var service = String.keychainServiceLoggedInShopkeeper + var account = Strings.keychainAccountLoggedInShopkeeper + var service = Strings.keychainServiceLoggedInShopkeeper typealias DataType = LoggedInShopkeeper } diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1.imageset/Contents.json b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1.imageset/Contents.json index 3ad151e..c6d8f78 100644 --- a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1.imageset/Contents.json +++ b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1.imageset/Contents.json @@ -1,17 +1,17 @@ { "images" : [ { - "filename" : "overview1~universal@1x.png", + "filename" : "onboarding1.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "overview1~universal@2x.png", + "filename" : "onboarding1@2x.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "overview1~universal@3x.png", + "filename" : "onboarding1@3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1.imageset/onboarding1.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1.imageset/onboarding1.png new file mode 100644 index 0000000..069eee0 Binary files /dev/null and b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1.imageset/onboarding1.png differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1.imageset/onboarding1@2x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1.imageset/onboarding1@2x.png new file mode 100644 index 0000000..d50ec4a Binary files /dev/null and b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1.imageset/onboarding1@2x.png differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1.imageset/onboarding1@3x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1.imageset/onboarding1@3x.png new file mode 100644 index 0000000..8dd36f4 Binary files /dev/null and b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1.imageset/onboarding1@3x.png differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1.imageset/overview1~universal@1x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1.imageset/overview1~universal@1x.png deleted file mode 100644 index a489aeb..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1.imageset/overview1~universal@1x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1.imageset/overview1~universal@2x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1.imageset/overview1~universal@2x.png deleted file mode 100644 index 981debb..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1.imageset/overview1~universal@2x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1.imageset/overview1~universal@3x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1.imageset/overview1~universal@3x.png deleted file mode 100644 index 24f277b..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1.imageset/overview1~universal@3x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding10.imageset/Contents.json b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding10.imageset/Contents.json deleted file mode 100644 index 217fd0c..0000000 --- a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding10.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "filename" : "overview9~universal@1x.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "overview9~universal@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "overview9~universal@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding10.imageset/overview9~universal@1x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding10.imageset/overview9~universal@1x.png deleted file mode 100644 index f99c88b..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding10.imageset/overview9~universal@1x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding10.imageset/overview9~universal@2x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding10.imageset/overview9~universal@2x.png deleted file mode 100644 index 5e3b376..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding10.imageset/overview9~universal@2x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding10.imageset/overview9~universal@3x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding10.imageset/overview9~universal@3x.png deleted file mode 100644 index 475cb1e..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding10.imageset/overview9~universal@3x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding11.imageset/Contents.json b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding11.imageset/Contents.json deleted file mode 100644 index 74fb7ff..0000000 --- a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding11.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "filename" : "overview9_phone_customer1~universal@1x.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "overview9_phone_customer1~universal@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "overview9_phone_customer1~universal@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding11.imageset/overview9_phone_customer1~universal@1x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding11.imageset/overview9_phone_customer1~universal@1x.png deleted file mode 100644 index 7a3b4e5..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding11.imageset/overview9_phone_customer1~universal@1x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding11.imageset/overview9_phone_customer1~universal@2x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding11.imageset/overview9_phone_customer1~universal@2x.png deleted file mode 100644 index 1a21a15..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding11.imageset/overview9_phone_customer1~universal@2x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding11.imageset/overview9_phone_customer1~universal@3x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding11.imageset/overview9_phone_customer1~universal@3x.png deleted file mode 100644 index 63d4876..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding11.imageset/overview9_phone_customer1~universal@3x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding12.imageset/Contents.json b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding12.imageset/Contents.json deleted file mode 100644 index b91d394..0000000 --- a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding12.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "filename" : "overview13~universal@1x.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "overview13~universal@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "overview13~universal@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding12.imageset/overview13~universal@1x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding12.imageset/overview13~universal@1x.png deleted file mode 100644 index aa04c1d..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding12.imageset/overview13~universal@1x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding12.imageset/overview13~universal@2x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding12.imageset/overview13~universal@2x.png deleted file mode 100644 index 2be7d25..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding12.imageset/overview13~universal@2x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding12.imageset/overview13~universal@3x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding12.imageset/overview13~universal@3x.png deleted file mode 100644 index 3e9cd7c..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding12.imageset/overview13~universal@3x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding13.imageset/Contents.json b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding13.imageset/Contents.json deleted file mode 100644 index b4514a7..0000000 --- a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding13.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "filename" : "overview14~universal@1x.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "overview14~universal@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "overview14~universal@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding13.imageset/overview14~universal@1x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding13.imageset/overview14~universal@1x.png deleted file mode 100644 index 72ddc38..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding13.imageset/overview14~universal@1x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding13.imageset/overview14~universal@2x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding13.imageset/overview14~universal@2x.png deleted file mode 100644 index 2f796d3..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding13.imageset/overview14~universal@2x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding13.imageset/overview14~universal@3x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding13.imageset/overview14~universal@3x.png deleted file mode 100644 index 7d9bf57..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding13.imageset/overview14~universal@3x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1Slim.imageset/Contents.json b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1Slim.imageset/Contents.json deleted file mode 100644 index f732902..0000000 --- a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1Slim.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "filename" : "overview1_slim~universal@1x.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "overview1_slim~universal@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "overview1_slim~universal@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1Slim.imageset/overview1_slim~universal@1x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1Slim.imageset/overview1_slim~universal@1x.png deleted file mode 100644 index 1132e9b..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1Slim.imageset/overview1_slim~universal@1x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1Slim.imageset/overview1_slim~universal@2x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1Slim.imageset/overview1_slim~universal@2x.png deleted file mode 100644 index 0db8518..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1Slim.imageset/overview1_slim~universal@2x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1Slim.imageset/overview1_slim~universal@3x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1Slim.imageset/overview1_slim~universal@3x.png deleted file mode 100644 index 24e8755..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding1Slim.imageset/overview1_slim~universal@3x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding2.imageset/Contents.json b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding2.imageset/Contents.json index 150d51a..86fe1fc 100644 --- a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding2.imageset/Contents.json +++ b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding2.imageset/Contents.json @@ -1,17 +1,17 @@ { "images" : [ { - "filename" : "overview2~universal@1x.png", + "filename" : "onboarding2.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "overview2~universal@2x.png", + "filename" : "onboarding2@2x.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "overview2~universal@3x.png", + "filename" : "onboarding2@3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding2.imageset/onboarding2.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding2.imageset/onboarding2.png new file mode 100644 index 0000000..0d3b4c5 Binary files /dev/null and b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding2.imageset/onboarding2.png differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding2.imageset/onboarding2@2x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding2.imageset/onboarding2@2x.png new file mode 100644 index 0000000..d82ba12 Binary files /dev/null and b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding2.imageset/onboarding2@2x.png differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding2.imageset/onboarding2@3x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding2.imageset/onboarding2@3x.png new file mode 100644 index 0000000..7d67de5 Binary files /dev/null and b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding2.imageset/onboarding2@3x.png differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding2.imageset/overview2~universal@1x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding2.imageset/overview2~universal@1x.png deleted file mode 100644 index 208e71b..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding2.imageset/overview2~universal@1x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding2.imageset/overview2~universal@2x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding2.imageset/overview2~universal@2x.png deleted file mode 100644 index 5688e42..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding2.imageset/overview2~universal@2x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding2.imageset/overview2~universal@3x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding2.imageset/overview2~universal@3x.png deleted file mode 100644 index f95799a..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding2.imageset/overview2~universal@3x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding3.imageset/Contents.json b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding3.imageset/Contents.json index 0afd4d1..bae0dae 100644 --- a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding3.imageset/Contents.json +++ b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding3.imageset/Contents.json @@ -1,17 +1,17 @@ { "images" : [ { - "filename" : "overview6~universal@1x.png", + "filename" : "onboarding3.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "overview6~universal@2x.png", + "filename" : "onboarding3@2x.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "overview6~universal@3x.png", + "filename" : "onboarding3@3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding3.imageset/onboarding3.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding3.imageset/onboarding3.png new file mode 100644 index 0000000..df744a0 Binary files /dev/null and b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding3.imageset/onboarding3.png differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding3.imageset/onboarding3@2x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding3.imageset/onboarding3@2x.png new file mode 100644 index 0000000..f97e133 Binary files /dev/null and b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding3.imageset/onboarding3@2x.png differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding3.imageset/onboarding3@3x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding3.imageset/onboarding3@3x.png new file mode 100644 index 0000000..a7554f8 Binary files /dev/null and b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding3.imageset/onboarding3@3x.png differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding3.imageset/overview6~universal@1x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding3.imageset/overview6~universal@1x.png deleted file mode 100644 index 8f95ac6..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding3.imageset/overview6~universal@1x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding3.imageset/overview6~universal@2x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding3.imageset/overview6~universal@2x.png deleted file mode 100644 index e6851de..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding3.imageset/overview6~universal@2x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding3.imageset/overview6~universal@3x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding3.imageset/overview6~universal@3x.png deleted file mode 100644 index 208ff05..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding3.imageset/overview6~universal@3x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding4.imageset/Contents.json b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding4.imageset/Contents.json index bac0322..028e291 100644 --- a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding4.imageset/Contents.json +++ b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding4.imageset/Contents.json @@ -1,17 +1,17 @@ { "images" : [ { - "filename" : "overview6_phone_customer2~universal@1x.png", + "filename" : "onboarding4.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "overview6_phone_customer2~universal@2x.png", + "filename" : "onboarding4@2x.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "overview6_phone_customer2~universal@3x.png", + "filename" : "onboarding4@3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding4.imageset/onboarding4.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding4.imageset/onboarding4.png new file mode 100644 index 0000000..cdb9c58 Binary files /dev/null and b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding4.imageset/onboarding4.png differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding4.imageset/onboarding4@2x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding4.imageset/onboarding4@2x.png new file mode 100644 index 0000000..f54dfd6 Binary files /dev/null and b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding4.imageset/onboarding4@2x.png differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding4.imageset/onboarding4@3x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding4.imageset/onboarding4@3x.png new file mode 100644 index 0000000..bcb4417 Binary files /dev/null and b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding4.imageset/onboarding4@3x.png differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding4.imageset/overview6_phone_customer2~universal@1x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding4.imageset/overview6_phone_customer2~universal@1x.png deleted file mode 100644 index ae5b650..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding4.imageset/overview6_phone_customer2~universal@1x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding4.imageset/overview6_phone_customer2~universal@2x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding4.imageset/overview6_phone_customer2~universal@2x.png deleted file mode 100644 index bbbcf98..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding4.imageset/overview6_phone_customer2~universal@2x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding4.imageset/overview6_phone_customer2~universal@3x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding4.imageset/overview6_phone_customer2~universal@3x.png deleted file mode 100644 index 103473a..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding4.imageset/overview6_phone_customer2~universal@3x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding5.imageset/overview7~universal@1x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding5.imageset/overview7~universal@1x.png deleted file mode 100644 index a0d1f14..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding5.imageset/overview7~universal@1x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding5.imageset/overview7~universal@2x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding5.imageset/overview7~universal@2x.png deleted file mode 100644 index 44b4331..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding5.imageset/overview7~universal@2x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding5.imageset/overview7~universal@3x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding5.imageset/overview7~universal@3x.png deleted file mode 100644 index 33eb96c..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding5.imageset/overview7~universal@3x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding6.imageset/Contents.json b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding6.imageset/Contents.json deleted file mode 100644 index 1199c20..0000000 --- a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding6.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "filename" : "overview8~universal@1x.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "overview8~universal@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "overview8~universal@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding6.imageset/overview8~universal@1x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding6.imageset/overview8~universal@1x.png deleted file mode 100644 index 9a7322e..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding6.imageset/overview8~universal@1x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding6.imageset/overview8~universal@2x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding6.imageset/overview8~universal@2x.png deleted file mode 100644 index 4a42338..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding6.imageset/overview8~universal@2x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding6.imageset/overview8~universal@3x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding6.imageset/overview8~universal@3x.png deleted file mode 100644 index fb195d4..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding6.imageset/overview8~universal@3x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding7.imageset/Contents.json b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding7.imageset/Contents.json deleted file mode 100644 index 37efc1f..0000000 --- a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding7.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "filename" : "overview8_phone_server2~universal@1x.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "overview8_phone_server2~universal@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "overview8_phone_server2~universal@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding7.imageset/overview8_phone_server2~universal@1x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding7.imageset/overview8_phone_server2~universal@1x.png deleted file mode 100644 index 69ba137..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding7.imageset/overview8_phone_server2~universal@1x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding7.imageset/overview8_phone_server2~universal@2x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding7.imageset/overview8_phone_server2~universal@2x.png deleted file mode 100644 index 121547b..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding7.imageset/overview8_phone_server2~universal@2x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding7.imageset/overview8_phone_server2~universal@3x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding7.imageset/overview8_phone_server2~universal@3x.png deleted file mode 100644 index 29d4130..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding7.imageset/overview8_phone_server2~universal@3x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding8.imageset/Contents.json b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding8.imageset/Contents.json deleted file mode 100644 index 8089f48..0000000 --- a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding8.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "filename" : "overview8_phone_server3~universal@1x.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "overview8_phone_server3~universal@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "overview8_phone_server3~universal@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding8.imageset/overview8_phone_server3~universal@1x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding8.imageset/overview8_phone_server3~universal@1x.png deleted file mode 100644 index 109baf9..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding8.imageset/overview8_phone_server3~universal@1x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding8.imageset/overview8_phone_server3~universal@2x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding8.imageset/overview8_phone_server3~universal@2x.png deleted file mode 100644 index 670d83b..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding8.imageset/overview8_phone_server3~universal@2x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding8.imageset/overview8_phone_server3~universal@3x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding8.imageset/overview8_phone_server3~universal@3x.png deleted file mode 100644 index 0d0db3b..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding8.imageset/overview8_phone_server3~universal@3x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding9.imageset/Contents.json b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding9.imageset/Contents.json deleted file mode 100644 index 2fc221b..0000000 --- a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding9.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "filename" : "overview8_2~universal@1x.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "overview8_2~universal@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "overview8_2~universal@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding9.imageset/overview8_2~universal@1x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding9.imageset/overview8_2~universal@1x.png deleted file mode 100644 index ee74b05..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding9.imageset/overview8_2~universal@1x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding9.imageset/overview8_2~universal@2x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding9.imageset/overview8_2~universal@2x.png deleted file mode 100644 index 56ad329..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding9.imageset/overview8_2~universal@2x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding9.imageset/overview8_2~universal@3x.png b/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding9.imageset/overview8_2~universal@3x.png deleted file mode 100644 index d644a5a..0000000 Binary files a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding9.imageset/overview8_2~universal@3x.png and /dev/null differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding5.imageset/Contents.json b/NativeAppTemplate/Resources/Assets.xcassets/hero.imageset/Contents.json similarity index 64% rename from NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding5.imageset/Contents.json rename to NativeAppTemplate/Resources/Assets.xcassets/hero.imageset/Contents.json index ce975c3..e2d4444 100644 --- a/NativeAppTemplate/Resources/Assets.xcassets/Onboarding/onboarding5.imageset/Contents.json +++ b/NativeAppTemplate/Resources/Assets.xcassets/hero.imageset/Contents.json @@ -1,17 +1,17 @@ { "images" : [ { - "filename" : "overview7~universal@1x.png", + "filename" : "hero.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "overview7~universal@2x.png", + "filename" : "hero@2x.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "overview7~universal@3x.png", + "filename" : "hero@3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/NativeAppTemplate/Resources/Assets.xcassets/hero.imageset/hero.png b/NativeAppTemplate/Resources/Assets.xcassets/hero.imageset/hero.png new file mode 100644 index 0000000..7a2d682 Binary files /dev/null and b/NativeAppTemplate/Resources/Assets.xcassets/hero.imageset/hero.png differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/hero.imageset/hero@2x.png b/NativeAppTemplate/Resources/Assets.xcassets/hero.imageset/hero@2x.png new file mode 100644 index 0000000..8628e01 Binary files /dev/null and b/NativeAppTemplate/Resources/Assets.xcassets/hero.imageset/hero@2x.png differ diff --git a/NativeAppTemplate/Resources/Assets.xcassets/hero.imageset/hero@3x.png b/NativeAppTemplate/Resources/Assets.xcassets/hero.imageset/hero@3x.png new file mode 100644 index 0000000..67d6409 Binary files /dev/null and b/NativeAppTemplate/Resources/Assets.xcassets/hero.imageset/hero@3x.png differ diff --git a/NativeAppTemplate/Sessions/SessionController.swift b/NativeAppTemplate/Sessions/SessionController.swift index 7da5ade..34c94d1 100644 --- a/NativeAppTemplate/Sessions/SessionController.swift +++ b/NativeAppTemplate/Sessions/SessionController.swift @@ -14,15 +14,10 @@ import Observation private(set) var permissionState: PermissionState = .notLoaded private(set) var didFetchPermissions = false var shouldPopToRootView = false - var didBackgroundTagReading = false - - var completeScanResult = CompleteScanResult() - var showTagInfoScanResult = ShowTagInfoScanResult() var shouldUpdateApp = false var shouldUpdatePrivacy = false var shouldUpdateTerms = false - var maximumQueueNumberLength = 0 var shopLimitCount = 0 var shopkeeper: Shopkeeper? { @@ -175,8 +170,6 @@ import Observation shouldUpdateApp = Int(Bundle.main.appBuild)! < permissionsResponse.iosAppVersion shouldUpdatePrivacy = permissionsResponse.shouldUpdatePrivacy shouldUpdateTerms = permissionsResponse.shouldUpdateTerms - maximumQueueNumberLength = permissionsResponse.maximumQueueNumberLength - shopLimitCount = permissionsResponse.shopLimitCount didFetchPermissions = true diff --git a/NativeAppTemplate/Sessions/SessionControllerProtocol.swift b/NativeAppTemplate/Sessions/SessionControllerProtocol.swift index 7d1f314..7233c63 100644 --- a/NativeAppTemplate/Sessions/SessionControllerProtocol.swift +++ b/NativeAppTemplate/Sessions/SessionControllerProtocol.swift @@ -34,15 +34,10 @@ protocol SessionControllerProtocol: AnyObject, Observable, Sendable { var didFetchPermissions: Bool { get } var shouldPopToRootView: Bool { get set } - var didBackgroundTagReading: Bool { get set } - - var completeScanResult: CompleteScanResult { get set } - var showTagInfoScanResult: ShowTagInfoScanResult { get set } var shouldUpdateApp: Bool { get set } var shouldUpdatePrivacy: Bool { get set } var shouldUpdateTerms: Bool { get set } - var maximumQueueNumberLength: Int { get set } var shopLimitCount: Int { get set } var shopkeeper: Shopkeeper? { get set } diff --git a/NativeAppTemplate/Styleguide/Color+Extensions.swift b/NativeAppTemplate/Styleguide/Color+Extensions.swift index 4a7a2b9..d9781ea 100644 --- a/NativeAppTemplate/Styleguide/Color+Extensions.swift +++ b/NativeAppTemplate/Styleguide/Color+Extensions.swift @@ -141,18 +141,6 @@ extension Color { static let idlingTagBorder = coolGrey10 // #F5F7FA static let idlingTagForeground = coolGrey2 // #323F4B - // MARK: Tags - Preparing - - static let preparingTagBackground = yellow9 // #FFF3C4 - static let preparingTagBorder = yellow1 // #8D2B0B - static let preparingTagForeground = yellow1 // #8D2B0B - - // MARK: Tags - Customer Scanned - - static let customerScannedTagBackground = red9 // #FFBDBD - static let customerScannedTagBorder = coolGrey10 // #F5F7FA - static let customerScannedTagForeground = red1 // #610316 - // MARK: Tags - Completed static let completedTagBackground = green9 // #C1F2C7 @@ -184,10 +172,6 @@ extension Color { static let validationError = red5 // #E12D39 - // MARK: Calendar - - static let todayIndicator = red5 // #E12D39 - // MARK: Glass static let glassBorder = coolGrey10 // #F5F7FA @@ -198,10 +182,6 @@ extension Color { static let arrowBackground = coolGrey10 // #F5F7FA - // MARK: Tag Webpage Preview - - static let tagWebpagePreviewBackground = coolGrey1 // #1F2933 - // MARK: Accent static let accent = lightBlue7 // #5ED0FA @@ -339,30 +319,6 @@ extension ShapeStyle where Self == Color { Color.idlingTagForeground } - static var preparingTagBackground: Color { - Color.preparingTagBackground - } - - static var preparingTagBorder: Color { - Color.preparingTagBorder - } - - static var preparingTagForeground: Color { - Color.preparingTagForeground - } - - static var customerScannedTagBackground: Color { - Color.customerScannedTagBackground - } - - static var customerScannedTagBorder: Color { - Color.customerScannedTagBorder - } - - static var customerScannedTagForeground: Color { - Color.customerScannedTagForeground - } - static var completedTagBackground: Color { Color.completedTagBackground } @@ -429,11 +385,6 @@ extension ShapeStyle where Self == Color { Color.validationError } - /// Calendar - static var todayIndicator: Color { - Color.todayIndicator - } - /// Glass static var glassBorder: Color { Color.glassBorder @@ -452,11 +403,6 @@ extension ShapeStyle where Self == Color { Color.arrowBackground } - /// Tag Webpage Preview - static var tagWebpagePreviewBackground: Color { - Color.tagWebpagePreviewBackground - } - /// Accent static var accent: Color { Color.accent diff --git a/NativeAppTemplate/Styleguide/Font+Extensions.swift b/NativeAppTemplate/Styleguide/Font+Extensions.swift index 49fbe97..cc8d215 100644 --- a/NativeAppTemplate/Styleguide/Font+Extensions.swift +++ b/NativeAppTemplate/Styleguide/Font+Extensions.swift @@ -34,10 +34,6 @@ extension Font { .system(size: UIFontMetrics.default.scaledValue(for: 18.0)).weight(.semibold) } - static var uiNumberBox: Font { - .system(size: UIFontMetrics.default.scaledValue(for: 12.0)).weight(.bold) - } - static var uiBodyAppleDefault: Font { .body } @@ -75,10 +71,6 @@ extension Font { .system(size: UIFontMetrics.default.scaledValue(for: 14.0)) } - static var uiUppercase: Font { - .system(size: UIFontMetrics.default.scaledValue(for: 12.0)).weight(.semibold) - } - static var uiUppercaseTag: Font { .system(size: UIFontMetrics.default.scaledValue(for: 10.0)).weight(.semibold) } diff --git a/NativeAppTemplate/UI/App Root/AcceptPrivacyView.swift b/NativeAppTemplate/UI/App Root/AcceptPrivacyView.swift index e553299..fbb024b 100644 --- a/NativeAppTemplate/UI/App Root/AcceptPrivacyView.swift +++ b/NativeAppTemplate/UI/App Root/AcceptPrivacyView.swift @@ -42,18 +42,18 @@ private extension AcceptPrivacyView { var acceptPrivacyView: some View { VStack { - let agreement = "Please accept updated [\(String.privacyPolicy)](\(String.privacyPolicyUrl))." + let agreement = "Please accept updated [\(Strings.privacyPolicy)](\(Strings.privacyPolicyUrl))." Text(.init(agreement)) .padding(.top, NativeAppTemplateConstants.Spacing.xl) - MainButtonView(title: String.accept, type: .primary(withArrow: false)) { + MainButtonView(title: Strings.accept, type: .primary(withArrow: false)) { viewModel.updateConfirmedPrivacyVersion() } .padding(NativeAppTemplateConstants.Spacing.md) Spacer() } - .navigationTitle(String.privacyPolicyUpdated) + .navigationTitle(Strings.privacyPolicyUpdated) .navigationBarTitleDisplayMode(.inline) } } diff --git a/NativeAppTemplate/UI/App Root/AcceptPrivacyViewModel.swift b/NativeAppTemplate/UI/App Root/AcceptPrivacyViewModel.swift index d4e0c21..764337a 100644 --- a/NativeAppTemplate/UI/App Root/AcceptPrivacyViewModel.swift +++ b/NativeAppTemplate/UI/App Root/AcceptPrivacyViewModel.swift @@ -29,11 +29,11 @@ final class AcceptPrivacyViewModel { do { isUpdating = true try await sessionController.updateConfirmedPrivacyVersion() - messageBus.post(message: Message(level: .success, message: .confirmedPrivacyVersionUpdated)) + messageBus.post(message: Message(level: .success, message: Strings.confirmedPrivacyVersionUpdated)) } catch { messageBus.post(message: Message( level: .error, - message: "\(String.confirmedPrivacyVersionUpdatedError) \(error.codedDescription)", + message: "\(Strings.confirmedPrivacyVersionUpdatedError) \(error.codedDescription)", autoDismiss: false )) } diff --git a/NativeAppTemplate/UI/App Root/AcceptTermsView.swift b/NativeAppTemplate/UI/App Root/AcceptTermsView.swift index a0abb06..7f01b34 100644 --- a/NativeAppTemplate/UI/App Root/AcceptTermsView.swift +++ b/NativeAppTemplate/UI/App Root/AcceptTermsView.swift @@ -42,18 +42,18 @@ private extension AcceptTermsView { var acceptTermsView: some View { VStack { - let agreement = "Please accept updated [\(String.termsOfUse)](\(String.termsOfUseUrl))." + let agreement = "Please accept updated [\(Strings.termsOfUse)](\(Strings.termsOfUseUrl))." Text(.init(agreement)) .padding(.top, NativeAppTemplateConstants.Spacing.xl) - MainButtonView(title: String.accept, type: .primary(withArrow: false)) { + MainButtonView(title: Strings.accept, type: .primary(withArrow: false)) { viewModel.updateConfirmedTermsVersion() } .padding(NativeAppTemplateConstants.Spacing.md) Spacer() } - .navigationTitle(String.termsOfUseUpdated) + .navigationTitle(Strings.termsOfUseUpdated) .navigationBarTitleDisplayMode(.inline) } } diff --git a/NativeAppTemplate/UI/App Root/AcceptTermsViewModel.swift b/NativeAppTemplate/UI/App Root/AcceptTermsViewModel.swift index 36a07a7..2905469 100644 --- a/NativeAppTemplate/UI/App Root/AcceptTermsViewModel.swift +++ b/NativeAppTemplate/UI/App Root/AcceptTermsViewModel.swift @@ -29,11 +29,11 @@ final class AcceptTermsViewModel { do { isUpdating = true try await sessionController.updateConfirmedTermsVersion() - messageBus.post(message: Message(level: .success, message: .confirmedTermsVersionUpdated)) + messageBus.post(message: Message(level: .success, message: Strings.confirmedTermsVersionUpdated)) } catch { messageBus.post(message: Message( level: .error, - message: "\(String.confirmedTermsVersionUpdatedError) \(error.codedDescription)", + message: "\(Strings.confirmedTermsVersionUpdatedError) \(error.codedDescription)", autoDismiss: false )) } diff --git a/NativeAppTemplate/UI/App Root/AppTabView.swift b/NativeAppTemplate/UI/App Root/AppTabView.swift index 820a5dc..d7ff848 100644 --- a/NativeAppTemplate/UI/App Root/AppTabView.swift +++ b/NativeAppTemplate/UI/App Root/AppTabView.swift @@ -7,7 +7,6 @@ import SwiftUI struct AppTabView< ShopListView: View, - ScanView: View, SettingsView: View > { @Environment(\.sessionController) private var sessionController @@ -15,16 +14,13 @@ struct AppTabView< @Environment(TabViewModel.self) private var model @State var navigationPathShops = NavigationPath() private let shopListView: () -> ShopListView - private let scanView: () -> ScanView private let settingsView: () -> SettingsView init( shopListView: @escaping () -> ShopListView, - scanView: @escaping () -> ScanView, settingsView: @escaping () -> SettingsView ) { self.shopListView = shopListView - self.scanView = scanView self.settingsView = settingsView } } @@ -57,23 +53,15 @@ extension AppTabView: View { tab( content: shopListView, navigationPath: $navigationPathShops, - text: .shops, + text: Strings.shops, imageName: "storefront.fill", tab: .shops ) - tab( - content: scanView, - navigationPath: nil, - text: .scan, - imageName: "platter.filled.bottom.iphone", - tab: .scan - ) - tab( content: settingsView, navigationPath: nil, - text: .settings, + text: Strings.settings, imageName: "gearshape.fill", tab: .settings ) @@ -96,7 +84,6 @@ struct AppTabView_Previews: PreviewProvider { static var previews: some View { AppTabView( shopListView: { Text(verbatim: "SHOPS") }, - scanView: { Text(verbatim: "SCAN") }, settingsView: { Text(verbatim: "SETTINGS") } ).environment(TabViewModel()) } diff --git a/NativeAppTemplate/UI/App Root/ForgotPasswordView.swift b/NativeAppTemplate/UI/App Root/ForgotPasswordView.swift index 5baca37..e001651 100644 --- a/NativeAppTemplate/UI/App Root/ForgotPasswordView.swift +++ b/NativeAppTemplate/UI/App Root/ForgotPasswordView.swift @@ -45,27 +45,27 @@ private extension ForgotPasswordView { var forgotPasswordView: some View { Form { Section { - TextField(String.placeholderEmail, text: $viewModel.email) + TextField(Strings.placeholderEmail, text: $viewModel.email) .textContentType(.emailAddress) .autocapitalization(.none) } header: { - Text(String.email) + Text(Strings.email) } footer: { if viewModel.isEmailBlank { - Text(String.emailIsRequired) + Text(Strings.emailIsRequired) .foregroundStyle(.validationError) } else if viewModel.isEmailInvalid { - Text(String.emailIsInvalid) + Text(Strings.emailIsInvalid) .foregroundStyle(.validationError) } } - MainButtonView(title: String.buttonSendMeResetPasswordInstructions, type: .primary(withArrow: false)) { + MainButtonView(title: Strings.buttonSendMeResetPasswordInstructions, type: .primary(withArrow: false)) { viewModel.sendMeResetPasswordInstructionsTapped() } .disabled(viewModel.hasInvalidData) .listRowBackground(Color.clear) } - .navigationTitle(String.forgotYourPassword) + .navigationTitle(Strings.forgotYourPassword) } } diff --git a/NativeAppTemplate/UI/App Root/ForgotPasswordViewModel.swift b/NativeAppTemplate/UI/App Root/ForgotPasswordViewModel.swift index 20c8267..eb932d9 100644 --- a/NativeAppTemplate/UI/App Root/ForgotPasswordViewModel.swift +++ b/NativeAppTemplate/UI/App Root/ForgotPasswordViewModel.swift @@ -25,11 +25,11 @@ final class ForgotPasswordViewModel { } var hasInvalidData: Bool { - if Utility.isBlank(email) { + if email.isBlank { return true } - if !Utility.validateEmail(email) { + if !email.isValidEmail { return true } @@ -37,11 +37,11 @@ final class ForgotPasswordViewModel { } var isEmailBlank: Bool { - Utility.isBlank(email) + email.isBlank } var isEmailInvalid: Bool { - !Utility.isBlank(email) && !Utility.validateEmail(email) + !email.isBlank && !email.isValidEmail } func sendMeResetPasswordInstructionsTapped() { @@ -56,7 +56,7 @@ final class ForgotPasswordViewModel { try await signUpRepository.sendResetPasswordInstruction(sendResetPassword: sendResetPassword) messageBus.post(message: Message( level: .success, - message: .sentResetPasswordInstruction, + message: Strings.sentResetPasswordInstruction, autoDismiss: false )) shouldDismiss = true @@ -64,7 +64,7 @@ final class ForgotPasswordViewModel { UIApplication.dismissKeyboard() messageBus.post(message: Message( level: .error, - message: String.sentResetPasswordInstructionError, + message: Strings.sentResetPasswordInstructionError, autoDismiss: false )) } diff --git a/NativeAppTemplate/UI/App Root/MainView.swift b/NativeAppTemplate/UI/App Root/MainView.swift index e73fe86..97e4aa3 100644 --- a/NativeAppTemplate/UI/App Root/MainView.swift +++ b/NativeAppTemplate/UI/App Root/MainView.swift @@ -19,27 +19,6 @@ struct MainView: View { contentView .background(Color.backgroundColor) .overlay(MessageBarView(messageBus: messageBus), alignment: .bottom) - .onContinueUserActivity(NSUserActivityTypeBrowsingWeb, perform: { userActivity in - if let viewModel { - viewModel.handleBackgroundTagReading(userActivity) - } - }) - .alert( - String.itemTagAlreadyCompleted, - isPresented: Binding( - get: { viewModel?.isShowingResetConfirmationDialog ?? false }, - set: { viewModel?.isShowingResetConfirmationDialog = $0 } - ) - ) { - Button(String.reset, role: .destructive) { - viewModel?.resetTag() - } - Button(String.cancel, role: .cancel) { - viewModel?.cancelResetDialog() - } - } message: { - Text(String.areYouSure) - } .onAppear { if viewModel == nil { viewModel = MainViewModel( @@ -73,7 +52,7 @@ private extension MainView { case .error: ErrorView( buttonAction: { viewModel?.logout() }, - buttonTitle: .backToStartScreen + buttonTitle: Strings.backToStartScreen ) } } @@ -115,7 +94,6 @@ private extension MainView { if dataManager.isRebuildingRepositories { AppTabView( shopListView: LoadingView.init, - scanView: LoadingView.init, settingsView: LoadingView.init ) .environment(tabViewModel) @@ -123,14 +101,12 @@ private extension MainView { if sessionController.shouldUpdateApp { AppTabView( shopListView: NeedAppUpdatesView.init, - scanView: NeedAppUpdatesView.init, settingsView: NeedAppUpdatesView.init ) .environment(tabViewModel) } else { AppTabView( shopListView: shopListView, - scanView: scanView, settingsView: settingsView ) .environment(tabViewModel) @@ -139,7 +115,6 @@ private extension MainView { case .offline: AppTabView( shopListView: OfflineView.init, - scanView: OfflineView.init, settingsView: OfflineView.init ) .environment(tabViewModel) @@ -159,17 +134,6 @@ private extension MainView { ) } - func scanView() -> ScanView { - .init( - viewModel: ScanViewModel( - itemTagRepository: dataManager.itemTagRepository, - sessionController: dataManager.sessionController, - messageBus: messageBus, - nfcManager: appSingletons.nfcManager - ) - ) - } - func settingsView() -> SettingsView { .init( viewModel: SettingsViewModel( diff --git a/NativeAppTemplate/UI/App Root/MainViewModel.swift b/NativeAppTemplate/UI/App Root/MainViewModel.swift index 8b665df..8a29d79 100644 --- a/NativeAppTemplate/UI/App Root/MainViewModel.swift +++ b/NativeAppTemplate/UI/App Root/MainViewModel.swift @@ -3,17 +3,12 @@ // NativeAppTemplate // -import CoreNFC import Observation import SwiftUI @Observable @MainActor final class MainViewModel { - var isShowingForceAppUpdatesAlert = false - var itemTagId: String? - var isResetting = false - var isShowingResetConfirmationDialog = false var arePrivacyAccepted = false var areTermsAccepted = false @@ -34,91 +29,9 @@ final class MainViewModel { self.tabViewModel = tabViewModel } - func resetTag() { - guard let itemTagId else { return } - resetTag(itemTagId: itemTagId) - } - - func cancelResetDialog() { - isShowingResetConfirmationDialog = false - } - - func handleBackgroundTagReading(_ userActivity: NSUserActivity) { - guard sessionController.isLoggedIn else { - messageBus.post(message: Message(level: .error, message: String.pleaseSignIn, autoDismiss: false)) - return - } - - let ndefMessage = userActivity.ndefMessagePayload - guard !ndefMessage.records.isEmpty, - ndefMessage.records[0].typeNameFormat != .empty else { - return - } - - let itemTagInfo = Utility.extractItemTagInfoFrom(message: ndefMessage) - - if itemTagInfo.success { - itemTagId = itemTagInfo.id - completeTag(itemTagId: itemTagInfo.id) - } else { - messageBus.post(message: Message(level: .error, message: itemTagInfo.message, autoDismiss: false)) - tabViewModel.selectedTab = .scan - } - } - func logout() { Task { try await sessionController.logout() } } - - // MARK: - Private Methods - - private func completeTag(itemTagId: String) { - Task { - do { - let itemTag = try await dataManager.itemTagRepository.complete(id: itemTagId) - - sessionController.completeScanResult = CompleteScanResult( - itemTag: itemTag, - type: .completed - ) - - if itemTag.alreadyCompleted! { - isShowingResetConfirmationDialog = true - } - } catch { - sessionController.completeScanResult = CompleteScanResult( - type: .failed, - message: error.codedDescription - ) - } - - sessionController.didBackgroundTagReading = true - tabViewModel.selectedTab = .scan - } - } - - private func resetTag(itemTagId: String) { - Task { - isResetting = true - - do { - let itemTag = try await dataManager.itemTagRepository.reset(id: itemTagId) - sessionController.completeScanResult = CompleteScanResult( - itemTag: itemTag, - type: .reset - ) - } catch { - sessionController.completeScanResult = CompleteScanResult( - type: .failed, - message: error.codedDescription - ) - } - - isResetting = false - sessionController.didBackgroundTagReading = true - tabViewModel.selectedTab = .scan - } - } } diff --git a/NativeAppTemplate/UI/App Root/OnboardingView.swift b/NativeAppTemplate/UI/App Root/OnboardingView.swift index 02cd4d9..16185aa 100644 --- a/NativeAppTemplate/UI/App Root/OnboardingView.swift +++ b/NativeAppTemplate/UI/App Root/OnboardingView.swift @@ -6,7 +6,6 @@ import SwiftUI struct OnboardingView: View { - let isAppStorePromotion = false @State private var viewModel: OnboardingViewModel init(onboardingRepository: OnboardingRepositoryProtocol) { @@ -16,9 +15,6 @@ struct OnboardingView: View { var body: some View { NavigationStack { contentView - .task { - viewModel.reload() - } } } } @@ -35,21 +31,19 @@ private extension OnboardingView { page( image: "onboarding\(id)", text: viewModel.onboardingDescription(index: id), - isPortraitImage: onboarding.isPortraitImage + imageOrientation: onboarding.imageOrientation ) } } - .tabViewStyle(.page(indexDisplayMode: isAppStorePromotion ? .never : .always)) + .tabViewStyle(.page(indexDisplayMode: .always)) .toolbar { - if !isAppStorePromotion { - ToolbarItem(placement: .navigationBarLeading) { - Link(String.howToUse, destination: URL(string: String.howToUseUrl)!) - } - ToolbarItem(placement: .navigationBarTrailing) { - NavigationLink(destination: SignUpOrSignInView()) { - Text(verbatim: "Start") - .font(.title) - } + ToolbarItem(placement: .navigationBarLeading) { + Link(Strings.supportWebsite, destination: URL(string: Strings.supportWebsiteUrl)!) + } + ToolbarItem(placement: .navigationBarTrailing) { + NavigationLink(destination: SignUpOrSignInView()) { + Text(verbatim: "Start") + .font(.title) } } } @@ -66,13 +60,13 @@ private extension OnboardingView { .frame(width: 256, height: 24) } - private func page(image: String, text: String, isPortraitImage: Bool) -> some View { + private func page(image: String, text: String, imageOrientation: ImageOrientation) -> some View { ZStack(alignment: .bottom) { Image(image) .resizable() .aspectRatio(contentMode: .fit) .padding(.top, NativeAppTemplateConstants.Spacing.md) - .padding(.bottom, isPortraitImage ? 0 : 192) + .padding(.bottom, imageOrientation == .portrait ? 0 : 192) ZStack(alignment: .top) { VStack { diff --git a/NativeAppTemplate/UI/App Root/OnboardingViewModel.swift b/NativeAppTemplate/UI/App Root/OnboardingViewModel.swift index 7247e7e..b2efd18 100644 --- a/NativeAppTemplate/UI/App Root/OnboardingViewModel.swift +++ b/NativeAppTemplate/UI/App Root/OnboardingViewModel.swift @@ -9,49 +9,28 @@ import SwiftUI @Observable @MainActor final class OnboardingViewModel { - var onboardings: [Onboarding] = [] - private let onboardingRepository: OnboardingRepositoryProtocol - init(onboardingRepository: OnboardingRepositoryProtocol) { - self.onboardingRepository = onboardingRepository + var onboardings: [Onboarding] { + onboardingRepository.onboardings } - func reload() { - onboardingRepository.reload() - onboardings = onboardingRepository.onboardings + init(onboardingRepository: OnboardingRepositoryProtocol) { + self.onboardingRepository = onboardingRepository } func onboardingDescription(index: Int) -> String { switch index { case 1: - String.onboardingDescription1 + Strings.onboardingDescription1 case 2: - String.onboardingDescription2 + Strings.onboardingDescription2 case 3: - String.onboardingDescription3 + Strings.onboardingDescription3 case 4: - String.onboardingDescription4 - case 5: - String.onboardingDescription5 - case 6: - String.onboardingDescription6 - case 7: - String.onboardingDescription7 - case 8: - String.onboardingDescription8 - case 9: - String.onboardingDescription9 - case 10: - String.onboardingDescription10 - case 11: - String.onboardingDescription11 - case 12: - String.onboardingDescription12 - case 13: - String.onboardingDescription13 + Strings.onboardingDescription4 default: - String.onboardingDescription1 + Strings.onboardingDescription1 } } } diff --git a/NativeAppTemplate/UI/App Root/PermissionsLoadingView.swift b/NativeAppTemplate/UI/App Root/PermissionsLoadingView.swift index 05ae6e1..2804f68 100644 --- a/NativeAppTemplate/UI/App Root/PermissionsLoadingView.swift +++ b/NativeAppTemplate/UI/App Root/PermissionsLoadingView.swift @@ -15,13 +15,13 @@ struct PermissionsLoadingView: View { isShowingLogoutAlert.toggle() } .alert( - String.forceSignOut, + Strings.forceSignOut, isPresented: $isShowingLogoutAlert ) { Button(role: .destructive) { logout() } label: { - Text(String.signOut) + Text(Strings.signOut) } } } diff --git a/NativeAppTemplate/UI/App Root/ResendConfirmationInstructionsView.swift b/NativeAppTemplate/UI/App Root/ResendConfirmationInstructionsView.swift index aff34fb..50a6b15 100644 --- a/NativeAppTemplate/UI/App Root/ResendConfirmationInstructionsView.swift +++ b/NativeAppTemplate/UI/App Root/ResendConfirmationInstructionsView.swift @@ -45,27 +45,27 @@ private extension ResendConfirmationInstructionsView { var resendConfirmationInstructionsView: some View { Form { Section { - TextField(String.placeholderEmail, text: $viewModel.email) + TextField(Strings.placeholderEmail, text: $viewModel.email) .textContentType(.emailAddress) .autocapitalization(.none) } header: { - Text(String.email) + Text(Strings.email) } footer: { if viewModel.isEmailBlank { - Text(String.emailIsRequired) + Text(Strings.emailIsRequired) .foregroundStyle(.validationError) } else if viewModel.isEmailInvalid { - Text(String.emailIsInvalid) + Text(Strings.emailIsInvalid) .foregroundStyle(.validationError) } } - MainButtonView(title: String.buttonSendMeConfirmationInstructions, type: .primary(withArrow: false)) { + MainButtonView(title: Strings.buttonSendMeConfirmationInstructions, type: .primary(withArrow: false)) { viewModel.sendMeConfirmationInstructionsTapped() } .disabled(viewModel.hasInvalidData) .listRowBackground(Color.clear) } - .navigationTitle(String.didntReceiveConfirmationInstructions) + .navigationTitle(Strings.didntReceiveConfirmationInstructions) } } diff --git a/NativeAppTemplate/UI/App Root/ResendConfirmationInstructionsViewModel.swift b/NativeAppTemplate/UI/App Root/ResendConfirmationInstructionsViewModel.swift index 808178b..03f0fe2 100644 --- a/NativeAppTemplate/UI/App Root/ResendConfirmationInstructionsViewModel.swift +++ b/NativeAppTemplate/UI/App Root/ResendConfirmationInstructionsViewModel.swift @@ -25,11 +25,11 @@ final class ResendConfirmationInstructionsViewModel { } var hasInvalidData: Bool { - if Utility.isBlank(email) { + if email.isBlank { return true } - if !Utility.validateEmail(email) { + if !email.isValidEmail { return true } @@ -37,11 +37,11 @@ final class ResendConfirmationInstructionsViewModel { } var isEmailBlank: Bool { - Utility.isBlank(email) + email.isBlank } var isEmailInvalid: Bool { - !Utility.isBlank(email) && !Utility.validateEmail(email) + !email.isBlank && !email.isValidEmail } func sendMeConfirmationInstructionsTapped() { @@ -56,7 +56,7 @@ final class ResendConfirmationInstructionsViewModel { try await signUpRepository.sendConfirmationInstruction(sendConfirmation: sendConfirmation) messageBus.post(message: Message( level: .success, - message: .sentConfirmationInstruction, + message: Strings.sentConfirmationInstruction, autoDismiss: false )) shouldDismiss = true @@ -64,7 +64,7 @@ final class ResendConfirmationInstructionsViewModel { UIApplication.dismissKeyboard() messageBus.post(message: Message( level: .error, - message: String.sentConfirmationInstructionError, + message: Strings.sentConfirmationInstructionError, autoDismiss: false )) } diff --git a/NativeAppTemplate/UI/App Root/SignInEmailAndPasswordView.swift b/NativeAppTemplate/UI/App Root/SignInEmailAndPasswordView.swift index 572b093..5ea70bf 100644 --- a/NativeAppTemplate/UI/App Root/SignInEmailAndPasswordView.swift +++ b/NativeAppTemplate/UI/App Root/SignInEmailAndPasswordView.swift @@ -40,41 +40,41 @@ private extension SignInEmailAndPasswordView { VStack { Form { Section { - TextField(String.placeholderEmail, text: $viewModel.email) + TextField(Strings.placeholderEmail, text: $viewModel.email) .textContentType(.emailAddress) .autocapitalization(.none) .accessibilityIdentifier("SignInEmailAndPasswordView_email_textField") } header: { - Text(String.email) + Text(Strings.email) } footer: { if viewModel.isEmailBlank { - Text(String.emailIsRequired) + Text(Strings.emailIsRequired) .foregroundStyle(.validationError) } else if viewModel.isEmailInvalid { - Text(String.emailIsInvalid) + Text(Strings.emailIsInvalid) .foregroundStyle(.validationError) } } Section { - SecureField(String.placeholderPassword, text: $viewModel.password) + SecureField(Strings.placeholderPassword, text: $viewModel.password) .textContentType(.password) .autocapitalization(.none) .autocorrectionDisabled(true) .accessibilityIdentifier("SignInEmailAndPasswordView_password_secureTextField") } header: { - Text(String.password) + Text(Strings.password) } footer: { if viewModel.isPasswordBlank { - Text(String.passwordIsRequired) + Text(Strings.passwordIsRequired) .foregroundStyle(.validationError) } else if viewModel.hasInvalidDataPassword { - Text(String.passwordIsInvalid) + Text(Strings.passwordIsInvalid) .foregroundStyle(.validationError) } } Section { - MainButtonView(title: String.signIn, type: .primary(withArrow: false)) { + MainButtonView(title: Strings.signIn, type: .primary(withArrow: false)) { viewModel.signIn() } .disabled(viewModel.hasInvalidData) @@ -92,7 +92,7 @@ private extension SignInEmailAndPasswordView { ) ) ) { - Text(String.forgotYourPassword) + Text(Strings.forgotYourPassword) } NavigationLink( @@ -103,10 +103,10 @@ private extension SignInEmailAndPasswordView { ) ) ) { - Text(String.didntReceiveConfirmationInstructions) + Text(Strings.didntReceiveConfirmationInstructions) } } } - .navigationTitle(String.signIn) + .navigationTitle(Strings.signIn) } } diff --git a/NativeAppTemplate/UI/App Root/SignInEmailAndPasswordViewModel.swift b/NativeAppTemplate/UI/App Root/SignInEmailAndPasswordViewModel.swift index 3bb1a40..0cab50a 100644 --- a/NativeAppTemplate/UI/App Root/SignInEmailAndPasswordViewModel.swift +++ b/NativeAppTemplate/UI/App Root/SignInEmailAndPasswordViewModel.swift @@ -25,11 +25,11 @@ final class SignInEmailAndPasswordViewModel { } var hasInvalidData: Bool { - if Utility.isBlank(email) || Utility.isBlank(password) { + if email.isBlank || password.isBlank { return true } - if !Utility.validateEmail(email) { + if !email.isValidEmail { return true } @@ -41,7 +41,7 @@ final class SignInEmailAndPasswordViewModel { } var hasInvalidDataPassword: Bool { - if Utility.isBlank(password) { + if password.isBlank { return true } @@ -49,15 +49,15 @@ final class SignInEmailAndPasswordViewModel { } var isEmailBlank: Bool { - Utility.isBlank(email) + email.isBlank } var isEmailInvalid: Bool { - Utility.isBlank(email) || !Utility.validateEmail(email) + email.isBlank || !email.isValidEmail } var isPasswordBlank: Bool { - Utility.isBlank(password) + password.isBlank } func signIn() { diff --git a/NativeAppTemplate/UI/App Root/SignUpOrSignInView.swift b/NativeAppTemplate/UI/App Root/SignUpOrSignInView.swift index 3d5dbe9..a2c7263 100644 --- a/NativeAppTemplate/UI/App Root/SignUpOrSignInView.swift +++ b/NativeAppTemplate/UI/App Root/SignUpOrSignInView.swift @@ -28,15 +28,15 @@ private extension SignUpOrSignInView { .frame(width: 384, height: 24) .padding() - Image("onboarding1Slim") + Image("hero") .resizable() .aspectRatio(contentMode: .fit) .frame(height: 256) .padding() let agreement = "By signing up or signing in, you agree to the " + - "[\(String.termsOfUse)](\(String.termsOfUseUrl)) " + - "and [\(String.privacyPolicy)](\(String.privacyPolicyUrl))." + "[\(Strings.termsOfUse)](\(Strings.termsOfUseUrl)) " + + "and [\(Strings.privacyPolicy)](\(Strings.privacyPolicyUrl))." Text(.init(agreement)) .padding(.top, NativeAppTemplateConstants.Spacing.sm) .padding(.horizontal, NativeAppTemplateConstants.Spacing.md) @@ -48,7 +48,7 @@ private extension SignUpOrSignInView { messageBus: messageBus ) )) { - MainButtonImageView(title: String.signUpForAnAccount, type: .primary(withArrow: false)) + MainButtonImageView(title: Strings.signUpForAnAccount, type: .primary(withArrow: false)) .padding(.top, NativeAppTemplateConstants.Spacing.xxs) .padding(.horizontal, NativeAppTemplateConstants.Spacing.md) } @@ -62,7 +62,7 @@ private extension SignUpOrSignInView { messageBus: messageBus ) )) { - Text(String.signInToYourAccount) + Text(Strings.signInToYourAccount) .font(.uiLabel) } .padding(.top, NativeAppTemplateConstants.Spacing.xxs) @@ -76,7 +76,7 @@ private extension SignUpOrSignInView { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { - Link(String.supportWebsite, destination: URL(string: String.supportWebsiteUrl)!) + Link(Strings.supportWebsite, destination: URL(string: Strings.supportWebsiteUrl)!) } } .background(Color.backgroundColor) diff --git a/NativeAppTemplate/UI/App Root/SignUpView.swift b/NativeAppTemplate/UI/App Root/SignUpView.swift index 97c667a..f142273 100644 --- a/NativeAppTemplate/UI/App Root/SignUpView.swift +++ b/NativeAppTemplate/UI/App Root/SignUpView.swift @@ -44,69 +44,69 @@ private extension SignUpView { NavigationStack { Form { Section { - TextField(String.placeholderFullName, text: $viewModel.name) + TextField(Strings.placeholderFullName, text: $viewModel.name) } header: { - Text(String.fullName) + Text(Strings.fullName) } footer: { - Text(String.fullNameIsRequired) + Text(Strings.fullNameIsRequired) .font(.caption) .foregroundStyle(viewModel.isNameBlank ? .validationError : .clear) } Section { - TextField(String.placeholderEmail, text: $viewModel.email) + TextField(Strings.placeholderEmail, text: $viewModel.email) .textContentType(.emailAddress) .autocapitalization(.none) } header: { - Text(String.email) + Text(Strings.email) } footer: { if viewModel.isEmailBlank { - Text(String.emailIsRequired) + Text(Strings.emailIsRequired) .foregroundStyle(.validationError) } else if viewModel.hasInvalidDataEmail { - Text(String.emailIsInvalid) + Text(Strings.emailIsInvalid) .foregroundStyle(.validationError) } } - Picker(String.timeZone, selection: $viewModel.selectedTimeZone) { + Picker(Strings.timeZone, selection: $viewModel.selectedTimeZone) { ForEach(timeZones.keys, id: \.self) { key in Text(timeZones[key]!).tag(key) } } Section { - SecureField(String.placeholderPassword, text: $viewModel.password) + SecureField(Strings.placeholderPassword, text: $viewModel.password) .textContentType(.password) .autocapitalization(.none) .autocorrectionDisabled(true) } header: { - Text(String.password) + Text(Strings.password) } footer: { VStack(alignment: .leading) { Text("\(Int.minimumPasswordLength) characters minimum.") if viewModel.isPasswordBlank { - Text(String.passwordIsRequired) + Text(Strings.passwordIsRequired) .foregroundStyle(.validationError) } else if viewModel.hasInvalidDataPassword { - Text(String.passwordIsInvalid) + Text(Strings.passwordIsInvalid) .foregroundStyle(.validationError) } } } Section { - MainButtonView(title: String.signUp, type: .primary(withArrow: false)) { + MainButtonView(title: Strings.signUp, type: .primary(withArrow: false)) { viewModel.createShopkeeper() } .disabled(viewModel.hasInvalidData) .listRowBackground(Color.clear) } } - .navigationTitle(String.signUp) + .navigationTitle(Strings.signUp) } .alert( - String.shopkeeperCreatedError, + Strings.shopkeeperCreatedError, isPresented: $viewModel.isShowingAlert ) {} message: { Text(viewModel.errorMessage) diff --git a/NativeAppTemplate/UI/App Root/SignUpViewModel.swift b/NativeAppTemplate/UI/App Root/SignUpViewModel.swift index bbd21ea..aae2116 100644 --- a/NativeAppTemplate/UI/App Root/SignUpViewModel.swift +++ b/NativeAppTemplate/UI/App Root/SignUpViewModel.swift @@ -31,7 +31,7 @@ final class SignUpViewModel { } var hasInvalidData: Bool { - if Utility.isBlank(name) { + if name.isBlank { return true } @@ -47,11 +47,11 @@ final class SignUpViewModel { } var hasInvalidDataEmail: Bool { - if Utility.isBlank(email) { + if email.isBlank { return true } - if !Utility.validateEmail(email) { + if !email.isValidEmail { return true } @@ -59,7 +59,7 @@ final class SignUpViewModel { } var hasInvalidDataPassword: Bool { - if Utility.isBlank(password) { + if password.isBlank { return true } @@ -71,15 +71,15 @@ final class SignUpViewModel { } var isNameBlank: Bool { - Utility.isBlank(name) + name.isBlank } var isEmailBlank: Bool { - Utility.isBlank(email) + email.isBlank } var isPasswordBlank: Bool { - Utility.isBlank(password) + password.isBlank } func createShopkeeper() { @@ -102,7 +102,7 @@ final class SignUpViewModel { messageBus.post(message: Message( level: .success, - message: String.signedUpButUnconfirmed, + message: Strings.signedUpButUnconfirmed, autoDismiss: false )) shouldDismiss = true diff --git a/NativeAppTemplate/UI/Empty States/LoadingView.swift b/NativeAppTemplate/UI/Empty States/LoadingView.swift index 1714bb4..904e746 100644 --- a/NativeAppTemplate/UI/Empty States/LoadingView.swift +++ b/NativeAppTemplate/UI/Empty States/LoadingView.swift @@ -11,7 +11,7 @@ struct LoadingView: View { VStack { ProgressView().scaleEffect(1.0, anchor: .center) .padding([.bottom], NativeAppTemplateConstants.Spacing.xs) - Text(String.loading) + Text(Strings.loading) .font(.uiHeadline) } } diff --git a/NativeAppTemplate/UI/Empty States/NeedAppUpdatesView.swift b/NativeAppTemplate/UI/Empty States/NeedAppUpdatesView.swift index 33ebfd1..43fe40e 100644 --- a/NativeAppTemplate/UI/Empty States/NeedAppUpdatesView.swift +++ b/NativeAppTemplate/UI/Empty States/NeedAppUpdatesView.swift @@ -20,17 +20,17 @@ struct NeedAppUpdatesView: View { .frame(width: NativeAppTemplateConstants.Spacing.xxxl) .foregroundStyle(.titleText) .padding() - Text(String.updateApp) + Text(Strings.updateApp) .font(.uiTitle1) .foregroundStyle(.titleText) .padding(.top) - Text(String.installNewVersionApp) + Text(Strings.installNewVersionApp) .foregroundStyle(.contentText) .padding(.top, NativeAppTemplateConstants.Spacing.xxxs) Button { - openURL(URL(string: String.appStoreUrl)!) + openURL(URL(string: Strings.appStoreUrl)!) } label: { - Text(String.updateApp) + Text(Strings.updateApp) } .padding(.top) } diff --git a/NativeAppTemplate/UI/Empty States/OfflineView.swift b/NativeAppTemplate/UI/Empty States/OfflineView.swift index 203c955..9b9b742 100644 --- a/NativeAppTemplate/UI/Empty States/OfflineView.swift +++ b/NativeAppTemplate/UI/Empty States/OfflineView.swift @@ -19,13 +19,13 @@ struct OfflineView: View { .padding() .foregroundStyle(.titleText) - Text(String.noConnection) + Text(Strings.noConnection) .font(.uiTitle1) .foregroundStyle(.titleText) .multilineTextAlignment(.center) .padding(.top) - Text(String.checkInternetConnection) + Text(Strings.checkInternetConnection) .font(.uiLabel) .lineSpacing(NativeAppTemplateConstants.Spacing.xxs) .foregroundStyle(.contentText) diff --git a/NativeAppTemplate/UI/Scan/CompleteScanResultView.swift b/NativeAppTemplate/UI/Scan/CompleteScanResultView.swift deleted file mode 100644 index e8e5986..0000000 --- a/NativeAppTemplate/UI/Scan/CompleteScanResultView.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// CompleteScanResultView.swift -// NativeAppTemplate -// - -import SwiftUI - -struct CompleteScanResultView: View { - @Environment(\.dismiss) private var dismiss - @Environment(MessageBus.self) private var messageBus - var completeScanResult: CompleteScanResult - - var body: some View { - contentView - } -} - -// MARK: - private - -private extension CompleteScanResultView { - var contentView: some View { - @ViewBuilder var contentView: some View { - switch completeScanResult.type { - case .completed, .reset: - succeededView - case .failed: - failedView - case .idled: - idledView - } - } - - return contentView - } - - @ViewBuilder var succeededView: some View { - if let itemTag = completeScanResult.itemTag { - GroupBox(label: Label(String("Result"), systemImage: "checkmark.circle")) { - Text(String(itemTag.queueNumber)) - .font(.uiTitle1) - - if itemTag.state == .completed { - CompletedTag() - } else { - IdlingTag() - } - - if completeScanResult.type == .reset { - Text(completeScanResult.type.displayString) - } - - HStack(alignment: .firstTextBaseline) { - Text(completeScanResult.scannedAt.cardTimeAgoInWordsDateString) - .font(.uiBodyCustom) - .foregroundStyle(.successSecondaryForeground) - Text(verbatim: "complete scanned") - .font(.uiFootnote) - .foregroundStyle(.successSecondaryForeground) - } - .padding(.top, NativeAppTemplateConstants.Spacing.xxs) - } - .backgroundStyle(.ultraThinMaterial) - } - } - - var failedView: some View { - GroupBox(label: Label(String("Error"), systemImage: "exclamationmark.triangle")) { - Text(completeScanResult.message) - .padding(.top, NativeAppTemplateConstants.Spacing.xxs) - } - .foregroundStyle(.validationError) - .backgroundStyle(.ultraThinMaterial) - } - - var idledView: some View { - GroupBox(label: Label(String("Result"), systemImage: "checkmark.circle")) {} - .backgroundStyle(.ultraThinMaterial) - } -} diff --git a/NativeAppTemplate/UI/Scan/ScanView.swift b/NativeAppTemplate/UI/Scan/ScanView.swift deleted file mode 100644 index 3425568..0000000 --- a/NativeAppTemplate/UI/Scan/ScanView.swift +++ /dev/null @@ -1,157 +0,0 @@ -// -// ScanView.swift -// NativeAppTemplate -// - -import CoreNFC -import SwiftUI - -enum ScanType: String { - case completeScan - case test - - var displayString: String { - switch self { - case .completeScan: - "Complete Scan" - case .test: - "Test" - } - } -} - -// MARK: - CaseIterable - -extension ScanType: CaseIterable { - var index: Self.AllCases.Index { - get { - Self.allCases.firstIndex(of: self)! - } - set { - self = Self.allCases[newValue] - } - } - - var count: Int { - Self.allCases.count - } -} - -// MARK: - Identifiable - -extension ScanType: Identifiable { - var id: Self { - self - } -} - -struct ScanView: View { - @Environment(\.sessionController) private var sessionController: SessionControllerProtocol - @StateObject private var nfcManager = appSingletons.nfcManager - @State private var viewModel: ScanViewModel - - init(viewModel: ScanViewModel) { - _viewModel = State(initialValue: viewModel) - } - - var body: some View { - contentView - .onChange(of: sessionController.didBackgroundTagReading) { - viewModel.handleBackgroundTagReading() - } - } -} - -// MARK: - private - -private extension ScanView { - var contentView: some View { - @ViewBuilder var contentView: some View { - if viewModel.isBusy { - LoadingView() - } else { - scanView - .onChange(of: nfcManager.isScanResultChanged) { - viewModel.handleScanResultChanged() - } - .onChange(of: nfcManager.isScanResultChangedForTesting) { - viewModel.handleScanResultChangedForTesting() - } - } - } - - return contentView - } - - var scanView: some View { - ScrollView { - VStack(spacing: NativeAppTemplateConstants.Spacing.xxl) { - switch viewModel.scanType { - case .completeScan: - if !viewModel.isShowingResetConfirmationDialog { - GroupBox(label: Label(String.completeScan, systemImage: "flag.checkered")) { - MainButtonView(title: String.scan, type: .coloredPrimary(withArrow: false)) { - viewModel.startCompleteScan() - } - .padding() - - Text(String.completeScanHelp) - .font(.uiFootnote) - .foregroundStyle(.contentText) - } - .foregroundStyle(.accent) - .backgroundStyle(.ultraThinMaterial) - } - - CompleteScanResultView( - completeScanResult: sessionController.completeScanResult - ) - case .test: - GroupBox(label: Label(String.showTagInfoScan, systemImage: "info.circle")) { - MainButtonView(title: String.scan, type: .coloredSecondary(withArrow: false)) { - viewModel.startTestScan() - } - .padding() - - Text(String.showTagInfoScanHelp) - .font(.uiFootnote) - .foregroundStyle(.contentText) - } - .foregroundStyle(.contentText) - .backgroundStyle(.ultraThinMaterial) - - ShowTagInfoScanResultView( - showTagInfoScanResult: sessionController.showTagInfoScanResult - ) - } - - Spacer() - } - } - .toolbar { - ToolbarItem(placement: .principal) { - Picker(String("ScanType"), selection: $viewModel.scanType) { - Text(String.completeScan).tag(ScanType.completeScan) - Text(String.showTagInfoScan).tag(ScanType.test) - } - .pickerStyle(SegmentedPickerStyle()) - } - } - .padding() - .alert( - String.itemTagAlreadyCompleted, - isPresented: $viewModel.isShowingResetConfirmationDialog - ) { - Button(String.reset, role: .destructive) { - viewModel.resetTag() - } - Button(String.cancel, role: .cancel) { - viewModel.dismissResetConfirmationDialog() - } - } message: { - Text(String.areYouSure) - } - .accessibility(identifier: "scanView") - .scrollContentBackground(.hidden) - } -} diff --git a/NativeAppTemplate/UI/Scan/ScanViewModel.swift b/NativeAppTemplate/UI/Scan/ScanViewModel.swift deleted file mode 100644 index 9e94119..0000000 --- a/NativeAppTemplate/UI/Scan/ScanViewModel.swift +++ /dev/null @@ -1,194 +0,0 @@ -// -// ScanViewModel.swift -// NativeAppTemplate -// - -import CoreNFC -import Observation -import SwiftUI - -@Observable -@MainActor -final class ScanViewModel { - var scanType: ScanType = .completeScan - var isShowingResetConfirmationDialog = false - var isFetching = false - var isResetting = false - - private let itemTagRepository: ItemTagRepositoryProtocol - private let sessionController: SessionControllerProtocol - private let messageBus: MessageBus - private let nfcManager: NFCManagerProtocol - - init( - itemTagRepository: ItemTagRepositoryProtocol, - sessionController: SessionControllerProtocol, - messageBus: MessageBus, - nfcManager: NFCManagerProtocol - ) { - self.itemTagRepository = itemTagRepository - self.sessionController = sessionController - self.messageBus = messageBus - self.nfcManager = nfcManager - } - - var isBusy: Bool { - isFetching || isResetting - } - - func handleBackgroundTagReading() { - if sessionController.didBackgroundTagReading { - sessionController.didBackgroundTagReading = false - scanType = .completeScan - } - } - - func handleScanResultChanged() { - guard nfcManager.isScanResultChanged else { return } - guard nfcManager.scanResult != nil else { return } - - switch nfcManager.scanResult { - case let .success(itemTagData): - completeTag(itemTagId: itemTagData.itemTagId) - case let .failure(error): - sessionController.completeScanResult = CompleteScanResult( - type: .failed, - message: error.codedDescription - ) - default: - break - } - } - - func handleScanResultChangedForTesting() { - guard nfcManager.isScanResultChangedForTesting else { return } - guard nfcManager.scanResult != nil else { return } - - switch nfcManager.scanResult { - case let .success(itemTagData): - fetchItemTagDetail(itemTagData: itemTagData) - case let .failure(error): - sessionController.showTagInfoScanResult = ShowTagInfoScanResult( - type: .failed, - message: error.codedDescription - ) - default: - break - } - } - - func startCompleteScan() { - guard NFCNDEFReaderSession.readingAvailable else { - messageBus.post( - message: Message( - level: .error, - message: String.thisDeviceDoesNotSupportTagScanning, - autoDismiss: false - ) - ) - return - } - - sessionController.completeScanResult = CompleteScanResult() - - Task { - await nfcManager.startReading() - } - } - - func startTestScan() { - guard NFCNDEFReaderSession.readingAvailable else { - messageBus.post( - message: Message( - level: .error, - message: String.thisDeviceDoesNotSupportTagScanning, - autoDismiss: false - ) - ) - return - } - - sessionController.showTagInfoScanResult = ShowTagInfoScanResult() - - Task { - await nfcManager.startReadingForTesting() - } - } - - func resetTag() { - guard let itemTagId = sessionController.completeScanResult.itemTag?.id else { return } - resetTag(itemTagId: itemTagId) - } - - func dismissResetConfirmationDialog() { - isShowingResetConfirmationDialog = false - } - - private func completeTag(itemTagId: String) { - Task { - do { - let itemTag = try await itemTagRepository.complete(id: itemTagId) - - sessionController.completeScanResult = CompleteScanResult( - itemTag: itemTag, - type: .completed - ) - - if itemTag.alreadyCompleted! { - isShowingResetConfirmationDialog = true - } - } catch { - sessionController.completeScanResult = CompleteScanResult( - type: .failed, - message: error.codedDescription - ) - } - } - } - - private func resetTag(itemTagId: String) { - Task { - isResetting = true - - do { - let itemTag = try await itemTagRepository.reset(id: itemTagId) - sessionController.completeScanResult = CompleteScanResult( - itemTag: itemTag, - type: .reset - ) - } catch { - sessionController.completeScanResult = CompleteScanResult( - type: .failed, - message: error.codedDescription - ) - } - - isResetting = false - } - } - - private func fetchItemTagDetail(itemTagData: ItemTagData) { - Task { - isFetching = true - - do { - let itemTag = try await itemTagRepository.fetchDetail(id: itemTagData.itemTagId) - - sessionController.showTagInfoScanResult = ShowTagInfoScanResult( - itemTag: itemTag, - itemTagType: itemTagData.itemTagType, - isReadOnly: itemTagData.isReadOnly, - type: .succeeded, - scannedAt: itemTagData.scannedAt - ) - } catch { - sessionController.showTagInfoScanResult = ShowTagInfoScanResult( - type: .failed, - message: error.codedDescription - ) - } - - isFetching = false - } - } -} diff --git a/NativeAppTemplate/UI/Scan/ShowTagInfoScanResultView.swift b/NativeAppTemplate/UI/Scan/ShowTagInfoScanResultView.swift deleted file mode 100644 index 6d0ce64..0000000 --- a/NativeAppTemplate/UI/Scan/ShowTagInfoScanResultView.swift +++ /dev/null @@ -1,168 +0,0 @@ -// -// ShowTagInfoScanResultView.swift -// NativeAppTemplate -// - -import SwiftUI - -struct ShowTagInfoScanResultView: View { - @Environment(\.dismiss) private var dismiss - @Environment(MessageBus.self) private var messageBus - var showTagInfoScanResult: ShowTagInfoScanResult - - var body: some View { - contentView - } -} - -// MARK: - private - -private extension ShowTagInfoScanResultView { - var contentView: some View { - @ViewBuilder var contentView: some View { - switch showTagInfoScanResult.type { - case .succeeded: - succeededView - case .failed: - failedView - case .idled: - idledView - } - } - - return contentView - } - - var succeededView: some View { - GroupBox(label: Label(String.tagInfo, systemImage: "rectangle")) { - VStack { - if let itemTag = showTagInfoScanResult.itemTag { - let scannedAt = showTagInfoScanResult.scannedAt - let itemTagType = showTagInfoScanResult.itemTagType - let isReadOnly = showTagInfoScanResult.isReadOnly - let displayReadOnly = isReadOnly ? String.readOnly : String.writable - - let imageSize = 10.0 - - Text(String(itemTag.queueNumber)) - .font(.uiTitle1) - .foregroundStyle(itemTagType == .server ? .serverForeground : .accent) - HStack(alignment: .firstTextBaseline) { - Text(String(scannedAt.cardTimeAgoInWordsDateString)) - .font(.uiBodyCustom) - .foregroundStyle(.secondaryText) - Text(verbatim: "show tag info scanned") - .font(.uiFootnote) - .foregroundStyle(.secondaryText) - } - - Grid( - alignment: .leadingFirstTextBaseline, - horizontalSpacing: NativeAppTemplateConstants.Spacing.xs, - verticalSpacing: NativeAppTemplateConstants.Spacing.xxs - ) { - GridRow { - Image(systemName: "storefront") - .frame(width: imageSize, height: imageSize) - .foregroundStyle(.secondaryText) - Text(itemTag.shopName) - .font(.uiLabelBold) - Text(" ") - .font(.uiFootnote) - .foregroundStyle(.secondaryText) - } - GridRow { - Image(systemName: "info.circle") - .frame(width: imageSize, height: imageSize) - .foregroundStyle(.secondaryText) - Text(itemTagType.displayString) - .font(.uiLabelBold) - .foregroundStyle(itemTagType == .server ? .serverForeground : .accent) - Text(verbatim: "tag type") - .font(.uiFootnote) - .foregroundStyle(.secondaryText) - } - GridRow { - Image(systemName: "flag.checkered") - .frame(width: imageSize, height: imageSize) - .foregroundStyle(.secondaryText) - if itemTag.state == .completed { - CompletedTag() - } else { - IdlingTag() - } - Text(verbatim: "tag status") - .font(.uiFootnote) - .foregroundStyle(.secondaryText) - } - - if itemTag.scanState == ScanState.scanned, itemTag.customerReadAt != nil { - GridRow { - Image(systemName: "person.2") - .frame(width: imageSize, height: imageSize) - .foregroundStyle(.secondaryText) - Text(itemTag.customerReadAt!.cardTimeString) - .font(.uiLabelBold) - Text(verbatim: "scanned by a customer") - .font(.uiFootnote) - .foregroundStyle(.secondaryText) - } - } - - if itemTag.state == ItemTagState.completed, itemTag.completedAt != nil { - GridRow { - Image(systemName: "flag.checkered.circle") - .frame(width: imageSize, height: imageSize) - .foregroundStyle(.secondaryText) - Text(itemTag.completedAt!.cardTimeString) - .font(.uiLabelBold) - Text(verbatim: "completed") - .font(.uiFootnote) - .foregroundStyle(.secondaryText) - } - } - - GridRow { - Image(systemName: "rectangle") - .frame(width: imageSize, height: imageSize) - .foregroundStyle(.secondaryText) - Text(displayReadOnly) - .font(.uiLabelBold) - Text(verbatim: "NFC tag") - .font(.uiFootnote) - .foregroundStyle(.secondaryText) - } - GridRow { - Image(systemName: "clock") - .frame(width: imageSize, height: imageSize) - .foregroundStyle(.secondaryText) - Text(itemTag.createdAt.cardDateString) - .font(.uiLabelBold) - Text(verbatim: "created") - .font(.uiFootnote) - .foregroundStyle(.secondaryText) - } - } - } - } - } - .foregroundStyle(.contentText) - .backgroundStyle(.ultraThinMaterial) - .dynamicTypeSize(...DynamicTypeSize.accessibility1) - } - - var failedView: some View { - GroupBox(label: Label(String("Error"), systemImage: "exclamationmark.triangle")) { - Text(showTagInfoScanResult.message) - .padding(.top, NativeAppTemplateConstants.Spacing.xxs) - } - .foregroundStyle(.validationError) - .backgroundStyle(.ultraThinMaterial) - } - - var idledView: some View { - GroupBox(label: Label(String.tagInfo, systemImage: "rectangle")) {} - .foregroundStyle(.contentText) - .backgroundStyle(.ultraThinMaterial) - } -} diff --git a/NativeAppTemplate/UI/Settings/PasswordEditView.swift b/NativeAppTemplate/UI/Settings/PasswordEditView.swift index c912673..49ed3b6 100644 --- a/NativeAppTemplate/UI/Settings/PasswordEditView.swift +++ b/NativeAppTemplate/UI/Settings/PasswordEditView.swift @@ -41,64 +41,64 @@ private extension PasswordEditView { var passwordEditView: some View { Form { Section { - SecureField(String.currentPassword, text: $viewModel.currentPassword) + SecureField(Strings.currentPassword, text: $viewModel.currentPassword) .textContentType(.password) .autocapitalization(.none) .autocorrectionDisabled(true) } header: { - Text(String.currentPassword) + Text(Strings.currentPassword) } footer: { VStack(alignment: .leading) { - Text(String.weNeedYourCurrentPassword) + Text(Strings.weNeedYourCurrentPassword) .font(.uiFootnote) - Text(String.currentPasswordIsRequired) - .foregroundStyle(Utility.isBlank(viewModel.currentPassword) ? .validationError : .clear) + Text(Strings.currentPasswordIsRequired) + .foregroundStyle(viewModel.currentPassword.isBlank ? .validationError : .clear) .font(.uiFootnote) } } Section { - SecureField(String.newPassword, text: $viewModel.password) + SecureField(Strings.newPassword, text: $viewModel.password) .textContentType(.password) .autocapitalization(.none) .autocorrectionDisabled(true) } header: { - Text(String.newPassword) + Text(Strings.newPassword) } footer: { VStack(alignment: .leading) { Text("\(viewModel.minimumPasswordLength) characters minimum.") .font(.uiFootnote) - if Utility.isBlank(viewModel.password) { - Text(String.newPasswordIsRequired) + if viewModel.password.isBlank { + Text(Strings.newPasswordIsRequired) .foregroundStyle(.validationError) .font(.uiFootnote) } else if viewModel.hasInvalidDataPassword { - Text(String.passwordIsInvalid) + Text(Strings.passwordIsInvalid) .foregroundStyle(.validationError) .font(.uiFootnote) } } } Section { - SecureField(String.confirmNewPassword, text: $viewModel.passwordConfirmation) + SecureField(Strings.confirmNewPassword, text: $viewModel.passwordConfirmation) .textContentType(.password) .autocapitalization(.none) .autocorrectionDisabled(true) } header: { - Text(String.confirmNewPassword) + Text(Strings.confirmNewPassword) } footer: { - Text(String.confirmNewPasswordIsRequired) + Text(Strings.confirmNewPasswordIsRequired) .font(.uiFootnote) - .foregroundStyle(Utility.isBlank(viewModel.passwordConfirmation) ? .validationError : .clear) + .foregroundStyle(viewModel.passwordConfirmation.isBlank ? .validationError : .clear) } } - .navigationTitle(String.updatePassword) + .navigationTitle(Strings.updatePassword) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button { viewModel.updatePassword() } label: { - Text(String.save) + Text(Strings.save) } .disabled(viewModel.hasInvalidData) } diff --git a/NativeAppTemplate/UI/Settings/PasswordEditViewModel.swift b/NativeAppTemplate/UI/Settings/PasswordEditViewModel.swift index 8bd96eb..1299bbc 100644 --- a/NativeAppTemplate/UI/Settings/PasswordEditViewModel.swift +++ b/NativeAppTemplate/UI/Settings/PasswordEditViewModel.swift @@ -31,9 +31,9 @@ final class PasswordEditViewModel { } var hasInvalidData: Bool { - if Utility.isBlank(currentPassword) || - Utility.isBlank(password) || - Utility.isBlank(passwordConfirmation) { + if currentPassword.isBlank || + password.isBlank || + passwordConfirmation.isBlank { return true } @@ -45,7 +45,7 @@ final class PasswordEditViewModel { } var hasInvalidDataPassword: Bool { - if Utility.isBlank(password) { + if password.isBlank { return true } @@ -77,7 +77,7 @@ final class PasswordEditViewModel { ) try await accountPasswordRepository.update(updatePassword: updatePassword) - messageBus.post(message: Message(level: .success, message: .passwordUpdated)) + messageBus.post(message: Message(level: .success, message: Strings.passwordUpdated)) shouldDismiss = true } catch { messageBus.post(message: Message(error: error)) diff --git a/NativeAppTemplate/UI/Settings/SettingsView.swift b/NativeAppTemplate/UI/Settings/SettingsView.swift index 7274581..59500d8 100644 --- a/NativeAppTemplate/UI/Settings/SettingsView.swift +++ b/NativeAppTemplate/UI/Settings/SettingsView.swift @@ -22,7 +22,7 @@ struct SettingsView: View { var body: some View { VStack(spacing: 0) { List { - Section(header: Text(String.myAccount)) { + Section(header: Text(Strings.myAccount)) { if let shopkeeper = viewModel.shopkeeper { NavigationLink( destination: ShopkeeperEditView( @@ -35,7 +35,7 @@ struct SettingsView: View { ) ) ) { - Label(String.profile, systemImage: "person") + Label(Strings.profile, systemImage: "person") } } @@ -47,41 +47,37 @@ struct SettingsView: View { ) ) ) { - Label(String.password, systemImage: "key") + Label(Strings.password, systemImage: "key") } } .listRowBackground(Color.cardBackground.opacity(0.7)) Section(header: Text(verbatim: "Support")) { - Link(destination: URL(string: String.howToUseUrl)!) { - Label(String.howToUse, systemImage: "info") - } - - Link(destination: URL(string: String.faqsUrl)!) { - Label(String.faqs, systemImage: "questionmark") + Link(destination: URL(string: Strings.faqsUrl)!) { + Label(Strings.faqs, systemImage: "questionmark") } Link(destination: supportEmailURL) { - Label(String.contact, systemImage: "envelope") + Label(Strings.contact, systemImage: "envelope") } Button { requestReview() } label: { - Label(String.rateApp, systemImage: "hand.thumbsup") + Label(Strings.rateApp, systemImage: "hand.thumbsup") } } .listRowBackground(Color.cardBackground.opacity(0.7)) Section(header: Text(verbatim: "About")) { - Link(destination: URL(string: String.supportWebsiteUrl)!) { + Link(destination: URL(string: Strings.supportWebsiteUrl)!) { Label("Website", systemImage: "globe") } - Link(destination: URL(string: String.privacyPolicyUrl)!) { - Label(String.privacyPolicy, systemImage: "hand.raised") + Link(destination: URL(string: Strings.privacyPolicyUrl)!) { + Label(Strings.privacyPolicy, systemImage: "hand.raised") } - Link(destination: URL(string: String.termsOfUseUrl)!) { - Label(String.termsOfUse, systemImage: "doc.text") + Link(destination: URL(string: Strings.termsOfUseUrl)!) { + Label(Strings.termsOfUse, systemImage: "doc.text") } } .listRowBackground(Color.cardBackground.opacity(0.7)) @@ -117,7 +113,7 @@ struct SettingsView: View { #endif } } - .navigationTitle(String.settings) + .navigationTitle(Strings.settings) .navigationBarTitleDisplayMode(.inline) } @@ -146,6 +142,6 @@ struct SettingsView: View { """ let encodedBody = body.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" - return URL(string: "mailto:\(String.supportMail)?body=\(encodedBody)")! + return URL(string: "mailto:\(Strings.supportMail)?body=\(encodedBody)")! } } diff --git a/NativeAppTemplate/UI/Settings/SettingsViewModel.swift b/NativeAppTemplate/UI/Settings/SettingsViewModel.swift index 731311c..70534ab 100644 --- a/NativeAppTemplate/UI/Settings/SettingsViewModel.swift +++ b/NativeAppTemplate/UI/Settings/SettingsViewModel.swift @@ -40,13 +40,13 @@ final class SettingsViewModel { do { try await sessionController.logout() #if DEBUG - messageBus.post(message: Message(level: .success, message: .signedOut)) + messageBus.post(message: Message(level: .success, message: Strings.signedOut)) #endif } catch { #if DEBUG messageBus.post(message: Message( level: .error, - message: "\(String.signedOutError) \(error.codedDescription)", + message: "\(Strings.signedOutError) \(error.codedDescription)", autoDismiss: false )) #endif diff --git a/NativeAppTemplate/UI/Settings/ShopkeeperEditView.swift b/NativeAppTemplate/UI/Settings/ShopkeeperEditView.swift index 7aadfae..b339a4c 100644 --- a/NativeAppTemplate/UI/Settings/ShopkeeperEditView.swift +++ b/NativeAppTemplate/UI/Settings/ShopkeeperEditView.swift @@ -42,32 +42,32 @@ private extension ShopkeeperEditView { var shopkeeperEditView: some View { Form { Section { - TextField(String.placeholderFullName, text: $viewModel.name) + TextField(Strings.placeholderFullName, text: $viewModel.name) } header: { - Text(String.fullName) + Text(Strings.fullName) } footer: { - Text(String.fullNameIsRequired) - .foregroundStyle(Utility.isBlank(viewModel.name) ? .validationError : .clear) + Text(Strings.fullNameIsRequired) + .foregroundStyle(viewModel.name.isBlank ? .validationError : .clear) } Section { - TextField(String.placeholderEmail, text: $viewModel.email) + TextField(Strings.placeholderEmail, text: $viewModel.email) .textContentType(.emailAddress) .autocapitalization(.none) } header: { - Text(String.email) + Text(Strings.email) } footer: { - if Utility.isBlank(viewModel.email) { - Text(String.emailIsRequired) + if viewModel.email.isBlank { + Text(Strings.emailIsRequired) .foregroundStyle(.validationError) } else if viewModel.hasInvalidDataEmail { - Text(String.emailIsInvalid) + Text(Strings.emailIsInvalid) .foregroundStyle(.validationError) } } Section { - Picker(String.timeZone, selection: $viewModel.selectedTimeZone) { + Picker(Strings.timeZone, selection: $viewModel.selectedTimeZone) { ForEach(timeZones.keys, id: \.self) { key in Text(timeZones[key]!).tag(key) } @@ -78,33 +78,33 @@ private extension ShopkeeperEditView { .listRowBackground(Color.clear) Section { - MainButtonView(title: String.deleteMyAccount, type: .destructive(withArrow: false)) { + MainButtonView(title: Strings.deleteMyAccount, type: .destructive(withArrow: false)) { viewModel.isShowingDeleteConfirmationDialog = true } .listRowBackground(Color.clear) } } .alert( - String.deleteMyAccount, + Strings.deleteMyAccount, isPresented: $viewModel.isShowingDeleteConfirmationDialog ) { - Button(String.deleteMyAccount, role: .destructive) { + Button(Strings.deleteMyAccount, role: .destructive) { viewModel.destroyShopkeeper() } - Button(String.cancel, role: .cancel) { + Button(Strings.cancel, role: .cancel) { viewModel.isShowingDeleteConfirmationDialog = false } } message: { - Text(String.areYouSure) + Text(Strings.areYouSure) } - .navigationTitle(String.editProfile) + .navigationTitle(Strings.editProfile) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button { viewModel.updateShopkeeper() } label: { - Text(String.save) + Text(Strings.save) } .disabled(viewModel.hasInvalidData) } diff --git a/NativeAppTemplate/UI/Settings/ShopkeeperEditViewModel.swift b/NativeAppTemplate/UI/Settings/ShopkeeperEditViewModel.swift index 7243876..fd0f361 100644 --- a/NativeAppTemplate/UI/Settings/ShopkeeperEditViewModel.swift +++ b/NativeAppTemplate/UI/Settings/ShopkeeperEditViewModel.swift @@ -45,7 +45,7 @@ final class ShopkeeperEditViewModel { } var hasInvalidData: Bool { - if Utility.isBlank(name) { + if name.isBlank { return true } @@ -63,11 +63,11 @@ final class ShopkeeperEditViewModel { } var hasInvalidDataEmail: Bool { - if Utility.isBlank(email) { + if email.isBlank { return true } - if !Utility.validateEmail(email) { + if !email.isValidEmail { return true } @@ -107,12 +107,12 @@ final class ShopkeeperEditViewModel { if emailUpdated { messageBus.post(message: Message( level: .success, - message: .reconfirmDescription, + message: Strings.reconfirmDescription, autoDismiss: false )) try await sessionController.logout() } else { - messageBus.post(message: Message(level: .success, message: .shopkeeperUpdated)) + messageBus.post(message: Message(level: .success, message: Strings.shopkeeperUpdated)) } shouldDismiss = true @@ -130,11 +130,11 @@ final class ShopkeeperEditViewModel { do { try await signUpRepository.destroy(networkClient: sessionController.client) - messageBus.post(message: Message(level: .success, message: .shopkeeperDeleted)) + messageBus.post(message: Message(level: .success, message: Strings.shopkeeperDeleted)) } catch { messageBus.post(message: Message( level: .error, - message: "\(String.shopkeeperDeletedError) \(error.codedDescription)", + message: "\(Strings.shopkeeperDeletedError) \(error.codedDescription)", autoDismiss: false )) } @@ -144,7 +144,7 @@ final class ShopkeeperEditViewModel { } catch { messageBus.post(message: Message( level: .error, - message: "\(String.shopkeeperDeletedError) \(error.codedDescription)", + message: "\(Strings.shopkeeperDeletedError) \(error.codedDescription)", autoDismiss: false )) } diff --git a/NativeAppTemplate/UI/Shared/MainButtonView.swift b/NativeAppTemplate/UI/Shared/MainButtonView.swift index 3e986ac..b1a9696 100644 --- a/NativeAppTemplate/UI/Shared/MainButtonView.swift +++ b/NativeAppTemplate/UI/Shared/MainButtonView.swift @@ -73,11 +73,6 @@ struct MainButtonView: View { .font(.uiButtonLabelLarge) .foregroundStyle(type.color) .padding(NativeAppTemplateConstants.Spacing.sm) - // If commenting out below and select max large font size on settings accessibility, you will - // not be enable to tap Scan button on Scan tab. -// .background(GeometryReader { proxy in -// Color.clear.preference(key: SizeKey.self, value: proxy.size) -// }) Spacer() } diff --git a/NativeAppTemplate/UI/Shared/Tags/CustomerScannedTag.swift b/NativeAppTemplate/UI/Shared/Tags/CustomerScannedTag.swift deleted file mode 100644 index 0b3a146..0000000 --- a/NativeAppTemplate/UI/Shared/Tags/CustomerScannedTag.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// CustomerScannedTag.swift -// NativeAppTemplate -// - -import SwiftUI - -struct CustomerScannedTag: View { - var body: some View { - TagView( - text: "customer scanned", - textColor: .customerScannedTagForeground, - backgroundColor: .customerScannedTagBackground, - borderColor: .customerScannedTagBorder - ) - } -} - -struct CustomerScannedTag_Previews: PreviewProvider { - static var previews: some View { - VStack(spacing: NativeAppTemplateConstants.Spacing.xs) { - customerScannedTag.colorScheme(.light) - customerScannedTag.colorScheme(.dark) - } - } - - static var customerScannedTag: some View { - CustomerScannedTag() - .padding() - .background(Color.backgroundColor) - } -} diff --git a/NativeAppTemplate/UI/Shop Detail/ShopDetailCardView.swift b/NativeAppTemplate/UI/Shop Detail/ShopDetailCardView.swift index 4abbd36..6431519 100644 --- a/NativeAppTemplate/UI/Shop Detail/ShopDetailCardView.swift +++ b/NativeAppTemplate/UI/Shop Detail/ShopDetailCardView.swift @@ -14,31 +14,17 @@ struct ShopDetailCardView: View { var content: some View { HStack { - Text(String(itemTag.queueNumber)) + Text(String(itemTag.name)) .font(.uiTitle4) Spacer() - VStack(alignment: .trailing) { - if itemTag.scanState == ScanState.scanned { - CustomerScannedTag() - - if let customerReadAt = itemTag.customerReadAt { - Text(customerReadAt.cardTimeString) - .font(.uiFootnote) - .foregroundStyle(.contentText) - } - } - } - - Spacer() - VStack(alignment: .trailing) { if itemTag.state == .completed { CompletedTag() if let completedAt = itemTag.completedAt { - Text(completedAt.cardTimeString) + Text(completedAt.cardDateTimeString) .font(.uiFootnote) .foregroundStyle(.contentText) } diff --git a/NativeAppTemplate/UI/Shop Detail/ShopDetailView.swift b/NativeAppTemplate/UI/Shop Detail/ShopDetailView.swift index 7bbd3d0..f318dc8 100644 --- a/NativeAppTemplate/UI/Shop Detail/ShopDetailView.swift +++ b/NativeAppTemplate/UI/Shop Detail/ShopDetailView.swift @@ -4,21 +4,6 @@ // import SwiftUI -import TipKit - -struct ReadInstructionsTip: Tip { - var title: Text { - Text(String.readInstructions) - } - - var message: Text? { - Text(String.haveFun) - } - - var image: Image? { - Image(systemName: "info.circle") - } -} struct ShopDetailView: View { @Environment(\.dismiss) private var dismiss @@ -66,45 +51,10 @@ private extension ShopDetailView { } func header(shop: Shop) -> some View { - ScrollView(.horizontal) { - VStack(alignment: .leading, spacing: 0) { - let tip = ReadInstructionsTip() - TipView(tip, arrowEdge: .bottom) - .tint(.alarm) - - Text("\(String.instructions):") - .foregroundStyle(.contentText) - HStack(alignment: .firstTextBaseline) { - Text(verbatim: "1.") - .font(.uiCaption) - .foregroundStyle(.contentText) - HStack { - let openServerNumberTagsWebpage = - "\(String.open) [\(String.serverNumberTagsWebpage)](\(shop.displayShopServerUrl))." - Text(.init(openServerNumberTagsWebpage)) - .font(.uiCaption) - .foregroundStyle(.contentText) - } - } - HStack(alignment: .firstTextBaseline) { - Text(verbatim: "2.") - .font(.uiCaption) - .foregroundStyle(.contentText) - Text("\(String.swipeNumberTagBelow) \(String.tapDisplayedButton)") - .font(.uiCaption) - .foregroundStyle(.contentText) - } - HStack(alignment: .firstTextBaseline) { - Text(verbatim: "3.") - .font(.uiCaption) - .foregroundStyle(.contentText) - Text(String.serverNumberTagsWebpageWillBeUpdated) - .font(.uiCaption) - .foregroundStyle(.contentText) - } - Link(String.learnMore, destination: URL(string: String.howToUseUrl)!) - } - } + Text(Strings.shopDetailInstruction) + .foregroundStyle(.contentText) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.leading) } var cardsView: some View { @@ -113,13 +63,13 @@ private extension ShopDetailView { .swipeActions(edge: .trailing, allowsFullSwipe: false) { if itemTag.state == ItemTagState.idled { Button { viewModel.completeTag(itemTagId: itemTag.id) } label: { - Label(String.complete, systemImage: "bolt.fill") + Label(Strings.complete, systemImage: "bolt.fill") .labelStyle(.titleOnly) } .tint(.blue) } else { - Button(role: .destructive) { viewModel.resetTag(itemTagId: itemTag.id) } label: { - Label(String.reset, systemImage: "trash") + Button(role: .destructive) { viewModel.idleTag(itemTagId: itemTag.id) } label: { + Label(Strings.idle, systemImage: "trash") .labelStyle(.titleOnly) } .tint(.validationError) diff --git a/NativeAppTemplate/UI/Shop Detail/ShopDetailViewModel.swift b/NativeAppTemplate/UI/Shop Detail/ShopDetailViewModel.swift index 8d03110..a8b296f 100644 --- a/NativeAppTemplate/UI/Shop Detail/ShopDetailViewModel.swift +++ b/NativeAppTemplate/UI/Shop Detail/ShopDetailViewModel.swift @@ -10,7 +10,7 @@ import SwiftUI @MainActor final class ShopDetailViewModel { var isFetching = true - var isResetting = false + var isIdling = false var isCompleting = false var itemTags: [ItemTag] = [] var shouldDismiss: Bool = false @@ -43,7 +43,7 @@ final class ShopDetailViewModel { } var isBusy: Bool { - isFetching || isResetting || isCompleting + isFetching || isIdling || isCompleting } var isLoggedIn: Bool { @@ -75,21 +75,12 @@ final class ShopDetailViewModel { isCompleting = true do { - let itemTag = try await itemTagRepository.complete(id: itemTagId) - if itemTag.alreadyCompleted == true { - messageBus.post(message: Message( - level: .warning, - message: .itemTagAlreadyCompleted, - autoDismiss: false - )) - } else { - messageBus.post(message: Message(level: .success, message: .itemTagCompleted)) - } + _ = try await itemTagRepository.complete(id: itemTagId) } catch { messageBus.post( message: Message( level: .error, - message: "\(String.itemTagCompletedError) \(error.codedDescription)", + message: "\(Strings.itemTagCompletedError) \(error.codedDescription)", autoDismiss: false ) ) @@ -100,24 +91,23 @@ final class ShopDetailViewModel { } } - func resetTag(itemTagId: String) { + func idleTag(itemTagId: String) { Task { - isResetting = true + isIdling = true do { - _ = try await itemTagRepository.reset(id: itemTagId) - messageBus.post(message: Message(level: .success, message: .itemTagReset)) + _ = try await itemTagRepository.idle(id: itemTagId) } catch { messageBus.post( message: Message( level: .error, - message: "\(String.itemTagResetError) \(error.codedDescription)", + message: "\(Strings.itemTagIdledError) \(error.codedDescription)", autoDismiss: false ) ) } - isResetting = false + isIdling = false reload() } } diff --git a/NativeAppTemplate/UI/Shop List/ShopCreateView.swift b/NativeAppTemplate/UI/Shop List/ShopCreateView.swift index 970a6a1..cf67d40 100644 --- a/NativeAppTemplate/UI/Shop List/ShopCreateView.swift +++ b/NativeAppTemplate/UI/Shop List/ShopCreateView.swift @@ -35,36 +35,59 @@ struct ShopCreateView: View { NavigationStack { Form { Section { - TextField(String.name, text: $viewModel.name) + TextField(Strings.name, text: $viewModel.name) + .onChange(of: viewModel.name) { + viewModel.validateNameLength() + } + } header: { + Text(Strings.shopName) } footer: { - Text(String.shopNameIsRequired) - .foregroundStyle(viewModel.hasInvalidData ? .validationError : .clear) + VStack(alignment: .leading) { + Text(Strings.shopNameHelp(maximumLength: viewModel.maximumNameLength)) + .font(.uiFootnote) + Text(Strings.shopNameIsInvalid) + .font(.uiFootnote) + .foregroundStyle(viewModel.hasInvalidDataName ? .validationError : .clear) + } } Section { - TextField(String.descriptionString, text: $viewModel.description, axis: .vertical) + TextField(Strings.descriptionString, text: $viewModel.description, axis: .vertical) .lineLimit(10, reservesSpace: true) + .onChange(of: viewModel.description) { + viewModel.validateDescriptionLength() + } + } header: { + Text(Strings.descriptionString) + } footer: { + VStack(alignment: .leading) { + Text(Strings.shopDescriptionHelp(maximumLength: viewModel.maximumDescriptionLength)) + .font(.uiFootnote) + Text(Strings.shopDescriptionIsInvalid) + .font(.uiFootnote) + .foregroundStyle(viewModel.hasInvalidDataDescription ? .validationError : .clear) + } } Section { - Picker(String.timeZone, selection: $viewModel.selectedTimeZone) { + Picker(Strings.timeZone, selection: $viewModel.selectedTimeZone) { ForEach(timeZones.keys, id: \.self) { key in Text(timeZones[key]!).tag(key) } } } } - .navigationTitle(String.addShop) + .navigationTitle(Strings.addShop) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { - Button(String.save) { + Button(Strings.save) { viewModel.createShop() } .disabled(viewModel.hasInvalidData) } ToolbarItem(placement: .navigationBarLeading) { - Button(String.cancel) { + Button(Strings.cancel) { dismiss() } } diff --git a/NativeAppTemplate/UI/Shop List/ShopCreateViewModel.swift b/NativeAppTemplate/UI/Shop List/ShopCreateViewModel.swift index 9ba91f1..77eed06 100644 --- a/NativeAppTemplate/UI/Shop List/ShopCreateViewModel.swift +++ b/NativeAppTemplate/UI/Shop List/ShopCreateViewModel.swift @@ -31,7 +31,37 @@ final class ShopCreateViewModel { } var hasInvalidData: Bool { - Utility.isBlank(name) + hasInvalidDataName || hasInvalidDataDescription + } + + var hasInvalidDataName: Bool { + if name.isBlank { + return true + } + if name.count > maximumNameLength { + return true + } + return false + } + + var hasInvalidDataDescription: Bool { + description.count > maximumDescriptionLength + } + + var maximumNameLength: Int { + NativeAppTemplateConstants.maximumShopNameLength + } + + var maximumDescriptionLength: Int { + NativeAppTemplateConstants.maximumShopDescriptionLength + } + + func validateNameLength() { + name = String(name.prefix(maximumNameLength)) + } + + func validateDescriptionLength() { + description = String(description.prefix(maximumDescriptionLength)) } func createShop() { @@ -46,7 +76,7 @@ final class ShopCreateViewModel { timeZone: selectedTimeZone ) _ = try await shopRepository.create(shop: shop) - messageBus.post(message: Message(level: .success, message: .shopCreated)) + messageBus.post(message: Message(level: .success, message: Strings.shopCreated)) shouldDismiss = true } catch { messageBus.post(message: Message(error: error)) diff --git a/NativeAppTemplate/UI/Shop List/ShopListCardView.swift b/NativeAppTemplate/UI/Shop List/ShopListCardView.swift index 820b8d1..74ee1a9 100644 --- a/NativeAppTemplate/UI/Shop List/ShopListCardView.swift +++ b/NativeAppTemplate/UI/Shop List/ShopListCardView.swift @@ -21,18 +21,6 @@ struct ShopListCardView: View { horizontalSpacing: NativeAppTemplateConstants.Spacing.xs, verticalSpacing: NativeAppTemplateConstants.Spacing.xxxs ) { - GridRow { - Image(systemName: "person.2") - .frame(width: statImageSize, height: statImageSize) - .foregroundStyle(.secondaryText) - Text(String(shop.scannedItemTagsCount)) - .font(.uiLabelBold) - .gridColumnAlignment(.trailing) - Text(verbatim: "tags scanned by customers") - .font(.uiFootnote) - .foregroundStyle(.contentText) - } - GridRow { Image(systemName: "flag.checkered") .frame(width: statImageSize, height: statImageSize) diff --git a/NativeAppTemplate/UI/Shop List/ShopListView.swift b/NativeAppTemplate/UI/Shop List/ShopListView.swift index 57f6979..b5321b2 100644 --- a/NativeAppTemplate/UI/Shop List/ShopListView.swift +++ b/NativeAppTemplate/UI/Shop List/ShopListView.swift @@ -8,11 +8,11 @@ import TipKit struct TapShopBelowTip: Tip { var title: Text { - Text(String.tapShopBelow) + Text(Strings.tapShopBelow) } var message: Text? { - Text(String.haveFun) + Text(Strings.haveFun) } var image: Image? { @@ -129,7 +129,7 @@ private extension ShopListView { } } } - .navigationTitle(String.shops) + .navigationTitle(Strings.shops) .navigationBarTitleDisplayMode(.inline) .toolbar { if viewModel.leftInShopSlots > 0 { @@ -172,11 +172,11 @@ private extension ShopListView { .frame(width: NativeAppTemplateConstants.Spacing.xxxl) .padding() - Text(String.addShopDescription) + Text(Strings.addShopDescription) .foregroundStyle(.contentText) .padding() - MainButtonView(title: String.addShop, type: .primary(withArrow: false)) { + MainButtonView(title: Strings.addShop, type: .primary(withArrow: false)) { viewModel.showCreateView() } .padding() diff --git a/NativeAppTemplate/UI/Shop Settings/ItemTag Detail/ItemTagDetailView.swift b/NativeAppTemplate/UI/Shop Settings/ItemTag Detail/ItemTagDetailView.swift index 487452a..3f5a259 100644 --- a/NativeAppTemplate/UI/Shop Settings/ItemTag Detail/ItemTagDetailView.swift +++ b/NativeAppTemplate/UI/Shop Settings/ItemTag Detail/ItemTagDetailView.swift @@ -3,12 +3,12 @@ // NativeAppTemplate // -import CoreNFC -import Photos import SwiftUI struct ItemTagDetailView: View { @Environment(\.dismiss) private var dismiss + @Environment(DataManager.self) private var dataManager + @Environment(MessageBus.self) private var messageBus @State private var viewModel: ItemTagDetailViewModel init(viewModel: ItemTagDetailViewModel) { @@ -31,93 +31,25 @@ struct ItemTagDetailView: View { // MARK: - private private extension ItemTagDetailView { - var contentView: some View { - @ViewBuilder var contentView: some View { - if viewModel.isBusy { - LoadingView() - } else { - itemTagDetailView - } + @ViewBuilder var contentView: some View { + if viewModel.isBusy, viewModel.itemTag == nil { + LoadingView() + } else if let itemTag = viewModel.itemTag { + itemTagDetailView(itemTag: itemTag) } - - return contentView } - private var itemTagDetailView: some View { + func itemTagDetailView(itemTag: ItemTag) -> some View { ScrollView { - VStack(alignment: .center) { - VStack(alignment: .center, spacing: 0) { - Text(verbatim: "Write Info to Tag / Save Customer QR code") - .font(.title2) - .padding(.top, NativeAppTemplateConstants.Spacing.xxs) - - Text(viewModel.shop.name) - .font(.title3) - .padding(.top, NativeAppTemplateConstants.Spacing.sm) - - if let itemTag = viewModel.itemTag { - Text(String(itemTag.queueNumber)) - .font(.largeTitle) - .bold() - .padding(.top, NativeAppTemplateConstants.Spacing.xxs) - .foregroundStyle(.lightestAccent) - } - } - - GroupBox(label: Label(String("Lock"), systemImage: "lock")) { - Toggle(isOn: $viewModel.isLocked) { - Text(verbatim: "Lock") - .lineLimit(1) - } - .toggleStyle(.button) - .dynamicTypeSize(...DynamicTypeSize.large) - .tint(.lockForeground) + VStack(alignment: .leading, spacing: NativeAppTemplateConstants.Spacing.md) { + headerRow(itemTag: itemTag) + descriptionSection(itemTag: itemTag) + completedAtRow(itemTag: itemTag) + stateToggleButton(itemTag: itemTag) - if viewModel.isLocked { - Text(String.youCannotUndoAfterLockingTag) - .font(.uiFootnote) - .foregroundStyle(.alarm) - } - } - .foregroundStyle(.lockForeground) - .backgroundStyle(.ultraThinMaterial) - - GroupBox(label: Label(String("Server"), systemImage: "storefront")) { - MainButtonView(title: String.writeServerTag, type: .server(withArrow: false)) { - viewModel.writeServerTag() - } - .padding() - } - .foregroundStyle(.serverForeground) - .backgroundStyle(.ultraThinMaterial) - - GroupBox(label: Label(String("Customer"), systemImage: "person.2")) { - MainButtonView(title: String.writeCustomerTag, type: .customer(withArrow: false)) { - viewModel.writeCustomerTag() - } - .padding() - - if let customerTagQrCodeImage = viewModel.customerTagQrCodeImage { - Image(uiImage: customerTagQrCodeImage) - .resizable() - .frame( - width: NativeAppTemplateConstants.Spacing.xxxl, - height: NativeAppTemplateConstants.Spacing.xxxl - ) - - Button { - viewModel.saveImageToPhotoAlbum() - } label: { - Text(String.saveToPhotoAlbum) - } - } else { - generateCustomerQrCodeView - } - } - .padding(.top, NativeAppTemplateConstants.Spacing.md) - .foregroundStyle(.customerForeground) - .backgroundStyle(.ultraThinMaterial) + Spacer() } + .padding() } .sheet( isPresented: $viewModel.isShowingEditSheet, @@ -127,51 +59,108 @@ private extension ItemTagDetailView { content: { ItemTagEditView( viewModel: ItemTagEditViewModel( - itemTagRepository: viewModel.itemTagRepository, - messageBus: viewModel.messageBus, - sessionController: viewModel.sessionController, + itemTagRepository: dataManager.itemTagRepository, + messageBus: messageBus, itemTagId: viewModel.itemTagId ) ) } ) .alert( - String.buttonDeleteTag, + Strings.buttonDeleteItemTag, isPresented: $viewModel.isShowingDeleteConfirmationDialog ) { - Button(String.buttonDeleteTag, role: .destructive) { + Button(Strings.buttonDeleteItemTag, role: .destructive) { viewModel.destroyItemTag() } - Button(String.cancel, role: .cancel) { + Button(Strings.cancel, role: .cancel) { viewModel.isShowingDeleteConfirmationDialog = false } } message: { - Text(String.areYouSure) + Text(Strings.areYouSure) } - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button { - viewModel.isShowingEditSheet.toggle() - } label: { - Text(String.edit) - } + .toolbar { toolbarContent } + } + + func headerRow(itemTag: ItemTag) -> some View { + HStack { + Text(itemTag.name) + .font(.largeTitle) + .bold() + + Spacer() + + if itemTag.state == .completed { + CompletedTag() + } else { + IdlingTag() } - ToolbarItem(placement: .navigationBarTrailing) { - Button { - viewModel.isShowingDeleteConfirmationDialog.toggle() - } label: { - Image(systemName: "trash") - } + } + } + + @ViewBuilder + func descriptionSection(itemTag: ItemTag) -> some View { + if !itemTag.description.isEmpty { + VStack(alignment: .leading, spacing: NativeAppTemplateConstants.Spacing.xxs) { + Text(Strings.descriptionLabel) + .font(.uiTitle4) + .foregroundStyle(.titleText) + Text(itemTag.description) + .font(.body) + .foregroundStyle(.contentText) + } + } + } + + @ViewBuilder + func completedAtRow(itemTag: ItemTag) -> some View { + if let completedAt = itemTag.completedAt, itemTag.state == .completed { + HStack { + Text(Strings.completedAtLabel) + .font(.uiFootnote) + .foregroundStyle(.contentText) + Text(completedAt.cardDateTimeString) + .font(.uiFootnote) + .foregroundStyle(.contentText) } } } - private var generateCustomerQrCodeView: some View { - VStack { + @ViewBuilder + func stateToggleButton(itemTag: ItemTag) -> some View { + if itemTag.state == .idled { + MainButtonView( + title: Strings.markAsCompleted, + type: .primary(withArrow: false) + ) { + viewModel.completeItemTag() + } + .disabled(viewModel.isToggling) + } else { + MainButtonView( + title: Strings.markAsIdled, + type: .secondary(withArrow: false) + ) { + viewModel.idleItemTag() + } + .disabled(viewModel.isToggling) + } + } + + @ToolbarContentBuilder + var toolbarContent: some ToolbarContent { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + viewModel.isShowingEditSheet.toggle() + } label: { + Text(Strings.edit) + } + } + ToolbarItem(placement: .navigationBarTrailing) { Button { - viewModel.generateCustomerQrCode() + viewModel.isShowingDeleteConfirmationDialog.toggle() } label: { - Text(String.generateCustomerQrCode) + Image(systemName: "trash") } } } diff --git a/NativeAppTemplate/UI/Shop Settings/ItemTag Detail/ItemTagDetailViewModel.swift b/NativeAppTemplate/UI/Shop Settings/ItemTag Detail/ItemTagDetailViewModel.swift index f5b2978..d856661 100644 --- a/NativeAppTemplate/UI/Shop Settings/ItemTag Detail/ItemTagDetailViewModel.swift +++ b/NativeAppTemplate/UI/Shop Settings/ItemTag Detail/ItemTagDetailViewModel.swift @@ -3,30 +3,23 @@ // NativeAppTemplate // -import CoreNFC import Observation -import Photos import SwiftUI @Observable @MainActor final class ItemTagDetailViewModel { - var isLocked = false var isShowingEditSheet = false var isShowingDeleteConfirmationDialog = false var isFetching = true - var isGeneratingQrCode = false + var isToggling = false var isDeleting = false - var customerTagQrCodeImage: UIImage? var shouldDismiss = false private(set) var itemTag: ItemTag? let itemTagRepository: ItemTagRepositoryProtocol let messageBus: MessageBus let sessionController: SessionControllerProtocol - private let nfcManager: NFCManager - private let qrCodeGenerator = QRCodeGenerator() - private let imageSaver = ImageSaver() let shop: Shop let itemTagId: String @@ -34,105 +27,63 @@ final class ItemTagDetailViewModel { itemTagRepository: ItemTagRepositoryProtocol, messageBus: MessageBus, sessionController: SessionControllerProtocol, - nfcManager: NFCManager, shop: Shop, itemTagId: String ) { self.itemTagRepository = itemTagRepository self.messageBus = messageBus self.sessionController = sessionController - self.nfcManager = nfcManager self.shop = shop self.itemTagId = itemTagId } var isBusy: Bool { - isFetching || isDeleting || isGeneratingQrCode + isFetching || isDeleting || isToggling } func reload() { fetchItemTagDetail() } - func generateCustomerQrCode() { + func completeItemTag() { guard let itemTag else { return } - isGeneratingQrCode = true - - let scanUrl = itemTag.scanUrl(itemTagType: ItemTagType.customer) - - customerTagQrCodeImage = qrCodeGenerator.generateWithCenterText( - inputText: scanUrl.absoluteString, - centerText: String(itemTag.queueNumber) - ) - - isGeneratingQrCode = false - } - - func writeServerTag() { - guard let itemTag else { return } + Task { + isToggling = true - guard NFCNDEFReaderSession.readingAvailable else { - messageBus.post( - message: Message( + do { + let updated = try await itemTagRepository.complete(id: itemTag.id) + self.itemTag = updated + } catch { + messageBus.post(message: Message( level: .error, - message: String.thisDeviceDoesNotSupportTagScanning, + message: "\(Strings.itemTagCompletedError) \(error.codedDescription)", autoDismiss: false - ) - ) - return - } - - nonisolated(unsafe) let ndefMessage = createNdefMessage(itemTag: itemTag, itemTagType: .server) + )) + } - Task { - await nfcManager.startWriting(ndefMessage: ndefMessage, isLock: isLocked) + isToggling = false } } - func writeCustomerTag() { + func idleItemTag() { guard let itemTag else { return } - guard NFCNDEFReaderSession.readingAvailable else { - messageBus.post( - message: Message( - level: .error, - message: String.thisDeviceDoesNotSupportTagScanning, - autoDismiss: false - ) - ) - return - } - - nonisolated(unsafe) let ndefMessage = createNdefMessage(itemTag: itemTag, itemTagType: .customer) - Task { - await nfcManager.startWriting(ndefMessage: ndefMessage, isLock: isLocked) - } - } + isToggling = true - func saveImageToPhotoAlbum() { - guard let customerTagQrCodeImage else { return } - - getSaveToPhotoAlbumPermissionIfNeeded { granted in - guard granted else { return } - - self.imageSaver.save(image: customerTagQrCodeImage) { error in - if let error { - self.messageBus.post( - message: Message( - level: .error, - message: "\(String.customerQrCodeImageSavedToPhotoAlbumError)(\(error))", - autoDismiss: false - ) - ) - } else { - self.messageBus.post(message: Message( - level: .success, - message: .customerQrCodeImageSavedToPhotoAlbum - )) - } + do { + let updated = try await itemTagRepository.idle(id: itemTag.id) + self.itemTag = updated + } catch { + messageBus.post(message: Message( + level: .error, + message: "\(Strings.itemTagIdledError) \(error.codedDescription)", + autoDismiss: false + )) } + + isToggling = false } } @@ -144,11 +95,11 @@ final class ItemTagDetailViewModel { do { try await itemTagRepository.destroy(id: itemTag.id) - messageBus.post(message: Message(level: .success, message: .itemTagDeleted)) + messageBus.post(message: Message(level: .success, message: Strings.itemTagDeleted)) } catch { messageBus.post(message: Message( level: .error, - message: "\(String.itemTagDeletedError) \(error.codedDescription)", + message: "\(Strings.itemTagDeletedError) \(error.codedDescription)", autoDismiss: false )) } @@ -170,34 +121,4 @@ final class ItemTagDetailViewModel { isFetching = false } } - - private func createNdefMessage(itemTag: ItemTag, itemTagType: ItemTagType) -> NFCNDEFMessage { - let scanUrl = itemTag.scanUrl(itemTagType: itemTagType) - let urlPayload = NFCNDEFPayload.wellKnownTypeURIPayload(url: scanUrl) - let androidAarPayloadData = String.androidAar.data(using: .utf8)! - let androidAarPayload = NFCNDEFPayload( - format: .nfcExternal, - type: Data(String.androidAarNfcndefPayloadType.utf8), - identifier: Data(), - payload: androidAarPayloadData - ) - - return if itemTagType == ItemTagType.server { - NFCNDEFMessage(records: [urlPayload!, androidAarPayload]) - } else { - NFCNDEFMessage(records: [urlPayload!]) - } - } - - private func getSaveToPhotoAlbumPermissionIfNeeded(completionHandler: @escaping (Bool) -> Void) { - guard PHPhotoLibrary.authorizationStatus(for: .addOnly) != .authorized else { - completionHandler(true) - return - } - - nonisolated(unsafe) let completionHandler = completionHandler - PHPhotoLibrary.requestAuthorization(for: .addOnly) { status in - completionHandler(status == .authorized ? true : false) - } - } } diff --git a/NativeAppTemplate/UI/Shop Settings/ItemTag Detail/ItemTagEditView.swift b/NativeAppTemplate/UI/Shop Settings/ItemTag Detail/ItemTagEditView.swift index ecb623d..0a24769 100644 --- a/NativeAppTemplate/UI/Shop Settings/ItemTag Detail/ItemTagEditView.swift +++ b/NativeAppTemplate/UI/Shop Settings/ItemTag Detail/ItemTagEditView.swift @@ -33,7 +33,7 @@ private extension ItemTagEditView { @ViewBuilder var contentView: some View { if viewModel.isBusy { LoadingView() - } else { + } else if viewModel.itemTag != nil { itemTagEditView } } @@ -45,32 +45,47 @@ private extension ItemTagEditView { NavigationStack { Form { Section { - TextField(String("A001"), text: $viewModel.queueNumber) - .keyboardType(.asciiCapable) - .onChange(of: viewModel.queueNumber) { _, _ in - viewModel.validateQueueNumberLength() + TextField(Strings.itemTagNamePlaceholder, text: $viewModel.name) + .onChange(of: viewModel.name) { + viewModel.validateNameLength() } } header: { - Text(String.tagNumber) + Text(Strings.nameLabel) } footer: { VStack(alignment: .leading) { - Text("Name must be a 2-\(viewModel.maximumQueueNumberLength) alphanumeric characters.") + Text(Strings.itemTagNameHelp(maximumLength: viewModel.maximumNameLength)) .font(.uiFootnote) - Text(String.zeroPadding) + Text(Strings.itemTagNameIsInvalid) .font(.uiFootnote) - Text(String.tagNumberIsInvalid) + .foregroundStyle(viewModel.hasInvalidDataName ? .validationError : .clear) + } + } + + Section { + TextEditor(text: $viewModel.description) + .frame(minHeight: 100) + .onChange(of: viewModel.description) { + viewModel.validateDescriptionLength() + } + } header: { + Text(Strings.descriptionLabel) + } footer: { + VStack(alignment: .leading) { + Text(Strings.itemTagDescriptionHelp(maximumLength: viewModel.maximumDescriptionLength)) + .font(.uiFootnote) + Text(Strings.itemTagDescriptionIsInvalid) .font(.uiFootnote) - .foregroundStyle(viewModel.hasInvalidDataQueueNumber ? .validationError : .clear) + .foregroundStyle(viewModel.hasInvalidDataDescription ? .validationError : .clear) } } } - .navigationTitle(String.editTag) + .navigationTitle(Strings.editItemTag) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button { viewModel.updateItemTag() } label: { - Text(String.save) + Text(Strings.save) } .disabled(viewModel.hasInvalidData) } @@ -78,7 +93,7 @@ private extension ItemTagEditView { Button { dismiss() } label: { - Text(String.cancel) + Text(Strings.cancel) } } } diff --git a/NativeAppTemplate/UI/Shop Settings/ItemTag Detail/ItemTagEditViewModel.swift b/NativeAppTemplate/UI/Shop Settings/ItemTag Detail/ItemTagEditViewModel.swift index a7c2381..7216a61 100644 --- a/NativeAppTemplate/UI/Shop Settings/ItemTag Detail/ItemTagEditViewModel.swift +++ b/NativeAppTemplate/UI/Shop Settings/ItemTag Detail/ItemTagEditViewModel.swift @@ -9,7 +9,8 @@ import SwiftUI @Observable @MainActor final class ItemTagEditViewModel { - var queueNumber = "" + var name = "" + var description = "" var isFetching = true var isUpdating = false var shouldDismiss = false @@ -17,18 +18,15 @@ final class ItemTagEditViewModel { private let itemTagRepository: ItemTagRepositoryProtocol private let messageBus: MessageBus - private let sessionController: SessionControllerProtocol private let itemTagId: String init( itemTagRepository: ItemTagRepositoryProtocol, messageBus: MessageBus, - sessionController: SessionControllerProtocol, itemTagId: String ) { self.itemTagRepository = itemTagRepository self.messageBus = messageBus - self.sessionController = sessionController self.itemTagId = itemTagId } @@ -39,43 +37,53 @@ final class ItemTagEditViewModel { var hasInvalidData: Bool { guard let itemTag else { return true } - if hasInvalidDataQueueNumber { + if hasInvalidDataName { return true } - if itemTag.queueNumber == queueNumber { + if hasInvalidDataDescription { + return true + } + + if itemTag.name == name, itemTag.description == description { return true } return false } - var hasInvalidDataQueueNumber: Bool { - if Utility.isBlank(queueNumber) { + var hasInvalidDataName: Bool { + if name.isBlank { return true } - - if !queueNumber.isAlphanumeric(ignoreDiacritics: true) { + if name.count > maximumNameLength { return true } + return false + } - if !(queueNumber.count >= 2 && queueNumber.count <= maximumQueueNumberLength) { - return true - } + var hasInvalidDataDescription: Bool { + description.count > maximumDescriptionLength + } - return false + var maximumNameLength: Int { + NativeAppTemplateConstants.maximumItemTagNameLength } - var maximumQueueNumberLength: Int { - sessionController.maximumQueueNumberLength + var maximumDescriptionLength: Int { + NativeAppTemplateConstants.maximumItemTagDescriptionLength } func reload() { fetchItemTagDetail() } - func validateQueueNumberLength() { - queueNumber = String(queueNumber.prefix(maximumQueueNumberLength)) + func validateNameLength() { + name = String(name.prefix(maximumNameLength)) + } + + func validateDescriptionLength() { + description = String(description.prefix(maximumDescriptionLength)) } func updateItemTag() { @@ -85,11 +93,12 @@ final class ItemTagEditViewModel { do { let itemTag = ItemTag( id: itemTagId, - queueNumber: queueNumber + name: name, + description: description ) - _ = try await itemTagRepository.update(id: itemTagId, itemTag: itemTag) - messageBus.post(message: Message(level: .success, message: .itemTagUpdated)) + _ = try await itemTagRepository.update(id: itemTag.id, itemTag: itemTag) + messageBus.post(message: Message(level: .success, message: Strings.itemTagUpdated)) } catch { messageBus.post(message: Message(error: error)) } @@ -106,7 +115,8 @@ final class ItemTagEditViewModel { do { itemTag = try await itemTagRepository.fetchDetail(id: itemTagId) if let itemTag { - queueNumber = String(itemTag.queueNumber) + name = itemTag.name + description = itemTag.description } } catch { messageBus.post(message: Message(error: error)) diff --git a/NativeAppTemplate/UI/Shop Settings/ItemTag List/ItemTagCreateView.swift b/NativeAppTemplate/UI/Shop Settings/ItemTag List/ItemTagCreateView.swift index a7c8dcc..1152413 100644 --- a/NativeAppTemplate/UI/Shop Settings/ItemTag List/ItemTagCreateView.swift +++ b/NativeAppTemplate/UI/Shop Settings/ItemTag List/ItemTagCreateView.swift @@ -42,32 +42,47 @@ private extension ItemTagCreateView { NavigationStack { Form { Section { - TextField(String("A001"), text: $viewModel.queueNumber) - .keyboardType(.asciiCapable) - .onChange(of: viewModel.queueNumber) { _, _ in - viewModel.validateQueueNumberLength() + TextField(Strings.itemTagNamePlaceholder, text: $viewModel.name) + .onChange(of: viewModel.name) { + viewModel.validateNameLength() } } header: { - Text(String.tagNumber) + Text(Strings.nameLabel) } footer: { VStack(alignment: .leading) { - Text("Name must be a 2-\(viewModel.maximumQueueNumberLength) alphanumeric characters.") + Text(Strings.itemTagNameHelp(maximumLength: viewModel.maximumNameLength)) .font(.uiFootnote) - Text(String.zeroPadding) + Text(Strings.itemTagNameIsInvalid) .font(.uiFootnote) - Text(String.tagNumberIsInvalid) + .foregroundStyle(viewModel.hasInvalidDataName ? .validationError : .clear) + } + } + + Section { + TextEditor(text: $viewModel.description) + .frame(minHeight: 100) + .onChange(of: viewModel.description) { + viewModel.validateDescriptionLength() + } + } header: { + Text(Strings.descriptionLabel) + } footer: { + VStack(alignment: .leading) { + Text(Strings.itemTagDescriptionHelp(maximumLength: viewModel.maximumDescriptionLength)) + .font(.uiFootnote) + Text(Strings.itemTagDescriptionIsInvalid) .font(.uiFootnote) - .foregroundStyle(viewModel.hasInvalidDataQueueNumber ? .validationError : .clear) + .foregroundStyle(viewModel.hasInvalidDataDescription ? .validationError : .clear) } } } - .navigationTitle(String.addTag) + .navigationTitle(Strings.addItemTag) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button { viewModel.createItemTag() } label: { - Text(String.save) + Text(Strings.save) } .disabled(viewModel.hasInvalidData) } @@ -75,7 +90,7 @@ private extension ItemTagCreateView { Button { dismiss() } label: { - Text(String.cancel) + Text(Strings.cancel) } } } diff --git a/NativeAppTemplate/UI/Shop Settings/ItemTag List/ItemTagCreateViewModel.swift b/NativeAppTemplate/UI/Shop Settings/ItemTag List/ItemTagCreateViewModel.swift index f299624..c00c399 100644 --- a/NativeAppTemplate/UI/Shop Settings/ItemTag List/ItemTagCreateViewModel.swift +++ b/NativeAppTemplate/UI/Shop Settings/ItemTag List/ItemTagCreateViewModel.swift @@ -9,24 +9,22 @@ import SwiftUI @Observable @MainActor final class ItemTagCreateViewModel { - var queueNumber = "" + var name = "" + var description = "" var isCreating = false var shouldDismiss = false private let itemTagRepository: ItemTagRepositoryProtocol private let messageBus: MessageBus - private let sessionController: SessionControllerProtocol private let shopId: String init( itemTagRepository: ItemTagRepositoryProtocol, messageBus: MessageBus, - sessionController: SessionControllerProtocol, shopId: String ) { self.itemTagRepository = itemTagRepository self.messageBus = messageBus - self.sessionController = sessionController self.shopId = shopId } @@ -35,31 +33,37 @@ final class ItemTagCreateViewModel { } var hasInvalidData: Bool { - hasInvalidDataQueueNumber + hasInvalidDataName || hasInvalidDataDescription } - var hasInvalidDataQueueNumber: Bool { - if Utility.isBlank(queueNumber) { + var hasInvalidDataName: Bool { + if name.isBlank { return true } - - if !queueNumber.isAlphanumeric(ignoreDiacritics: true) { + if name.count > maximumNameLength { return true } + return false + } - if !(queueNumber.count >= 2 && queueNumber.count <= maximumQueueNumberLength) { - return true - } + var hasInvalidDataDescription: Bool { + description.count > maximumDescriptionLength + } - return false + var maximumNameLength: Int { + NativeAppTemplateConstants.maximumItemTagNameLength + } + + var maximumDescriptionLength: Int { + NativeAppTemplateConstants.maximumItemTagDescriptionLength } - var maximumQueueNumberLength: Int { - sessionController.maximumQueueNumberLength + func validateNameLength() { + name = String(name.prefix(maximumNameLength)) } - func validateQueueNumberLength() { - queueNumber = String(queueNumber.prefix(maximumQueueNumberLength)) + func validateDescriptionLength() { + description = String(description.prefix(maximumDescriptionLength)) } func createItemTag() { @@ -67,9 +71,9 @@ final class ItemTagCreateViewModel { isCreating = true do { - let itemTag = ItemTag(queueNumber: queueNumber) + let itemTag = ItemTag(name: name, description: description) _ = try await itemTagRepository.create(shopId: shopId, itemTag: itemTag) - messageBus.post(message: Message(level: .success, message: .itemTagCreated)) + messageBus.post(message: Message(level: .success, message: Strings.itemTagCreated)) } catch { messageBus.post(message: Message(error: error)) } diff --git a/NativeAppTemplate/UI/Shop Settings/ItemTag List/ItemTagListCardView.swift b/NativeAppTemplate/UI/Shop Settings/ItemTag List/ItemTagListCardView.swift index 68a464d..3103086 100644 --- a/NativeAppTemplate/UI/Shop Settings/ItemTag List/ItemTagListCardView.swift +++ b/NativeAppTemplate/UI/Shop Settings/ItemTag List/ItemTagListCardView.swift @@ -9,7 +9,36 @@ struct ItemTagListCardView: View { let itemTag: ItemTag var body: some View { - Text(String(itemTag.queueNumber)) - .font(.uiTitle4) + HStack { + VStack(alignment: .leading, spacing: NativeAppTemplateConstants.Spacing.xxs) { + Text(itemTag.name) + .font(.uiTitle4) + .foregroundStyle(.titleText) + + if !itemTag.description.isEmpty { + Text(itemTag.description) + .font(.uiFootnote) + .foregroundStyle(.contentText) + .lineLimit(2) + } + } + + Spacer() + + VStack(alignment: .trailing) { + if itemTag.state == .completed { + CompletedTag() + if let completedAt = itemTag.completedAt { + Text(completedAt.cardDateTimeString) + .font(.uiFootnote) + .foregroundStyle(.contentText) + } + } else { + IdlingTag() + } + } + .frame(minWidth: 82, alignment: .trailing) + } + .frame(minHeight: NativeAppTemplateConstants.Spacing.xl) } } diff --git a/NativeAppTemplate/UI/Shop Settings/ItemTag List/ItemTagListView.swift b/NativeAppTemplate/UI/Shop Settings/ItemTag List/ItemTagListView.swift index 3702fe5..9901a98 100644 --- a/NativeAppTemplate/UI/Shop Settings/ItemTag List/ItemTagListView.swift +++ b/NativeAppTemplate/UI/Shop Settings/ItemTag List/ItemTagListView.swift @@ -59,7 +59,13 @@ private extension ItemTagListView { ForEach(viewModel.itemTags) { itemTag in NavigationLink( destination: ItemTagDetailView( - viewModel: viewModel.createItemTagDetailViewModel(itemTagId: itemTag.id) + viewModel: ItemTagDetailViewModel( + itemTagRepository: dataManager.itemTagRepository, + messageBus: messageBus, + sessionController: dataManager.sessionController, + shop: viewModel.shop, + itemTagId: itemTag.id + ) ) ) { ItemTagListCardView( @@ -67,7 +73,7 @@ private extension ItemTagListView { ) .swipeActions(edge: .trailing, allowsFullSwipe: false) { Button(role: .destructive) { viewModel.destroyItemTag(itemTagId: itemTag.id) } label: { - Label(String.delete, systemImage: "trash") + Label(Strings.delete, systemImage: "trash") .labelStyle(.titleOnly) } .tint(.validationError) @@ -85,7 +91,7 @@ private extension ItemTagListView { } } } - .navigationTitle(String.shopSettingsManageItemTagsLabel) + .navigationTitle(Strings.shopSettingsManageItemTagsLabel) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button { @@ -102,7 +108,11 @@ private extension ItemTagListView { }, content: { ItemTagCreateView( - viewModel: viewModel.createItemTagCreateViewModel() + viewModel: ItemTagCreateViewModel( + itemTagRepository: dataManager.itemTagRepository, + messageBus: messageBus, + shopId: viewModel.shop.id + ) ) } ) @@ -128,12 +138,11 @@ private extension ItemTagListView { .aspectRatio(contentMode: .fit) .frame(width: NativeAppTemplateConstants.Spacing.xxxl) .padding() - - Text(String.addTagDescription) + Text(Strings.addItemTagDescription) .foregroundStyle(.contentText) .padding() - MainButtonView(title: String.addTag, type: .primary(withArrow: false)) { + MainButtonView(title: Strings.addItemTag, type: .primary(withArrow: false)) { viewModel.isShowingCreateSheet.toggle() } .padding() diff --git a/NativeAppTemplate/UI/Shop Settings/ItemTag List/ItemTagListViewModel.swift b/NativeAppTemplate/UI/Shop Settings/ItemTag List/ItemTagListViewModel.swift index 5104afb..f7bd6c3 100644 --- a/NativeAppTemplate/UI/Shop Settings/ItemTag List/ItemTagListViewModel.swift +++ b/NativeAppTemplate/UI/Shop Settings/ItemTag List/ItemTagListViewModel.swift @@ -71,12 +71,12 @@ final class ItemTagListViewModel { do { try await itemTagRepository.destroy(id: itemTagId) - messageBus.post(message: Message(level: .success, message: .itemTagDeleted)) + messageBus.post(message: Message(level: .success, message: Strings.itemTagDeleted)) reload() } catch { messageBus.post(message: Message( level: .error, - message: "\(String.itemTagDeletedError) \(error.codedDescription)", + message: "\(Strings.itemTagDeletedError) \(error.codedDescription)", autoDismiss: false )) } @@ -84,24 +84,4 @@ final class ItemTagListViewModel { isDeleting = false } } - - func createItemTagDetailViewModel(itemTagId: String) -> ItemTagDetailViewModel { - ItemTagDetailViewModel( - itemTagRepository: itemTagRepository, - messageBus: messageBus, - sessionController: sessionController, - nfcManager: appSingletons.nfcManager, - shop: shop, - itemTagId: itemTagId - ) - } - - func createItemTagCreateViewModel() -> ItemTagCreateViewModel { - ItemTagCreateViewModel( - itemTagRepository: itemTagRepository, - messageBus: messageBus, - sessionController: sessionController, - shopId: shop.id - ) - } } diff --git a/NativeAppTemplate/UI/Shop Settings/NumberTagsWebpageListView.swift b/NativeAppTemplate/UI/Shop Settings/NumberTagsWebpageListView.swift deleted file mode 100644 index 6dc2a54..0000000 --- a/NativeAppTemplate/UI/Shop Settings/NumberTagsWebpageListView.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// NumberTagsWebpageListView.swift -// NativeAppTemplate -// - -import SwiftUI -import UniformTypeIdentifiers - -enum NumberTagsWebpageListType: String, Identifiable, CaseIterable, Codable, Hashable { - case server - - var id: Self { - self - } - - var displayString: String { - switch self { - case .server: - String.serverNumberTagsWebpage - } - } -} - -struct NumberTagsWebpageListView: View { - @State private var viewModel: NumberTagsWebpageListViewModel - - init(viewModel: NumberTagsWebpageListViewModel) { - _viewModel = State(wrappedValue: viewModel) - } -} - -// MARK: - View - -extension NumberTagsWebpageListView { - var body: some View { - contentView - } -} - -// MARK: - private - -private extension NumberTagsWebpageListView { - var contentView: some View { - @ViewBuilder var contentView: some View { - numberTagsWebpageListView - } - - return contentView - } - - var numberTagsWebpageListView: some View { - VStack { - Text(viewModel.shop.name) - .font(.uiTitle1) - .foregroundStyle(.titleText) - .padding(.top, NativeAppTemplateConstants.Spacing.md) - List(NumberTagsWebpageListType.allCases) { numberTagsWebpageListType in - switch numberTagsWebpageListType { - case .server: - Section { - Link(numberTagsWebpageListType.displayString, destination: viewModel.shop.displayShopServerUrl) - } header: { - Label(String("Server"), systemImage: "storefront") - } footer: { - Button(String.copyWebpageUrl) { - viewModel.copyWebpageUrl(viewModel.shop.displayShopServerUrl.absoluteString) - } - } - .listRowBackground(Color.cardBackground.opacity(0.7)) - } - } - } - .navigationTitle(String.shopSettingsNumberTagsWebpageLabel) - } -} diff --git a/NativeAppTemplate/UI/Shop Settings/NumberTagsWebpageListViewModel.swift b/NativeAppTemplate/UI/Shop Settings/NumberTagsWebpageListViewModel.swift deleted file mode 100644 index c5ec40b..0000000 --- a/NativeAppTemplate/UI/Shop Settings/NumberTagsWebpageListViewModel.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// NumberTagsWebpageListViewModel.swift -// NativeAppTemplate -// - -import Observation -import SwiftUI -import UniformTypeIdentifiers - -@Observable -@MainActor -final class NumberTagsWebpageListViewModel { - let shop: Shop - - private let messageBus: MessageBus - - init( - shop: Shop, - messageBus: MessageBus - ) { - self.shop = shop - self.messageBus = messageBus - } - - func copyWebpageUrl(_ url: String) { - UIPasteboard.general.setValue(url, forPasteboardType: UTType.plainText.identifier) - messageBus.post(message: Message(level: .success, message: .webpageUrlCopied)) - } -} diff --git a/NativeAppTemplate/UI/Shop Settings/ShopBasicSettingsView.swift b/NativeAppTemplate/UI/Shop Settings/ShopBasicSettingsView.swift index 5551d09..4d59e8d 100644 --- a/NativeAppTemplate/UI/Shop Settings/ShopBasicSettingsView.swift +++ b/NativeAppTemplate/UI/Shop Settings/ShopBasicSettingsView.swift @@ -44,24 +44,42 @@ private extension ShopBasicSettingsView { var shopBasicSettingsView: some View { Form { Section { - TextField(String.shopName, text: $viewModel.name) + TextField(Strings.shopName, text: $viewModel.name) + .onChange(of: viewModel.name) { + viewModel.validateNameLength() + } } header: { - Text(String.shopName) + Text(Strings.shopName) } footer: { - Text(String.shopNameIsRequired) - .font(.uiFootnote) - .foregroundStyle(Utility.isBlank(viewModel.name) ? .validationError : .clear) + VStack(alignment: .leading) { + Text(Strings.shopNameHelp(maximumLength: viewModel.maximumNameLength)) + .font(.uiFootnote) + Text(Strings.shopNameIsInvalid) + .font(.uiFootnote) + .foregroundStyle(viewModel.hasInvalidDataName ? .validationError : .clear) + } } Section { - TextField(String.descriptionString, text: $viewModel.description, axis: .vertical) + TextField(Strings.descriptionString, text: $viewModel.description, axis: .vertical) .lineLimit(10, reservesSpace: true) + .onChange(of: viewModel.description) { + viewModel.validateDescriptionLength() + } } header: { - Text(String.descriptionString) + Text(Strings.descriptionString) + } footer: { + VStack(alignment: .leading) { + Text(Strings.shopDescriptionHelp(maximumLength: viewModel.maximumDescriptionLength)) + .font(.uiFootnote) + Text(Strings.shopDescriptionIsInvalid) + .font(.uiFootnote) + .foregroundStyle(viewModel.hasInvalidDataDescription ? .validationError : .clear) + } } Section { - Picker(String.timeZone, selection: $viewModel.selectedTimeZone) { + Picker(Strings.timeZone, selection: $viewModel.selectedTimeZone) { ForEach(timeZones.keys, id: \.self) { key in Text(timeZones[key]!).tag(key) } @@ -69,13 +87,13 @@ private extension ShopBasicSettingsView { } } .padding() - .navigationTitle(String.shopSettingsBasicSettingsLabel) + .navigationTitle(Strings.shopSettingsBasicSettingsLabel) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button { viewModel.updateShop() } label: { - Text(String.save) + Text(Strings.save) } .disabled(viewModel.hasInvalidData) } diff --git a/NativeAppTemplate/UI/Shop Settings/ShopBasicSettingsViewModel.swift b/NativeAppTemplate/UI/Shop Settings/ShopBasicSettingsViewModel.swift index bba5758..2447c8b 100644 --- a/NativeAppTemplate/UI/Shop Settings/ShopBasicSettingsViewModel.swift +++ b/NativeAppTemplate/UI/Shop Settings/ShopBasicSettingsViewModel.swift @@ -13,7 +13,7 @@ final class ShopBasicSettingsViewModel { var isUpdating = false var name = "" var description = "" - var selectedTimeZone = String.defaultTimeZone + var selectedTimeZone = Strings.defaultTimeZone var shouldDismiss: Bool = false private(set) var shop: Shop? @@ -39,7 +39,11 @@ final class ShopBasicSettingsViewModel { } var hasInvalidData: Bool { - if Utility.isBlank(name) { + if hasInvalidDataName { + return true + } + + if hasInvalidDataDescription { return true } @@ -54,6 +58,36 @@ final class ShopBasicSettingsViewModel { return false } + var hasInvalidDataName: Bool { + if name.isBlank { + return true + } + if name.count > maximumNameLength { + return true + } + return false + } + + var hasInvalidDataDescription: Bool { + description.count > maximumDescriptionLength + } + + var maximumNameLength: Int { + NativeAppTemplateConstants.maximumShopNameLength + } + + var maximumDescriptionLength: Int { + NativeAppTemplateConstants.maximumShopDescriptionLength + } + + func validateNameLength() { + name = String(name.prefix(maximumNameLength)) + } + + func validateDescriptionLength() { + description = String(description.prefix(maximumDescriptionLength)) + } + func reload() { Task { @MainActor in isFetching = true @@ -91,7 +125,7 @@ final class ShopBasicSettingsViewModel { timeZone: selectedTimeZone ) _ = try await shopRepository.update(id: shop.id, shop: shop) - messageBus.post(message: Message(level: .success, message: .basicSettingsUpdated)) + messageBus.post(message: Message(level: .success, message: Strings.basicSettingsUpdated)) } catch { messageBus.post(message: Message(error: error)) } diff --git a/NativeAppTemplate/UI/Shop Settings/ShopSettingsView.swift b/NativeAppTemplate/UI/Shop Settings/ShopSettingsView.swift index 5745021..8cf5214 100644 --- a/NativeAppTemplate/UI/Shop Settings/ShopSettingsView.swift +++ b/NativeAppTemplate/UI/Shop Settings/ShopSettingsView.swift @@ -47,7 +47,7 @@ private extension ShopSettingsView { return contentView } - func shopSettingsView(shop: Shop) -> some View { // swiftlint:disable:this function_body_length + func shopSettingsView(shop: Shop) -> some View { VStack { Text(shop.name) .font(.uiTitle1) @@ -66,7 +66,7 @@ private extension ShopSettingsView { ) ) } label: { - Label(String.shopSettingsBasicSettingsLabel, systemImage: "storefront") + Label(Strings.shopSettingsBasicSettingsLabel, systemImage: "storefront") } .listRowBackground(Color.cardBackground.opacity(0.7)) } @@ -82,39 +82,13 @@ private extension ShopSettingsView { ) ) } label: { - Label(String.shopSettingsManageItemTagsLabel, systemImage: "rectangle.stack") + Label(Strings.shopSettingsManageItemTagsLabel, systemImage: "rectangle.stack") } .listRowBackground(Color.cardBackground.opacity(0.7)) } Section { - NavigationLink { - NumberTagsWebpageListView( - viewModel: NumberTagsWebpageListViewModel( - shop: shop, - messageBus: messageBus - ) - ) - } label: { - Label(String.shopSettingsNumberTagsWebpageLabel, systemImage: "globe") - } - } - .listRowBackground(Color.cardBackground.opacity(0.7)) - - Section { - VStack(spacing: NativeAppTemplateConstants.Spacing.xxs) { - MainButtonView(title: String.resetNumberTags, type: .destructive(withArrow: false)) { - viewModel.isShowingResetConfirmationDialog = true - } - .listRowBackground(Color.clear) - Text(String.resetNumberTagsDescription) - .font(.uiFootnote) - .foregroundStyle(.contentText) - .listRowBackground(Color.clear) - } - .listRowBackground(Color.clear) - - MainButtonView(title: String.deleteShop, type: .destructive(withArrow: false)) { + MainButtonView(title: Strings.deleteShop, type: .destructive(withArrow: false)) { viewModel.isShowingDeleteConfirmationDialog = true } .listRowBackground(Color.clear) @@ -127,32 +101,19 @@ private extension ShopSettingsView { reload() } } - .navigationTitle(String.shopSettingsLabel) - .alert( - String.resetNumberTags, - isPresented: $viewModel.isShowingResetConfirmationDialog - ) { - Button(String.resetNumberTags, role: .destructive) { - viewModel.resetShop() - } - Button(String.cancel, role: .cancel) { - viewModel.isShowingResetConfirmationDialog = false - } - } message: { - Text(String.areYouSure) - } + .navigationTitle(Strings.shopSettingsLabel) .alert( - String.deleteShop, + Strings.deleteShop, isPresented: $viewModel.isShowingDeleteConfirmationDialog ) { - Button(String.deleteShop, role: .destructive) { + Button(Strings.deleteShop, role: .destructive) { viewModel.destroyShop() } - Button(String.cancel, role: .cancel) { + Button(Strings.cancel, role: .cancel) { viewModel.isShowingDeleteConfirmationDialog = false } } message: { - Text(String.areYouSure) + Text(Strings.areYouSure) } } diff --git a/NativeAppTemplate/UI/Shop Settings/ShopSettingsViewModel.swift b/NativeAppTemplate/UI/Shop Settings/ShopSettingsViewModel.swift index e3f8ab5..c5eea20 100644 --- a/NativeAppTemplate/UI/Shop Settings/ShopSettingsViewModel.swift +++ b/NativeAppTemplate/UI/Shop Settings/ShopSettingsViewModel.swift @@ -10,9 +10,7 @@ import SwiftUI @MainActor final class ShopSettingsViewModel { var isFetching = true - var isResetting = false var isDeleting = false - var isShowingResetConfirmationDialog = false var isShowingDeleteConfirmationDialog = false var shouldDismiss: Bool = false private(set) var shop: Shop? @@ -38,7 +36,7 @@ final class ShopSettingsViewModel { } var isBusy: Bool { - isFetching || isResetting || isDeleting + isFetching || isDeleting } func reload() { @@ -54,25 +52,6 @@ final class ShopSettingsViewModel { } } - func resetShop() { - guard let shop else { return } - - Task { - isResetting = true - do { - try await shopRepository.reset(id: shop.id) - messageBus.post(message: .init(level: .success, message: .shopReset)) - } catch { - messageBus.post(message: .init( - level: .error, - message: "\(String.shopResetError) \(error.codedDescription)", - autoDismiss: false - )) - } - shouldDismiss = true - } - } - func destroyShop() { guard let shop else { return } @@ -80,12 +59,12 @@ final class ShopSettingsViewModel { isDeleting = true do { try await shopRepository.destroy(id: shop.id) - messageBus.post(message: .init(level: .success, message: .shopDeleted)) + messageBus.post(message: .init(level: .success, message: Strings.shopDeleted)) sessionController.shouldPopToRootView = true } catch { messageBus.post(message: .init( level: .error, - message: "\(String.shopDeletedError) \(error.codedDescription)", + message: "\(Strings.shopDeletedError) \(error.codedDescription)", autoDismiss: false )) try await sessionController.logout() diff --git a/NativeAppTemplate/Utilities/ImageSaver.swift b/NativeAppTemplate/Utilities/ImageSaver.swift deleted file mode 100644 index dcedcb1..0000000 --- a/NativeAppTemplate/Utilities/ImageSaver.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// ImageSaver.swift -// NativeAppTemplate -// - -import UIKit - -class ImageSaver: NSObject { - private var completion: (_ error: Error?) -> Void = { _ in } - - func save(image: UIImage, completion: @escaping (_ error: Error?) -> Void) { - self.completion = completion - UIImageWriteToSavedPhotosAlbum(image, self, #selector(image(_:didFinishSavingWithError:contextInfo:)), nil) - } - - @objc - private func image(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) { - completion(error) - } -} diff --git a/NativeAppTemplate/Utilities/QRCodeGenerator.swift b/NativeAppTemplate/Utilities/QRCodeGenerator.swift deleted file mode 100644 index fc852de..0000000 --- a/NativeAppTemplate/Utilities/QRCodeGenerator.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// QRCodeGenerator.swift -// NativeAppTemplate -// - -import SwiftUI - -struct QRCodeGenerator { - func generate(inputText: String, scale: CGFloat = 2, centerImage: UIImage?) -> UIImage? { - guard let qrFilter = CIFilter(name: "CIQRCodeGenerator") - else { return nil } - - let inputData = inputText.data(using: .utf8) - qrFilter.setValue(inputData, forKey: "inputMessage") - qrFilter.setValue("H", forKey: "inputCorrectionLevel") - - guard let ciImage = qrFilter.outputImage - else { return nil } - - let sizeTransform = CGAffineTransform(scaleX: scale, y: scale) - let scaledCiImage = ciImage.transformed(by: sizeTransform) - - let context = CIContext() - guard let cgImage = context.createCGImage(scaledCiImage, from: scaledCiImage.extent) - else { return nil } - - if let centerImage { - return UIImage(cgImage: cgImage).composited(withSmallCenterImage: centerImage) - } else { - return UIImage(cgImage: cgImage) - } - } - - func generateWithCenterText(inputText: String, scale: CGFloat = 2, centerText: String) -> UIImage? { - if let centerImage = centerText.image( - withAttributes: [ - .font: UIFont.systemFont(ofSize: 40.0), - .backgroundColor: UIColor(Color.arrowBackground) - ] - ) { - generate(inputText: inputText, scale: scale, centerImage: centerImage) - } else { - generate(inputText: inputText, scale: scale, centerImage: nil) - } - } -} diff --git a/NativeAppTemplate/Utilities/Utility.swift b/NativeAppTemplate/Utilities/Utility.swift index 3f20488..89b3c82 100644 --- a/NativeAppTemplate/Utilities/Utility.swift +++ b/NativeAppTemplate/Utilities/Utility.swift @@ -3,26 +3,12 @@ // NativeAppTemplate // -import CoreNFC import Foundation import os enum Utility { - static func scanUrl(itemTagId: String, itemTagType: String) -> URL { - let path = itemTagType == "server" ? String.scanPath : String.scanPathCustomer - let pathURL = NativeAppTemplateEnvironment.prod.baseURL.appendingPathComponent(path) - var urlComponent = URLComponents(url: pathURL, resolvingAgainstBaseURL: true) - - urlComponent?.queryItems = [ - URLQueryItem(name: "item_tag_id", value: itemTagId), - URLQueryItem(name: "type", value: itemTagType) - ] - - return (urlComponent?.url)! - } - static func currentTimeZone() -> String { - let defaultTimeZone = String.defaultTimeZone + let defaultTimeZone = Strings.defaultTimeZone let timeZoneHourFormatted = currentTimeZoneHourFormatted() let timeZoneArray = TimeZone.current.identifier.components(separatedBy: "/") @@ -51,62 +37,6 @@ enum Utility { return defaultTimeZone } - static func extractItemTagInfoFrom(message: NFCNDEFMessage, test: Bool = false) -> ItemTagInfoFromNdefMessage { - var itemTagInfo = ItemTagInfoFromNdefMessage() - - let urls: [URLComponents] = message.records.compactMap { (payload: NFCNDEFPayload) -> URLComponents? in - // Search for URL record with matching domain host and scheme. - if let url = payload.wellKnownTypeURIPayload() { - let components = URLComponents(url: url, resolvingAgainstBaseURL: false) - if components?.host == String.domain, components?.scheme == String.scheme { - return components - } - } - return nil - } - - guard urls.count == 1, - let items = urls.first?.queryItems else { - return itemTagInfo - } - - for item in items { - switch item.name { - case "item_tag_id": - if let itemTagId = item.value { - itemTagInfo.id = itemTagId - } - appLogger.debug("item_tag_id: \(String(describing: itemTagInfo.id), privacy: .private)") - case "type": - if let type = item.value { - itemTagInfo.type = type - } - appLogger.debug("type: \(String(describing: itemTagInfo.type), privacy: .private)") - default: - break - } - } - - if test { - if itemTagInfo.id.isEmpty || itemTagInfo.type.isEmpty { - } else if itemTagInfo.type != ItemTagType.customer.rawValue, - itemTagInfo.type != ItemTagType.server.rawValue { - } else { - itemTagInfo.success = true - } - } else { - if itemTagInfo.id.isEmpty || itemTagInfo.type.isEmpty { - } else if itemTagInfo.type == ItemTagType.customer.rawValue { - itemTagInfo.message = .scanServerTag - } else if itemTagInfo.type != ItemTagType.server.rawValue { - } else { - itemTagInfo.success = true - } - } - - return itemTagInfo - } - static var deviceModel: String { var utsnameInstance = utsname() uname(&utsnameInstance) @@ -116,16 +46,6 @@ enum Utility { return optionalString ?? "N/A" } - static func isBlank(_ text: String) -> Bool { - let trimmed = text.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) - return trimmed.isEmpty - } - - static func validateEmail(_ email: String) -> Bool { - let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" - return NSPredicate(format: "SELF MATCHES %@", emailRegex).evaluate(with: email) - } - private static func currentTimeZoneHour() -> (Int, Int) { let secondsFromGmt: Int = TimeZone.current.secondsFromGMT() let hoursFromGmt = (secondsFromGmt / 3_600) diff --git a/NativeAppTemplateTests/Data/ViewModels/TabViewModelTest.swift b/NativeAppTemplateTests/Data/ViewModels/TabViewModelTest.swift index 8b611a7..995f1b6 100644 --- a/NativeAppTemplateTests/Data/ViewModels/TabViewModelTest.swift +++ b/NativeAppTemplateTests/Data/ViewModels/TabViewModelTest.swift @@ -26,7 +26,6 @@ struct TabViewModelTest { viewModel.showingDetailView[.shops] = true #expect(viewModel.showingDetailView[.shops] == true) - #expect(viewModel.showingDetailView[.scan] == false) #expect(viewModel.showingDetailView[.settings] == false) viewModel.showingDetailView[.shops] = false diff --git a/NativeAppTemplateTests/Demo/Data/Repositories/DemoItemTagRepository.swift b/NativeAppTemplateTests/Demo/Data/Repositories/DemoItemTagRepository.swift index 6b55890..cb967fa 100644 --- a/NativeAppTemplateTests/Demo/Data/Repositories/DemoItemTagRepository.swift +++ b/NativeAppTemplateTests/Demo/Data/Repositories/DemoItemTagRepository.swift @@ -77,12 +77,10 @@ final class DemoItemTagRepository: ItemTagRepositoryProtocol { return itemTag } - func reset(id: String) async throws -> ItemTag { + func idle(id: String) async throws -> ItemTag { var itemTag = itemTags.first { $0.id == id }! itemTag.state = .idled - itemTag.scanState = .unscanned itemTag.completedAt = nil - itemTag.customerReadAt = nil let index = itemTags.firstIndex { $0.id == id }! itemTags[index] = itemTag @@ -92,13 +90,13 @@ final class DemoItemTagRepository: ItemTagRepositoryProtocol { private func fetchAll() -> [ItemTag] { [ - mockItemTag(id: "1", shopId: "1", queueNumber: "A001"), - mockItemTag(id: "2", shopId: "1", queueNumber: "A002"), - mockItemTag(id: "3", shopId: "1", queueNumber: "A003"), - mockItemTag(id: "4", shopId: "2", queueNumber: "A001"), - mockItemTag(id: "5", shopId: "2", queueNumber: "A002"), - mockItemTag(id: "6", shopId: "2", queueNumber: "A003"), - mockItemTag(id: "7", shopId: "2", queueNumber: "A004") + mockItemTag(id: "1", shopId: "1", name: "A001"), + mockItemTag(id: "2", shopId: "1", name: "A002"), + mockItemTag(id: "3", shopId: "1", name: "A003"), + mockItemTag(id: "4", shopId: "2", name: "A001"), + mockItemTag(id: "5", shopId: "2", name: "A002"), + mockItemTag(id: "6", shopId: "2", name: "A003"), + mockItemTag(id: "7", shopId: "2", name: "A004") ] } @@ -107,19 +105,18 @@ final class DemoItemTagRepository: ItemTagRepositoryProtocol { private func mockItemTag( id: String = UUID().uuidString, shopId: String = UUID().uuidString, - queueNumber: String = "Mock ItemTag" + name: String = "Mock ItemTag" ) -> ItemTag { ItemTag( id: id, shopId: shopId, - queueNumber: queueNumber, + name: name, + description: "", + position: 1, state: .idled, - scanState: .unscanned, createdAt: .now, - customerReadAt: nil, completedAt: nil, - shopName: "Mock ItemTag", - alreadyCompleted: false + shopName: "Mock ItemTag" ) } } diff --git a/NativeAppTemplateTests/Demo/Data/Repositories/DemoItemTagRepositoryTest.swift b/NativeAppTemplateTests/Demo/Data/Repositories/DemoItemTagRepositoryTest.swift index 94c6f70..ec21dc8 100644 --- a/NativeAppTemplateTests/Demo/Data/Repositories/DemoItemTagRepositoryTest.swift +++ b/NativeAppTemplateTests/Demo/Data/Repositories/DemoItemTagRepositoryTest.swift @@ -17,7 +17,7 @@ struct DemoItemTagRepositoryTest { repository.reload(shopId: "1") let itemTags = repository.findBy(id: "1") - #expect(itemTags.queueNumber == "A001") + #expect(itemTags.name == "A001") } @Test @@ -40,7 +40,7 @@ struct DemoItemTagRepositoryTest { repository.reload(shopId: "1") let itemTag = try await repository.fetchDetail(id: "1") - #expect(itemTag.queueNumber == "A001") + #expect(itemTag.name == "A001") } @Test @@ -48,21 +48,20 @@ struct DemoItemTagRepositoryTest { let shopId = "1" repository.reload(shopId: shopId) - let newQueueNumber = "A099" + let newName = "A099" let newItemTag = ItemTag( shopId: shopId, - queueNumber: newQueueNumber, + name: newName, + description: "", + position: 1, state: .idled, - scanState: .unscanned, createdAt: .now, - customerReadAt: nil, completedAt: nil, - shopName: "Mock ItemTag", - alreadyCompleted: false + shopName: "Mock ItemTag" ) let createdItemTag = try await repository.create(shopId: shopId, itemTag: newItemTag) - #expect(createdItemTag.queueNumber == newQueueNumber) + #expect(createdItemTag.name == newName) #expect(repository.itemTags.count == 4) } @@ -71,10 +70,10 @@ struct DemoItemTagRepositoryTest { repository.reload(shopId: "1") var itemTag = repository.findBy(id: "1") - let newQueueNumber = "B001" - itemTag.queueNumber = newQueueNumber + let newName = "B001" + itemTag.name = newName let updatedItemTag = try await repository.update(id: "1", itemTag: itemTag) - #expect(updatedItemTag.queueNumber == newQueueNumber) + #expect(updatedItemTag.name == newName) } @Test @@ -99,21 +98,17 @@ struct DemoItemTagRepositoryTest { } @Test - func reset() async throws { + func idle() async throws { repository.reload(shopId: "1") var itemTag = repository.findBy(id: "1") itemTag.state = .completed - itemTag.scanState = .scanned itemTag.completedAt = .now - itemTag.customerReadAt = .now _ = try await repository.update(id: "1", itemTag: itemTag) - let resetItemTag = try await repository.reset(id: "1") - #expect(resetItemTag.state == .idled) - #expect(resetItemTag.scanState == .unscanned) - #expect(resetItemTag.completedAt == nil) - #expect(resetItemTag.customerReadAt == nil) + let idledItemTag = try await repository.idle(id: "1") + #expect(idledItemTag.state == .idled) + #expect(idledItemTag.completedAt == nil) } } } diff --git a/NativeAppTemplateTests/Demo/Data/Repositories/DemoOnboardingRepository.swift b/NativeAppTemplateTests/Demo/Data/Repositories/DemoOnboardingRepository.swift index 3f4c4e3..23dffa0 100644 --- a/NativeAppTemplateTests/Demo/Data/Repositories/DemoOnboardingRepository.swift +++ b/NativeAppTemplateTests/Demo/Data/Repositories/DemoOnboardingRepository.swift @@ -5,39 +5,36 @@ import Foundation @testable import NativeAppTemplate -import OrderedCollections @MainActor final class DemoOnboardingRepository: OnboardingRepositoryProtocol { var onboardings: [Onboarding] = [] - var onboardingsDictionary: OrderedDictionary { - var dict = OrderedDictionary() - for onboarding in onboardings { - dict[onboarding.id] = onboarding.isPortraitImage - } - return dict + + init() { + setupMockOnboardings() } - func reload() { - // Demo data with predefined onboarding items - let demoOnboardingData: OrderedDictionary = [ - 1: false, // Landscape image - 2: false, // Landscape image - 3: false, // Landscape image - 4: true, // Portrait image - 5: false, // Landscape image - 6: false, // Landscape image - 7: true, // Portrait image - 8: true, // Portrait image - 9: false, // Landscape image - 10: false, // Landscape image - 11: true, // Portrait image - 12: false, // Landscape image - 13: false // Landscape image - ] + // MARK: - Test Helpers - onboardings = demoOnboardingData.map { key, value in - Onboarding(id: key, isPortraitImage: value) - } + func resetState() { + setupMockOnboardings() + } + + func addOnboarding(_ onboarding: Onboarding) { + onboardings.append(onboarding) + } + + func clearOnboardings() { + onboardings.removeAll() + } + + private func setupMockOnboardings() { + onboardings = [ + Onboarding(id: 1, imageOrientation: .portrait), + Onboarding(id: 2, imageOrientation: .landscape), + Onboarding(id: 3, imageOrientation: .portrait), + Onboarding(id: 4, imageOrientation: .landscape), + Onboarding(id: 5, imageOrientation: .portrait) + ] } } diff --git a/NativeAppTemplateTests/Demo/Data/Repositories/DemoOnboardingRepositoryTest.swift b/NativeAppTemplateTests/Demo/Data/Repositories/DemoOnboardingRepositoryTest.swift index d6f8943..0d0a349 100644 --- a/NativeAppTemplateTests/Demo/Data/Repositories/DemoOnboardingRepositoryTest.swift +++ b/NativeAppTemplateTests/Demo/Data/Repositories/DemoOnboardingRepositoryTest.swift @@ -13,70 +13,86 @@ struct DemoOnboardingRepositoryTest { let repository = DemoOnboardingRepository() @Test - func reload() { - repository.reload() + func initialSetup() { + repository.resetState() - #expect(repository.onboardings.count == 13) - #expect(!repository.onboardings.isEmpty) + #expect(repository.onboardings.count == 5) } @Test - func onboardingsDictionary() { - repository.reload() - - let dictionary = repository.onboardingsDictionary - #expect(dictionary.count == 13) - // Test specific values from the demo data - #expect(dictionary[1] == false) // Landscape - #expect(dictionary[4] == true) // Portrait - #expect(dictionary[7] == true) // Portrait - #expect(dictionary[8] == true) // Portrait - #expect(dictionary[11] == true) // Portrait - #expect(dictionary[13] == false) // Landscape + func onboardingProperties() throws { + repository.resetState() + + let onboarding1 = try #require(repository.onboardings.first { $0.id == 1 }) + #expect(onboarding1.imageOrientation == .portrait) + + let onboarding2 = try #require(repository.onboardings.first { $0.id == 2 }) + #expect(onboarding2.imageOrientation == .landscape) + + let onboarding3 = try #require(repository.onboardings.first { $0.id == 3 }) + #expect(onboarding3.imageOrientation == .portrait) } @Test - func onboardingProperties() { - repository.reload() + func addOnboarding() throws { + repository.resetState() + + let newOnboarding = Onboarding(id: 99, imageOrientation: .portrait) + repository.addOnboarding(newOnboarding) - let firstOnboarding = repository.onboardings.first { $0.id == 1 } - #expect(firstOnboarding != nil) - #expect(firstOnboarding?.isPortraitImage == false) + #expect(repository.onboardings.count == 6) - let portraitOnboarding = repository.onboardings.first { $0.id == 4 } - #expect(portraitOnboarding != nil) - #expect(portraitOnboarding?.isPortraitImage == true) + let addedOnboarding = try #require(repository.onboardings.first { $0.id == 99 }) + #expect(addedOnboarding.imageOrientation == .portrait) } @Test - func onboardingIds() { - repository.reload() + func clearOnboardings() { + repository.resetState() - let ids = repository.onboardings.map(\.id).sorted() - let expectedIds = Array(1...13) - #expect(ids == expectedIds) + #expect(repository.onboardings.count == 5) + + repository.clearOnboardings() + + #expect(repository.onboardings.isEmpty) } @Test - func portraitImageCounts() { - repository.reload() + func onboardingOrdering() { + repository.resetState() + + let ids = repository.onboardings.map(\.id) + #expect(ids == [1, 2, 3, 4, 5]) - let portraitCount = repository.onboardings.count(where: { $0.isPortraitImage }) - let landscapeCount = repository.onboardings.count(where: { !$0.isPortraitImage }) + let newOnboarding = Onboarding(id: 0, imageOrientation: .portrait) + repository.addOnboarding(newOnboarding) - #expect(portraitCount == 4) // IDs: 4, 7, 8, 11 - #expect(landscapeCount == 9) // All others - #expect(portraitCount + landscapeCount == 13) + let updatedIds = repository.onboardings.map(\.id) + #expect(updatedIds == [1, 2, 3, 4, 5, 0]) } @Test - func dictionaryConsistency() { - repository.reload() + func onboardingIdentifiability() throws { + repository.resetState() + + let ids = repository.onboardings.map(\.id) + let uniqueIds = Set(ids) + #expect(ids.count == uniqueIds.count) + } + + @Test + func onboardingHashability() throws { + repository.resetState() + + let onboarding1 = try #require(repository.onboardings.first { $0.id == 1 }) + let onboarding2 = try #require(repository.onboardings.first { $0.id == 1 }) + let onboarding3 = try #require(repository.onboardings.first { $0.id == 2 }) + + #expect(onboarding1 == onboarding2) + #expect(onboarding1 != onboarding3) - // Verify that the dictionary computed property matches the onboardings array - for onboarding in repository.onboardings { - #expect(repository.onboardingsDictionary[onboarding.id] == onboarding.isPortraitImage) - } + let onboardingSet: Set = [onboarding1, onboarding2, onboarding3] + #expect(onboardingSet.count == 2) } } } diff --git a/NativeAppTemplateTests/Demo/Data/Repositories/DemoShopRepository.swift b/NativeAppTemplateTests/Demo/Data/Repositories/DemoShopRepository.swift index 80754d6..9ba807e 100644 --- a/NativeAppTemplateTests/Demo/Data/Repositories/DemoShopRepository.swift +++ b/NativeAppTemplateTests/Demo/Data/Repositories/DemoShopRepository.swift @@ -67,9 +67,7 @@ final class DemoShopRepository: ShopRepositoryProtocol { description: "This is a mock shop for testing", timeZone: "Tokyo", itemTagsCount: 10, - scannedItemTagsCount: 5, - completedItemTagsCount: 3, - displayShopServerPath: "https://api.nativeapptemplate.com/display/shops/\(id)?type=server" + completedItemTagsCount: 3 ) } } diff --git a/NativeAppTemplateTests/Demo/Data/Repositories/DemoShopRepositoryTest.swift b/NativeAppTemplateTests/Demo/Data/Repositories/DemoShopRepositoryTest.swift index ca57f93..ea190c7 100644 --- a/NativeAppTemplateTests/Demo/Data/Repositories/DemoShopRepositoryTest.swift +++ b/NativeAppTemplateTests/Demo/Data/Repositories/DemoShopRepositoryTest.swift @@ -47,9 +47,7 @@ struct DemoShopRepositoryTest { description: "A new shop", timeZone: "Tokyo", itemTagsCount: 0, - scannedItemTagsCount: 0, - completedItemTagsCount: 0, - displayShopServerPath: "https://api.nativeapptemplate.com/display/shops/99?type=server" + completedItemTagsCount: 0 ) let createdShop = try await repository.create(shop: newShop) diff --git a/NativeAppTemplateTests/Extensions/DateExtensionsTest.swift b/NativeAppTemplateTests/Extensions/DateExtensionsTest.swift new file mode 100644 index 0000000..9168702 --- /dev/null +++ b/NativeAppTemplateTests/Extensions/DateExtensionsTest.swift @@ -0,0 +1,58 @@ +// +// DateExtensionsTest.swift +// NativeAppTemplate +// + +import Foundation +@testable import NativeAppTemplate +import Testing + +struct DateExtensionsTest { + private func date(_ iso: String) throws -> Date { + try #require(iso.iso8601) + } + + @Test + func dateByAddingNumberOfSecondsAddsPositiveSeconds() throws { + let date = try date("2026-01-01T00:00:00.000Z") + let later = date.dateByAddingNumberOfSeconds(3600) + #expect(later.timeIntervalSince(date) == 3600) + } + + @Test + func dateByAddingNumberOfSecondsAcceptsNegative() throws { + let date = try date("2026-01-01T00:00:00.000Z") + let earlier = date.dateByAddingNumberOfSeconds(-60) + #expect(earlier.timeIntervalSince(date) == -60) + } + + @Test + func cardDateStringFormatsAsYearMonthDay() throws { + let date = try date("2026-04-29T12:34:56.000Z") + let formatted = date.cardDateString + // Output is locale-fixed (en_US_POSIX) but uses the device's current + // time zone, so we just assert the format shape. + #expect(formatted.count == 10) + #expect(formatted.contains("/")) + let parts = formatted.split(separator: "/") + #expect(parts.count == 3) + #expect(parts[0].count == 4) // year + #expect(parts[1].count == 2) // month + #expect(parts[2].count == 2) // day + } + + @Test + func cardTimeStringFormatsAsHourMinute() throws { + let date = try date("2026-04-29T12:34:56.000Z") + let formatted = date.cardTimeString + #expect(formatted.count == 5) + #expect(formatted[formatted.index(formatted.startIndex, offsetBy: 2)] == ":") + } + + @Test + func cardDateTimeStringConcatenatesDateAndTime() throws { + let date = try date("2026-04-29T12:34:56.000Z") + let combined = date.cardDateTimeString + #expect(combined == "\(date.cardDateString) \(date.cardTimeString)") + } +} diff --git a/NativeAppTemplateTests/Extensions/StringExtensionsTest.swift b/NativeAppTemplateTests/Extensions/StringExtensionsTest.swift index adfcf1b..efe3481 100644 --- a/NativeAppTemplateTests/Extensions/StringExtensionsTest.swift +++ b/NativeAppTemplateTests/Extensions/StringExtensionsTest.swift @@ -3,36 +3,33 @@ // NativeAppTemplate // -import CoreGraphics @testable import NativeAppTemplate import Testing struct StringExtensionsTest { @Test(arguments: [ - ("hello123", true), - ("ABC", true), - ("abc", true), - ("123", true), - ("Hello World", false), - ("hello!", false), - ("hello@world", false), - ("", false) + ("", true), + (" ", true), + ("\n", true), + (" \t\n ", true), + ("hello", false), + (" hello ", false), ]) - func isAlphanumeric(input: String, expected: Bool) { - #expect(input.isAlphanumeric() == expected) + func isBlank(text: String, expected: Bool) { + #expect(text.isBlank == expected) } - @Test - func isAlphanumericIgnoringDiacritics() { - #expect("hello123".isAlphanumeric(ignoreDiacritics: true) == true) - #expect("ABC123".isAlphanumeric(ignoreDiacritics: true) == true) - #expect("hello world".isAlphanumeric(ignoreDiacritics: true) == false) - #expect("".isAlphanumeric(ignoreDiacritics: true) == false) - } - - @Test - func imageGeneration() { - let image = "A".image(size: CGSize(width: 50, height: 50)) - #expect(image != nil) + @Test(arguments: [ + ("test@example.com", true), + ("user.name+tag@domain.co", true), + ("user@domain.com", true), + ("", false), + ("notanemail", false), + ("@domain.com", false), + ("user@", false), + ("user@.com", false), + ]) + func isValidEmail(email: String, expected: Bool) { + #expect(email.isValidEmail == expected) } } diff --git a/NativeAppTemplateTests/Models/ItemTagTest.swift b/NativeAppTemplateTests/Models/ItemTagTest.swift new file mode 100644 index 0000000..b250a13 --- /dev/null +++ b/NativeAppTemplateTests/Models/ItemTagTest.swift @@ -0,0 +1,50 @@ +// +// ItemTagTest.swift +// NativeAppTemplate +// + +@testable import NativeAppTemplate +import Testing + +struct ItemTagTest { + @Test + func toJsonWrapsFieldsUnderItemTagKey() throws { + var itemTag = ItemTag() + itemTag.name = "Table 1" + itemTag.description = "Window seat" + + let json = itemTag.toJson() + let inner = try #require(json["item_tag"] as? [String: Any]) + + #expect(inner["name"] as? String == "Table 1") + #expect(inner["description"] as? String == "Window seat") + } + + @Test + func toJsonOnlyIncludesNameAndDescription() throws { + var itemTag = ItemTag() + itemTag.id = "abc-123" + itemTag.shopId = "shop-1" + itemTag.name = "n" + itemTag.description = "d" + itemTag.position = 5 + itemTag.shopName = "Cafe" + + let json = itemTag.toJson() + let inner = try #require(json["item_tag"] as? [String: Any]) + + #expect(inner.keys.sorted() == ["description", "name"]) + } + + @Test + func defaultsAreEmptyAndIdled() { + let itemTag = ItemTag() + #expect(itemTag.id.isEmpty) + #expect(itemTag.shopId.isEmpty) + #expect(itemTag.name.isEmpty) + #expect(itemTag.description.isEmpty) + #expect(itemTag.position == 0) + #expect(itemTag.state == .idled) + #expect(itemTag.completedAt == nil) + } +} diff --git a/NativeAppTemplateTests/Models/ItemTagTypeTest.swift b/NativeAppTemplateTests/Models/ItemTagTypeTest.swift deleted file mode 100644 index a367d44..0000000 --- a/NativeAppTemplateTests/Models/ItemTagTypeTest.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// ItemTagTypeTest.swift -// NativeAppTemplate -// - -@testable import NativeAppTemplate -import Testing - -struct ItemTagTypeTest { - @Test - func initFromValidStrings() { - #expect(ItemTagType(string: "server") == .server) - #expect(ItemTagType(string: "customer") == .customer) - } - - @Test - func initFromUnknownStringDefaultsToServer() { - #expect(ItemTagType(string: "unknown") == .server) - #expect(ItemTagType(string: "") == .server) - } - - @Test - func toJsonRoundtrip() { - #expect(ItemTagType(string: ItemTagType.server.toJson()) == .server) - #expect(ItemTagType(string: ItemTagType.customer.toJson()) == .customer) - } -} diff --git a/NativeAppTemplateTests/Models/ScanStateTest.swift b/NativeAppTemplateTests/Models/ScanStateTest.swift deleted file mode 100644 index bac41f8..0000000 --- a/NativeAppTemplateTests/Models/ScanStateTest.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// ScanStateTest.swift -// NativeAppTemplate -// - -@testable import NativeAppTemplate -import Testing - -struct ScanStateTest { - @Test - func initFromValidStrings() { - #expect(ScanState(string: "unscanned") == .unscanned) - #expect(ScanState(string: "scanned") == .scanned) - } - - @Test - func initFromUnknownStringDefaultsToUnscanned() { - #expect(ScanState(string: "unknown") == .unscanned) - #expect(ScanState(string: "") == .unscanned) - } - - @Test - func toJsonRoundtrip() { - #expect(ScanState(string: ScanState.unscanned.toJson()) == .unscanned) - #expect(ScanState(string: ScanState.scanned.toJson()) == .scanned) - } -} diff --git a/NativeAppTemplateTests/Models/SendConfirmationTest.swift b/NativeAppTemplateTests/Models/SendConfirmationTest.swift new file mode 100644 index 0000000..e0f9f0e --- /dev/null +++ b/NativeAppTemplateTests/Models/SendConfirmationTest.swift @@ -0,0 +1,32 @@ +// +// SendConfirmationTest.swift +// NativeAppTemplate +// + +@testable import NativeAppTemplate +import Testing + +struct SendConfirmationTest { + @Test + func toJsonIncludesEmailAndRedirectUrl() { + let send = SendConfirmation(email: "alice@example.com") + + let json = send.toJson() + + #expect(json["email"] as? String == "alice@example.com") + #expect(json["redirect_url"] as? String == send.redirectUrl) + #expect(json.keys.sorted() == ["email", "redirect_url"]) + } + + @Test + func defaultRedirectUrlPointsToConfirmationResult() { + let send = SendConfirmation(email: "x") + #expect(send.redirectUrl.hasSuffix("/shopkeeper_auth/confirmation_result")) + } + + @Test + func redirectUrlCanBeOverridden() { + let send = SendConfirmation(email: "x", redirectUrl: "https://custom.example/cb") + #expect(send.toJson()["redirect_url"] as? String == "https://custom.example/cb") + } +} diff --git a/NativeAppTemplateTests/Models/SendResetPasswordTest.swift b/NativeAppTemplateTests/Models/SendResetPasswordTest.swift new file mode 100644 index 0000000..0d123b1 --- /dev/null +++ b/NativeAppTemplateTests/Models/SendResetPasswordTest.swift @@ -0,0 +1,32 @@ +// +// SendResetPasswordTest.swift +// NativeAppTemplate +// + +@testable import NativeAppTemplate +import Testing + +struct SendResetPasswordTest { + @Test + func toJsonIncludesEmailAndRedirectUrl() { + let send = SendResetPassword(email: "alice@example.com") + + let json = send.toJson() + + #expect(json["email"] as? String == "alice@example.com") + #expect(json["redirect_url"] as? String == send.redirectUrl) + #expect(json.keys.sorted() == ["email", "redirect_url"]) + } + + @Test + func defaultRedirectUrlPointsToResetPasswordEdit() { + let send = SendResetPassword(email: "x") + #expect(send.redirectUrl.hasSuffix("/shopkeeper_auth/reset_password/edit")) + } + + @Test + func redirectUrlCanBeOverridden() { + let send = SendResetPassword(email: "x", redirectUrl: "https://custom.example/cb") + #expect(send.toJson()["redirect_url"] as? String == "https://custom.example/cb") + } +} diff --git a/NativeAppTemplateTests/Models/ShopTest.swift b/NativeAppTemplateTests/Models/ShopTest.swift new file mode 100644 index 0000000..2503cce --- /dev/null +++ b/NativeAppTemplateTests/Models/ShopTest.swift @@ -0,0 +1,60 @@ +// +// ShopTest.swift +// NativeAppTemplate +// + +@testable import NativeAppTemplate +import Testing + +struct ShopTest { + private func makeShop() -> Shop { + Shop( + id: "shop-1", + name: "Cafe", + description: "A nice cafe", + timeZone: "Asia/Tokyo" + ) + } + + @Test + func toJsonForCreateWrapsFieldsUnderShopKey() throws { + let shop = makeShop() + + let json = shop.toJsonForCreate() + let inner = try #require(json["shop"] as? [String: Any]) + + #expect(inner["name"] as? String == "Cafe") + #expect(inner["description"] as? String == "A nice cafe") + #expect(inner["time_zone"] as? String == "Asia/Tokyo") + } + + @Test + func toJsonForCreateExcludesIdAndCounts() throws { + let shop = makeShop() + let inner = try #require(shop.toJsonForCreate()["shop"] as? [String: Any]) + + #expect(inner["id"] == nil) + #expect(inner["item_tags_count"] == nil) + #expect(inner["completed_item_tags_count"] == nil) + #expect(inner.keys.sorted() == ["description", "name", "time_zone"]) + } + + @Test + func toJsonForUpdateMatchesCreateShape() throws { + let shop = makeShop() + let create = try #require(shop.toJsonForCreate()["shop"] as? [String: Any]) + let update = try #require(shop.toJsonForUpdate()["shop"] as? [String: Any]) + + #expect(create.keys.sorted() == update.keys.sorted()) + #expect(create["name"] as? String == update["name"] as? String) + #expect(create["description"] as? String == update["description"] as? String) + #expect(create["time_zone"] as? String == update["time_zone"] as? String) + } + + @Test + func defaultCountsAreZero() { + let shop = makeShop() + #expect(shop.itemTagsCount == 0) + #expect(shop.completedItemTagsCount == 0) + } +} diff --git a/NativeAppTemplateTests/Models/SignUpTest.swift b/NativeAppTemplateTests/Models/SignUpTest.swift new file mode 100644 index 0000000..8879893 --- /dev/null +++ b/NativeAppTemplateTests/Models/SignUpTest.swift @@ -0,0 +1,52 @@ +// +// SignUpTest.swift +// NativeAppTemplate +// + +@testable import NativeAppTemplate +import Testing + +struct SignUpTest { + @Test + func toJsonForCreateIncludesPasswordAndDefaultsPlatformToIos() { + let signUp = SignUp( + name: "Alice", + email: "alice@example.com", + timeZone: "Asia/Tokyo", + password: "secret123" + ) + + let json = signUp.toJsonForCreate() + + #expect(json["name"] as? String == "Alice") + #expect(json["email"] as? String == "alice@example.com") + #expect(json["time_zone"] as? String == "Asia/Tokyo") + #expect(json["current_platform"] as? String == "ios") + #expect(json["password"] as? String == "secret123") + } + + @Test + func toJsonForUpdateExcludesPasswordAndPlatform() { + let signUp = SignUp( + name: "Alice", + email: "alice@example.com", + timeZone: "Asia/Tokyo", + password: "secret123" + ) + + let json = signUp.toJsonForUpdate() + + #expect(json["name"] as? String == "Alice") + #expect(json["email"] as? String == "alice@example.com") + #expect(json["time_zone"] as? String == "Asia/Tokyo") + #expect(json["password"] == nil) + #expect(json["current_platform"] == nil) + #expect(json.keys.sorted() == ["email", "name", "time_zone"]) + } + + @Test + func currentPlatformDefaultsToIos() { + let signUp = SignUp(name: "n", email: "e", timeZone: "tz") + #expect(signUp.currentPlatform == "ios") + } +} diff --git a/NativeAppTemplateTests/Models/UpdatePasswordTest.swift b/NativeAppTemplateTests/Models/UpdatePasswordTest.swift new file mode 100644 index 0000000..9851263 --- /dev/null +++ b/NativeAppTemplateTests/Models/UpdatePasswordTest.swift @@ -0,0 +1,37 @@ +// +// UpdatePasswordTest.swift +// NativeAppTemplate +// + +@testable import NativeAppTemplate +import Testing + +struct UpdatePasswordTest { + @Test + func toJsonWrapsFieldsUnderShopkeeperKey() throws { + let update = UpdatePassword( + currentPassword: "old-pw", + password: "new-pw", + passwordConfirmation: "new-pw" + ) + + let json = update.toJson() + let inner = try #require(json["shopkeeper"] as? [String: Any]) + + #expect(inner["current_password"] as? String == "old-pw") + #expect(inner["password"] as? String == "new-pw") + #expect(inner["password_confirmation"] as? String == "new-pw") + } + + @Test + func toJsonOnlyIncludesPasswordFields() throws { + let update = UpdatePassword( + currentPassword: "a", + password: "b", + passwordConfirmation: "c" + ) + let inner = try #require(update.toJson()["shopkeeper"] as? [String: Any]) + + #expect(inner.keys.sorted() == ["current_password", "password", "password_confirmation"]) + } +} diff --git a/NativeAppTemplateTests/Networking/Adapters/ItemTagAdapterTest.swift b/NativeAppTemplateTests/Networking/Adapters/ItemTagAdapterTest.swift index 3ff07c5..afa3839 100644 --- a/NativeAppTemplateTests/Networking/Adapters/ItemTagAdapterTest.swift +++ b/NativeAppTemplateTests/Networking/Adapters/ItemTagAdapterTest.swift @@ -12,14 +12,13 @@ struct ItemTagAdapterTest { "type": "item_tag", "attributes": [ "shop_id": "88705252-2FD2-4414-9E85-E6888033294A", - "queue_number": "A001", + "name": "A001", + "description": "", + "position": 1, "state": "idled", - "scan_state": "unscanned", "created_at": "2020-01-01T12:00:00.000Z", "shop_name": "Shop1", - "customer_read_at": "2020-01-02T12:00:00.000Z", - "completed_at": "2020-01-04T12:00:00.000Z", - "already_completed": false + "completed_at": "2020-01-04T12:00:00.000Z" ] ] @@ -66,4 +65,19 @@ struct ItemTagAdapterTest { return EntityAdapterError.invalidOrMissingAttributes == entityAdapterError } } + + @Test func missingPositionThrows() throws { + var sample = sampleResource + if var attributes = sample["attributes"] as? [String: Any] { + attributes.removeValue(forKey: "position") + sample["attributes"] = attributes + } + + let resource = try makeJsonAPIResource(for: sample) + + #expect { try ItemTagAdapter.process(resource: resource) } throws: { error in + let entityAdapterError = error as? EntityAdapterError + return EntityAdapterError.invalidOrMissingAttributes == entityAdapterError + } + } } diff --git a/NativeAppTemplateTests/Networking/Adapters/ShopAdapterTest.swift b/NativeAppTemplateTests/Networking/Adapters/ShopAdapterTest.swift index 23cf258..f119b36 100644 --- a/NativeAppTemplateTests/Networking/Adapters/ShopAdapterTest.swift +++ b/NativeAppTemplateTests/Networking/Adapters/ShopAdapterTest.swift @@ -14,7 +14,6 @@ struct ShopAdapterTest { "name": "Shop1", "description": "This is a Shop1", "time_zone": "Tokyo", - "display_shop_server_path": "https://api.nativeapptemplate.com/display/shops/1ed7ea32-65d5-4e64-97a0-0e00b6cee8c3?type=server", // swiftlint:disable:this line_length "item_tags_count": 10, "scanned_item_tags_count": 1, "completed_item_tags_count": 2 diff --git a/NativeAppTemplateTests/Testing/Repositories/TestItemTagRepository.swift b/NativeAppTemplateTests/Testing/Repositories/TestItemTagRepository.swift index ea3813b..6771622 100644 --- a/NativeAppTemplateTests/Testing/Repositories/TestItemTagRepository.swift +++ b/NativeAppTemplateTests/Testing/Repositories/TestItemTagRepository.swift @@ -119,18 +119,14 @@ final class TestItemTagRepository: ItemTagRepositoryProtocol { } var itemTag = findBy(id: id) - let wasAlreadyCompleted = itemTag.alreadyCompleted itemTag.state = .completed itemTag.completedAt = Date() _ = try await update(id: id, itemTag: itemTag) - // Preserve the alreadyCompleted flag for testing - itemTag.alreadyCompleted = wasAlreadyCompleted - return itemTag } - func reset(id: String) async throws -> ItemTag { + func idle(id: String) async throws -> ItemTag { guard error == nil else { state = .failed throw error! @@ -138,7 +134,6 @@ final class TestItemTagRepository: ItemTagRepositoryProtocol { var itemTag = findBy(id: id) itemTag.state = .idled - itemTag.scanState = .unscanned itemTag.completedAt = nil _ = try await update(id: id, itemTag: itemTag) diff --git a/NativeAppTemplateTests/Testing/Repositories/TestOnboardingRepository.swift b/NativeAppTemplateTests/Testing/Repositories/TestOnboardingRepository.swift index bf685aa..4cf7ff5 100644 --- a/NativeAppTemplateTests/Testing/Repositories/TestOnboardingRepository.swift +++ b/NativeAppTemplateTests/Testing/Repositories/TestOnboardingRepository.swift @@ -5,25 +5,10 @@ import Foundation @testable import NativeAppTemplate -import OrderedCollections @MainActor final class TestOnboardingRepository: OnboardingRepositoryProtocol { var onboardings: [Onboarding] = [] - var onboardingsDictionary: OrderedDictionary { - var dict = OrderedDictionary() - for onboarding in onboardings { - dict[onboarding.id] = onboarding.isPortraitImage - } - return dict - } - - /// A test-only - var reloadCalled = false - - func reload() { - reloadCalled = true - } /// A test-only func setOnboardings(onboardings: [Onboarding]) { diff --git a/NativeAppTemplateTests/Testing/Repositories/TestSessionController.swift b/NativeAppTemplateTests/Testing/Repositories/TestSessionController.swift index c8aec97..9a98161 100644 --- a/NativeAppTemplateTests/Testing/Repositories/TestSessionController.swift +++ b/NativeAppTemplateTests/Testing/Repositories/TestSessionController.swift @@ -15,17 +15,12 @@ import Foundation public var didFetchPermissions: Bool = false public var shouldPopToRootView: Bool = false - public var didBackgroundTagReading: Bool = false - - public var completeScanResult: CompleteScanResult = .init() - public var showTagInfoScanResult: ShowTagInfoScanResult = .init() public var shouldUpdateApp: Bool = false public var shouldUpdatePrivacy: Bool = false public var shouldUpdateTerms: Bool = false public var shouldThrowPrivacyError: Bool = false public var shouldThrowTermsError: Bool = false - public var maximumQueueNumberLength: Int = 4 public var shopLimitCount: Int = 1 public var shopkeeper: Shopkeeper? diff --git a/NativeAppTemplateTests/Testing/TestNFCManager.swift b/NativeAppTemplateTests/Testing/TestNFCManager.swift deleted file mode 100644 index 0c7b77e..0000000 --- a/NativeAppTemplateTests/Testing/TestNFCManager.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// TestNFCManager.swift -// NativeAppTemplate -// - -import CoreNFC -import Foundation -@testable import NativeAppTemplate - -final class TestNFCManager: NFCManagerProtocol, @unchecked Sendable { - var scanResult: Result? - var isScanResultChanged = false - var isScanResultChangedForTesting = false - - // Test control properties - var shouldSimulateSuccess = true - var simulatedItemTagData: ItemTagData? - var simulatedError: Error? - var readingStarted = false - var testingStarted = false - var writingStarted = false - - func startReading() async { - readingStarted = true - await simulateScanResult() - } - - func startReadingForTesting() async { - testingStarted = true - await simulateScanResultForTesting() - } - - func startWriting(ndefMessage: NFCNDEFMessage, isLock: Bool) async { - writingStarted = true - } - - /// Test helper methods - @MainActor func simulateScanResult() { - if shouldSimulateSuccess, let itemTagData = simulatedItemTagData { - scanResult = .success(itemTagData) - } else if let error = simulatedError { - scanResult = .failure(error) - } - isScanResultChanged = true - } - - @MainActor func simulateScanResultForTesting() { - if shouldSimulateSuccess, let itemTagData = simulatedItemTagData { - scanResult = .success(itemTagData) - } else if let error = simulatedError { - scanResult = .failure(error) - } - isScanResultChangedForTesting = true - } - - @MainActor func reset() { - scanResult = nil - isScanResultChanged = false - isScanResultChangedForTesting = false - readingStarted = false - testingStarted = false - writingStarted = false - shouldSimulateSuccess = true - simulatedItemTagData = nil - simulatedError = nil - } -} diff --git a/NativeAppTemplateTests/UI/App Root/ForgotPasswordViewModelTest.swift b/NativeAppTemplateTests/UI/App Root/ForgotPasswordViewModelTest.swift index 315bbaa..e8e775f 100644 --- a/NativeAppTemplateTests/UI/App Root/ForgotPasswordViewModelTest.swift +++ b/NativeAppTemplateTests/UI/App Root/ForgotPasswordViewModelTest.swift @@ -22,8 +22,8 @@ struct ForgotPasswordViewModelTest { let email = "" // Simulate the validation logic from the ViewModel - let isBlank = Utility.isBlank(email) - let isInvalid = !Utility.validateEmail(email) + let isBlank = email.isBlank + let isInvalid = !email.isValidEmail let hasInvalidData = isBlank || isInvalid #expect(hasInvalidData == true) @@ -33,8 +33,8 @@ struct ForgotPasswordViewModelTest { func hasInvalidDataWithInvalidEmail() { let email = "invalid-email" - let isBlank = Utility.isBlank(email) - let isInvalid = !Utility.validateEmail(email) + let isBlank = email.isBlank + let isInvalid = !email.isValidEmail let hasInvalidData = isBlank || isInvalid #expect(hasInvalidData == true) @@ -44,8 +44,8 @@ struct ForgotPasswordViewModelTest { func hasInvalidDataWithValidEmail() { let email = "valid@example.com" - let isBlank = Utility.isBlank(email) - let isInvalid = !Utility.validateEmail(email) + let isBlank = email.isBlank + let isInvalid = !email.isValidEmail let hasInvalidData = isBlank || isInvalid #expect(hasInvalidData == false) @@ -54,35 +54,35 @@ struct ForgotPasswordViewModelTest { @Test func isEmailBlankValidation() { // Test blank email detection - #expect(Utility.isBlank("") == true) - #expect(Utility.isBlank(" ") == true) - #expect(Utility.isBlank("test@example.com") == false) + #expect("".isBlank == true) + #expect(" ".isBlank == true) + #expect("test@example.com".isBlank == false) } @Test func isEmailInvalidValidation() { // Test email format validation - #expect(Utility.validateEmail("") == false) - #expect(Utility.validateEmail("invalid") == false) - #expect(Utility.validateEmail("invalid@") == false) - #expect(Utility.validateEmail("@invalid.com") == false) - #expect(Utility.validateEmail("valid@example.com") == true) - #expect(Utility.validateEmail("user+tag@domain.org") == true) + #expect("".isValidEmail == false) + #expect("invalid".isValidEmail == false) + #expect("invalid@".isValidEmail == false) + #expect("@invalid.com".isValidEmail == false) + #expect("valid@example.com".isValidEmail == true) + #expect("user+tag@domain.org".isValidEmail == true) } @Test func emailValidationEdgeCases() { // Test various email formats - #expect(Utility.validateEmail("user.name@domain.com") == true) - #expect(Utility.validateEmail("user+tag@domain.co.uk") == true) - #expect(Utility.validateEmail("user@subdomain.domain.org") == true) - #expect(Utility.validateEmail("123@domain.com") == true) + #expect("user.name@domain.com".isValidEmail == true) + #expect("user+tag@domain.co.uk".isValidEmail == true) + #expect("user@subdomain.domain.org".isValidEmail == true) + #expect("123@domain.com".isValidEmail == true) // Invalid cases - #expect(Utility.validateEmail("user@") == false) - #expect(Utility.validateEmail("@domain.com") == false) - #expect(Utility.validateEmail("user.domain.com") == false) - #expect(Utility.validateEmail("user space@domain.com") == false) + #expect("user@".isValidEmail == false) + #expect("@domain.com".isValidEmail == false) + #expect("user.domain.com".isValidEmail == false) + #expect("user space@domain.com".isValidEmail == false) } @Test @@ -104,7 +104,7 @@ struct ForgotPasswordViewModelTest { @Test func messageTypesForForgotPassword() { // Test the types of messages that would be posted - let successMessage = Message(level: .success, message: .sentResetPasswordInstruction) + let successMessage = Message(level: .success, message: Strings.sentResetPasswordInstruction) let errorMessage = Message(level: .error, message: "Email not found", autoDismiss: false) #expect(successMessage.level == .success) diff --git a/NativeAppTemplateTests/UI/App Root/MainViewModelTest.swift b/NativeAppTemplateTests/UI/App Root/MainViewModelTest.swift index 4874762..ecbda7f 100644 --- a/NativeAppTemplateTests/UI/App Root/MainViewModelTest.swift +++ b/NativeAppTemplateTests/UI/App Root/MainViewModelTest.swift @@ -34,10 +34,6 @@ struct MainViewModelTest { tabViewModel: tabViewModel ) - #expect(viewModel.isShowingForceAppUpdatesAlert == false) - #expect(viewModel.itemTagId == nil) - #expect(viewModel.isResetting == false) - #expect(viewModel.isShowingResetConfirmationDialog == false) #expect(viewModel.arePrivacyAccepted == false) #expect(viewModel.areTermsAccepted == false) } @@ -65,67 +61,6 @@ struct MainViewModelTest { #expect(sessionController.userState == .notLoggedIn) } - @Test - func resetTagWithoutItemTagId() { - let dataManager = createTestDataManager() - let tabViewModel = createTestTabViewModel() - - let viewModel = MainViewModel( - sessionController: sessionController, - dataManager: dataManager, - messageBus: messageBus, - tabViewModel: tabViewModel - ) - - // Should not reset when itemTagId is nil - viewModel.itemTagId = nil - viewModel.resetTag() - - // Nothing should happen - #expect(viewModel.isResetting == false) - } - - @Test - func resetTagWithItemTagId() { - let dataManager = createTestDataManager() - let tabViewModel = createTestTabViewModel() - - let viewModel = MainViewModel( - sessionController: sessionController, - dataManager: dataManager, - messageBus: messageBus, - tabViewModel: tabViewModel - ) - - // Set itemTagId - viewModel.itemTagId = "test-tag-id" - // Reset should work with itemTagId set - viewModel.resetTag() - - // This would trigger async operations in real implementation - #expect(viewModel.itemTagId == "test-tag-id") - } - - @Test - func cancelResetDialog() { - let dataManager = createTestDataManager() - let tabViewModel = createTestTabViewModel() - - let viewModel = MainViewModel( - sessionController: sessionController, - dataManager: dataManager, - messageBus: messageBus, - tabViewModel: tabViewModel - ) - - // Set dialog to showing - viewModel.isShowingResetConfirmationDialog = true - - viewModel.cancelResetDialog() - - #expect(viewModel.isShowingResetConfirmationDialog == false) - } - @Test func stateProperties() { let dataManager = createTestDataManager() @@ -138,27 +73,10 @@ struct MainViewModelTest { tabViewModel: tabViewModel ) - // Test all boolean state properties - viewModel.isShowingForceAppUpdatesAlert = true - #expect(viewModel.isShowingForceAppUpdatesAlert == true) - - viewModel.isResetting = true - #expect(viewModel.isResetting == true) - - viewModel.isShowingResetConfirmationDialog = true - #expect(viewModel.isShowingResetConfirmationDialog == true) - viewModel.arePrivacyAccepted = true #expect(viewModel.arePrivacyAccepted == true) viewModel.areTermsAccepted = true #expect(viewModel.areTermsAccepted == true) - - // Test itemTagId - viewModel.itemTagId = "test-id" - #expect(viewModel.itemTagId == "test-id") - - viewModel.itemTagId = nil - #expect(viewModel.itemTagId == nil) } } diff --git a/NativeAppTemplateTests/UI/App Root/OnboardingViewModelTest.swift b/NativeAppTemplateTests/UI/App Root/OnboardingViewModelTest.swift index 139131d..2d30733 100644 --- a/NativeAppTemplateTests/UI/App Root/OnboardingViewModelTest.swift +++ b/NativeAppTemplateTests/UI/App Root/OnboardingViewModelTest.swift @@ -14,11 +14,11 @@ struct OnboardingViewModelTest { func mockOnboarding( id: Int = 1, - isPortraitImage: Bool = true + imageOrientation: ImageOrientation = .portrait ) -> Onboarding { Onboarding( id: id, - isPortraitImage: isPortraitImage + imageOrientation: imageOrientation ) } @@ -32,11 +32,11 @@ struct OnboardingViewModelTest { } @Test - func reload() { + func exposesRepositoryOnboardings() { let onboardings = [ - mockOnboarding(id: 1, isPortraitImage: true), - mockOnboarding(id: 2, isPortraitImage: false), - mockOnboarding(id: 3, isPortraitImage: true) + mockOnboarding(id: 1, imageOrientation: .portrait), + mockOnboarding(id: 2, imageOrientation: .landscape), + mockOnboarding(id: 3, imageOrientation: .portrait) ] onboardingRepository.setOnboardings(onboardings: onboardings) @@ -45,101 +45,38 @@ struct OnboardingViewModelTest { onboardingRepository: onboardingRepository ) - viewModel.reload() - - #expect(onboardingRepository.reloadCalled == true) #expect(viewModel.onboardings.count == 3) } @Test func onboardingDescription() { - let onboardings = [ - mockOnboarding(id: 1), - mockOnboarding(id: 2), - mockOnboarding(id: 3) - ] - - onboardingRepository.setOnboardings(onboardings: onboardings) - let viewModel = OnboardingViewModel( onboardingRepository: onboardingRepository ) - viewModel.reload() - - // Test valid indices (1-based indexing in the switch case) - #expect(viewModel.onboardingDescription(index: 1) == String.onboardingDescription1) - #expect(viewModel.onboardingDescription(index: 2) == String.onboardingDescription2) - #expect(viewModel.onboardingDescription(index: 3) == String.onboardingDescription3) + #expect(viewModel.onboardingDescription(index: 1) == Strings.onboardingDescription1) + #expect(viewModel.onboardingDescription(index: 2) == Strings.onboardingDescription2) + #expect(viewModel.onboardingDescription(index: 3) == Strings.onboardingDescription3) + #expect(viewModel.onboardingDescription(index: 4) == Strings.onboardingDescription4) } @Test func onboardingDescriptionInvalidIndex() { - let onboardings = [ - mockOnboarding(id: 1) - ] - - onboardingRepository.setOnboardings(onboardings: onboardings) - - let viewModel = OnboardingViewModel( - onboardingRepository: onboardingRepository - ) - - viewModel.reload() - - // Test invalid indices - should return default (onboardingDescription1) - let result = viewModel.onboardingDescription(index: 0) - #expect(result == String.onboardingDescription1) - let result2 = viewModel.onboardingDescription(index: 99) - #expect(result2 == String.onboardingDescription1) - } - - @Test - func onboardingDescriptionAllSteps() { - let onboardings = (1...13).map { mockOnboarding(id: $0) } - onboardingRepository.setOnboardings(onboardings: onboardings) - - let viewModel = OnboardingViewModel( - onboardingRepository: onboardingRepository - ) - - viewModel.reload() - - // Test all 13 onboarding steps (1-based indexing) - let expectedDescriptions = [ - String.onboardingDescription1, String.onboardingDescription2, String.onboardingDescription3, - String.onboardingDescription4, String.onboardingDescription5, String.onboardingDescription6, - String.onboardingDescription7, String.onboardingDescription8, String.onboardingDescription9, - String.onboardingDescription10, String.onboardingDescription11, String.onboardingDescription12, - String.onboardingDescription13 - ] - - for index in 1...13 { - #expect(viewModel.onboardingDescription(index: index) == expectedDescriptions[index - 1]) - } - } - - @Test - func emptyOnboardings() { - onboardingRepository.setOnboardings(onboardings: []) - let viewModel = OnboardingViewModel( onboardingRepository: onboardingRepository ) - viewModel.reload() - - #expect(viewModel.onboardings.isEmpty) - #expect(onboardingRepository.reloadCalled == true) + #expect(viewModel.onboardingDescription(index: 0) == Strings.onboardingDescription1) + #expect(viewModel.onboardingDescription(index: 99) == Strings.onboardingDescription1) } @Test func onboardingWithMixedImageTypes() { let onboardings = [ - mockOnboarding(id: 1, isPortraitImage: true), - mockOnboarding(id: 2, isPortraitImage: false), - mockOnboarding(id: 3, isPortraitImage: true), - mockOnboarding(id: 4, isPortraitImage: false) + mockOnboarding(id: 1, imageOrientation: .portrait), + mockOnboarding(id: 2, imageOrientation: .landscape), + mockOnboarding(id: 3, imageOrientation: .portrait), + mockOnboarding(id: 4, imageOrientation: .landscape) ] onboardingRepository.setOnboardings(onboardings: onboardings) @@ -148,12 +85,10 @@ struct OnboardingViewModelTest { onboardingRepository: onboardingRepository ) - viewModel.reload() - #expect(viewModel.onboardings.count == 4) - #expect(viewModel.onboardings[0].isPortraitImage == true) - #expect(viewModel.onboardings[1].isPortraitImage == false) - #expect(viewModel.onboardings[2].isPortraitImage == true) - #expect(viewModel.onboardings[3].isPortraitImage == false) + #expect(viewModel.onboardings[0].imageOrientation == .portrait) + #expect(viewModel.onboardings[1].imageOrientation == .landscape) + #expect(viewModel.onboardings[2].imageOrientation == .portrait) + #expect(viewModel.onboardings[3].imageOrientation == .landscape) } } diff --git a/NativeAppTemplateTests/UI/App Root/ResendConfirmationInstructionsViewModelTest.swift b/NativeAppTemplateTests/UI/App Root/ResendConfirmationInstructionsViewModelTest.swift index ea5790b..2c4bbca 100644 --- a/NativeAppTemplateTests/UI/App Root/ResendConfirmationInstructionsViewModelTest.swift +++ b/NativeAppTemplateTests/UI/App Root/ResendConfirmationInstructionsViewModelTest.swift @@ -22,8 +22,8 @@ struct ResendConfirmationViewModelTest { let email = "" // Simulate the validation logic from the ViewModel - let isBlank = Utility.isBlank(email) - let isInvalid = !Utility.validateEmail(email) + let isBlank = email.isBlank + let isInvalid = !email.isValidEmail let hasInvalidData = isBlank || isInvalid #expect(hasInvalidData == true) @@ -33,8 +33,8 @@ struct ResendConfirmationViewModelTest { func hasInvalidDataWithInvalidEmail() { let email = "invalid-email" - let isBlank = Utility.isBlank(email) - let isInvalid = !Utility.validateEmail(email) + let isBlank = email.isBlank + let isInvalid = !email.isValidEmail let hasInvalidData = isBlank || isInvalid #expect(hasInvalidData == true) @@ -44,8 +44,8 @@ struct ResendConfirmationViewModelTest { func hasInvalidDataWithValidEmail() { let email = "valid@example.com" - let isBlank = Utility.isBlank(email) - let isInvalid = !Utility.validateEmail(email) + let isBlank = email.isBlank + let isInvalid = !email.isValidEmail let hasInvalidData = isBlank || isInvalid #expect(hasInvalidData == false) @@ -54,35 +54,35 @@ struct ResendConfirmationViewModelTest { @Test func isEmailBlankValidation() { // Test blank email detection - #expect(Utility.isBlank("") == true) - #expect(Utility.isBlank(" ") == true) - #expect(Utility.isBlank("test@example.com") == false) + #expect("".isBlank == true) + #expect(" ".isBlank == true) + #expect("test@example.com".isBlank == false) } @Test func isEmailInvalidValidation() { // Test email format validation - #expect(Utility.validateEmail("") == false) - #expect(Utility.validateEmail("invalid") == false) - #expect(Utility.validateEmail("invalid@") == false) - #expect(Utility.validateEmail("@invalid.com") == false) - #expect(Utility.validateEmail("valid@example.com") == true) - #expect(Utility.validateEmail("user+tag@domain.org") == true) + #expect("".isValidEmail == false) + #expect("invalid".isValidEmail == false) + #expect("invalid@".isValidEmail == false) + #expect("@invalid.com".isValidEmail == false) + #expect("valid@example.com".isValidEmail == true) + #expect("user+tag@domain.org".isValidEmail == true) } @Test func emailValidationEdgeCases() { // Test various email formats - #expect(Utility.validateEmail("user.name@domain.com") == true) - #expect(Utility.validateEmail("user+tag@domain.co.uk") == true) - #expect(Utility.validateEmail("user@subdomain.domain.org") == true) - #expect(Utility.validateEmail("123@domain.com") == true) + #expect("user.name@domain.com".isValidEmail == true) + #expect("user+tag@domain.co.uk".isValidEmail == true) + #expect("user@subdomain.domain.org".isValidEmail == true) + #expect("123@domain.com".isValidEmail == true) // Invalid cases - #expect(Utility.validateEmail("user@") == false) - #expect(Utility.validateEmail("@domain.com") == false) - #expect(Utility.validateEmail("user.domain.com") == false) - #expect(Utility.validateEmail("user space@domain.com") == false) + #expect("user@".isValidEmail == false) + #expect("@domain.com".isValidEmail == false) + #expect("user.domain.com".isValidEmail == false) + #expect("user space@domain.com".isValidEmail == false) } @Test @@ -104,7 +104,7 @@ struct ResendConfirmationViewModelTest { @Test func messageTypesForResendConfirmation() { // Test the types of messages that would be posted - let successMessage = Message(level: .success, message: .sentConfirmationInstruction) + let successMessage = Message(level: .success, message: Strings.sentConfirmationInstruction) let errorMessage = Message(level: .error, message: "Email not found", autoDismiss: false) #expect(successMessage.level == .success) diff --git a/NativeAppTemplateTests/UI/Scan/ScanViewModelTest.swift b/NativeAppTemplateTests/UI/Scan/ScanViewModelTest.swift deleted file mode 100644 index 7ed8244..0000000 --- a/NativeAppTemplateTests/UI/Scan/ScanViewModelTest.swift +++ /dev/null @@ -1,489 +0,0 @@ -// -// ScanViewModelTest.swift -// NativeAppTemplate -// - -// swiftlint:disable file_length - -import Foundation -@testable import NativeAppTemplate -import Testing - -@MainActor -@Suite -// swiftlint:disable:next type_body_length -struct ScanViewModelTest { - let itemTagRepository = TestItemTagRepository(itemTagsService: ItemTagsService()) - let sessionController = TestSessionController() - let messageBus = MessageBus() - let nfcManager = TestNFCManager() - - var testItemTag: ItemTag { - var tag = ItemTag() - tag.id = "test-tag-id" - tag.shopId = "test-shop-id" - tag.queueNumber = "123" - tag.state = .idled - tag.completedAt = nil - tag.alreadyCompleted = false - return tag - } - - var testItemTagData: ItemTagData { - ItemTagData( - itemTagId: "test-tag-id", - itemTagType: .server, - isReadOnly: false, - scannedAt: Date.now - ) - } - - @Test - func initializesCorrectly() { - itemTagRepository.setItemTags(itemTags: [testItemTag]) - - let viewModel = ScanViewModel( - itemTagRepository: itemTagRepository, - sessionController: sessionController, - messageBus: messageBus, - nfcManager: nfcManager - ) - - #expect(viewModel.scanType == ScanType.completeScan) - #expect(viewModel.isShowingResetConfirmationDialog == false) - #expect(viewModel.isFetching == false) - #expect(viewModel.isResetting == false) - #expect(viewModel.isBusy == false) - } - - @Test - func busyStateReflectsFetchingAndResettingStates() { - itemTagRepository.setItemTags(itemTags: [testItemTag]) - - let viewModel = ScanViewModel( - itemTagRepository: itemTagRepository, - sessionController: sessionController, - messageBus: messageBus, - nfcManager: nfcManager - ) - - #expect(viewModel.isBusy == false) - - viewModel.isFetching = true - #expect(viewModel.isBusy == true) - - viewModel.isFetching = false - viewModel.isResetting = true - #expect(viewModel.isBusy == true) - - viewModel.isResetting = false - #expect(viewModel.isBusy == false) - - // Both fetching and resetting - viewModel.isFetching = true - viewModel.isResetting = true - #expect(viewModel.isBusy == true) - } - - @Test - func handleBackgroundTagReadingUpdatesScanType() { - itemTagRepository.setItemTags(itemTags: [testItemTag]) - sessionController.didBackgroundTagReading = false - - let viewModel = ScanViewModel( - itemTagRepository: itemTagRepository, - sessionController: sessionController, - messageBus: messageBus, - nfcManager: nfcManager - ) - - viewModel.scanType = ScanType.test - viewModel.handleBackgroundTagReading() - #expect(viewModel.scanType == ScanType.test) // Should not change - #expect(sessionController.didBackgroundTagReading == false) - - sessionController.didBackgroundTagReading = true - viewModel.handleBackgroundTagReading() - #expect(viewModel.scanType == ScanType.completeScan) - #expect(sessionController.didBackgroundTagReading == false) - } - - @Test - func handleScanResultChangedWithSuccessCompletesTag() async { - itemTagRepository.setItemTags(itemTags: [testItemTag]) - - nfcManager.reset() - itemTagRepository.error = nil - - let viewModel = ScanViewModel( - itemTagRepository: itemTagRepository, - sessionController: sessionController, - messageBus: messageBus, - nfcManager: nfcManager - ) - - // Setup successful scan result - nfcManager.simulatedItemTagData = testItemTagData - nfcManager.shouldSimulateSuccess = true - nfcManager.simulateScanResult() - - viewModel.handleScanResultChanged() - - // Wait for async task to complete - try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds - - #expect(sessionController.completeScanResult.type == .completed) - #expect(sessionController.completeScanResult.itemTag?.id == "test-tag-id") - } - - @Test - func handleScanResultChangedWithFailureSetsError() { - itemTagRepository.setItemTags(itemTags: [testItemTag]) - nfcManager.reset() - - let viewModel = ScanViewModel( - itemTagRepository: itemTagRepository, - sessionController: sessionController, - messageBus: messageBus, - nfcManager: nfcManager - ) - - // Setup failed scan result - let testError = NSError( - domain: "TestError", - code: 123, - userInfo: [NSLocalizedDescriptionKey: "Test scan error"] - ) - nfcManager.simulatedError = testError - nfcManager.shouldSimulateSuccess = false - nfcManager.simulateScanResult() - - viewModel.handleScanResultChanged() - - #expect(sessionController.completeScanResult.type == .failed) - #expect(sessionController.completeScanResult.message == "Test scan error") - } - - @Test - func handleScanResultChangedWithoutChangedResultDoesNothing() { - itemTagRepository.setItemTags(itemTags: [testItemTag]) - nfcManager.reset() - nfcManager.isScanResultChanged = false - - let viewModel = ScanViewModel( - itemTagRepository: itemTagRepository, - sessionController: sessionController, - messageBus: messageBus, - nfcManager: nfcManager - ) - - let originalResult = sessionController.completeScanResult - viewModel.handleScanResultChanged() - - #expect(sessionController.completeScanResult.type == originalResult.type) - } - - @Test - func handleScanResultChangedForTestingWithSuccessFetchesDetail() async { - itemTagRepository.setItemTags(itemTags: [testItemTag]) - nfcManager.reset() - itemTagRepository.error = nil - - let viewModel = ScanViewModel( - itemTagRepository: itemTagRepository, - sessionController: sessionController, - messageBus: messageBus, - nfcManager: nfcManager - ) - - // Setup successful scan result for testing - nfcManager.simulatedItemTagData = testItemTagData - nfcManager.shouldSimulateSuccess = true - nfcManager.simulateScanResultForTesting() - - viewModel.handleScanResultChangedForTesting() - - // Wait for async task to complete - try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds - - #expect(sessionController.showTagInfoScanResult.type == .succeeded) - #expect(sessionController.showTagInfoScanResult.itemTag?.id == "test-tag-id") - #expect(sessionController.showTagInfoScanResult.itemTagType == .server) - #expect(sessionController.showTagInfoScanResult.isReadOnly == false) - } - - @Test - func handleScanResultChangedForTestingWithFailureSetsError() { - itemTagRepository.setItemTags(itemTags: [testItemTag]) - nfcManager.reset() - - let viewModel = ScanViewModel( - itemTagRepository: itemTagRepository, - sessionController: sessionController, - messageBus: messageBus, - nfcManager: nfcManager - ) - - // Setup failed scan result for testing - let testError = NSError( - domain: "TestError", - code: 456, - userInfo: [NSLocalizedDescriptionKey: "Test fetch error"] - ) - nfcManager.simulatedError = testError - nfcManager.shouldSimulateSuccess = false - nfcManager.simulateScanResultForTesting() - - viewModel.handleScanResultChangedForTesting() - - #expect(sessionController.showTagInfoScanResult.type == .failed) - #expect(sessionController.showTagInfoScanResult.message == "Test fetch error") - } - - @Test - func startCompleteScanInitializesResultAndStartsNFC() async { - itemTagRepository.setItemTags(itemTags: [testItemTag]) - nfcManager.reset() - - let viewModel = ScanViewModel( - itemTagRepository: itemTagRepository, - sessionController: sessionController, - messageBus: messageBus, - nfcManager: nfcManager - ) - - let startCompleteScanTask = Task { - viewModel.startCompleteScan() - } - await startCompleteScanTask.value - - // Wait for async task to complete - try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds - - #expect(sessionController.completeScanResult.type == .idled) - #expect(nfcManager.readingStarted) - } - - @Test - func startTestScanInitializesResultAndStartsNFC() async { - itemTagRepository.setItemTags(itemTags: [testItemTag]) - nfcManager.reset() - - let viewModel = ScanViewModel( - itemTagRepository: itemTagRepository, - sessionController: sessionController, - messageBus: messageBus, - nfcManager: nfcManager - ) - - let startTestScanTask = Task { - viewModel.startTestScan() - } - await startTestScanTask.value - - // Wait for async task to complete - try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds - - #expect(sessionController.showTagInfoScanResult.type == .idled) - #expect(nfcManager.testingStarted) - } - - @Test - func resetTagWithValidItemTagResetsSuccessfully() async { - itemTagRepository.setItemTags(itemTags: [testItemTag]) - - itemTagRepository.error = nil - - let viewModel = ScanViewModel( - itemTagRepository: itemTagRepository, - sessionController: sessionController, - messageBus: messageBus, - nfcManager: nfcManager - ) - - // Setup a completed scan result with item tag - sessionController.completeScanResult = CompleteScanResult( - itemTag: testItemTag, - type: .completed - ) - - viewModel.resetTag() - - // Wait for async task to complete - try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds - - #expect(sessionController.completeScanResult.type == .reset) - #expect(sessionController.completeScanResult.itemTag?.id == "test-tag-id") - } - - @Test - func resetTagWithoutItemTagDoesNothing() { - itemTagRepository.setItemTags(itemTags: [testItemTag]) - - let viewModel = ScanViewModel( - itemTagRepository: itemTagRepository, - sessionController: sessionController, - messageBus: messageBus, - nfcManager: nfcManager - ) - - // Setup scan result without item tag - sessionController.completeScanResult = CompleteScanResult(type: .idled) - let originalResult = sessionController.completeScanResult - - viewModel.resetTag() - - #expect(sessionController.completeScanResult.type == originalResult.type) - } - - @Test - func resetTagWithFailureUpdatesResult() async { - itemTagRepository.setItemTags(itemTags: [testItemTag]) - itemTagRepository.error = NativeAppTemplateAPIError.requestFailed(nil, 500, "Reset failed") - - let viewModel = ScanViewModel( - itemTagRepository: itemTagRepository, - sessionController: sessionController, - messageBus: messageBus, - nfcManager: nfcManager - ) - - // Setup a completed scan result with item tag - sessionController.completeScanResult = CompleteScanResult( - itemTag: testItemTag, - type: .completed - ) - - viewModel.resetTag() - - // Wait for async task to complete - try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds - - #expect(sessionController.completeScanResult.type == .failed) - #expect(sessionController.completeScanResult.message.contains("Reset failed")) - } - - @Test - func dismissResetConfirmationDialogUpdatesState() { - itemTagRepository.setItemTags(itemTags: [testItemTag]) - - let viewModel = ScanViewModel( - itemTagRepository: itemTagRepository, - sessionController: sessionController, - messageBus: messageBus, - nfcManager: nfcManager - ) - - viewModel.isShowingResetConfirmationDialog = true - #expect(viewModel.isShowingResetConfirmationDialog == true) - - viewModel.dismissResetConfirmationDialog() - #expect(viewModel.isShowingResetConfirmationDialog == false) - } - - @Test - func completeTagWithAlreadyCompletedShowsDialog() async { - itemTagRepository.setItemTags(itemTags: [testItemTag]) - itemTagRepository.error = nil - - var alreadyCompletedTag = ItemTag() - alreadyCompletedTag.id = "completed-tag-id" - alreadyCompletedTag.shopId = "test-shop-id" - alreadyCompletedTag.queueNumber = "456" - alreadyCompletedTag.state = .completed - alreadyCompletedTag.completedAt = Date.now - alreadyCompletedTag.alreadyCompleted = true - - // Add the already completed tag to repository - itemTagRepository.itemTags.append(alreadyCompletedTag) - - let viewModel = ScanViewModel( - itemTagRepository: itemTagRepository, - sessionController: sessionController, - messageBus: messageBus, - nfcManager: nfcManager - ) - - // Setup scan result for already completed tag - let completedTagData = ItemTagData( - itemTagId: "completed-tag-id", - itemTagType: .server, - isReadOnly: false, - scannedAt: Date.now - ) - - nfcManager.simulatedItemTagData = completedTagData - nfcManager.shouldSimulateSuccess = true - nfcManager.simulateScanResult() - - viewModel.handleScanResultChanged() - - // Wait for async task to complete - try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds - - #expect(viewModel.isShowingResetConfirmationDialog == true) - #expect(sessionController.completeScanResult.type == .completed) - } - - @Test - func busyStateDuringReset() async { - itemTagRepository.setItemTags(itemTags: [testItemTag]) - itemTagRepository.error = nil - - let viewModel = ScanViewModel( - itemTagRepository: itemTagRepository, - sessionController: sessionController, - messageBus: messageBus, - nfcManager: nfcManager - ) - - // Setup a completed scan result with item tag - sessionController.completeScanResult = CompleteScanResult( - itemTag: testItemTag, - type: .completed - ) - - let resetTask = Task { - viewModel.resetTag() - } - - // Check busy state immediately after starting - #expect(viewModel.isBusy == viewModel.isResetting) - - await resetTask.value - - #expect(viewModel.isBusy == false) - #expect(viewModel.isResetting == false) - } - - @Test - func busyStateDuringFetch() async { - itemTagRepository.setItemTags(itemTags: [testItemTag]) - itemTagRepository.error = nil - nfcManager.reset() - - let viewModel = ScanViewModel( - itemTagRepository: itemTagRepository, - sessionController: sessionController, - messageBus: messageBus, - nfcManager: nfcManager - ) - - // Setup successful scan result for testing - nfcManager.simulatedItemTagData = testItemTagData - nfcManager.shouldSimulateSuccess = true - nfcManager.simulateScanResultForTesting() - - let fetchTask = Task { - viewModel.handleScanResultChangedForTesting() - } - - // Check busy state immediately after starting - #expect(viewModel.isBusy == viewModel.isFetching) - - await fetchTask.value - - #expect(viewModel.isBusy == false) - #expect(viewModel.isFetching == false) - } -} diff --git a/NativeAppTemplateTests/UI/Settings/PasswordEditViewModelTest.swift b/NativeAppTemplateTests/UI/Settings/PasswordEditViewModelTest.swift index e398a0d..a803f11 100644 --- a/NativeAppTemplateTests/UI/Settings/PasswordEditViewModelTest.swift +++ b/NativeAppTemplateTests/UI/Settings/PasswordEditViewModelTest.swift @@ -139,7 +139,7 @@ struct PasswordEditViewModelTest { #expect(viewModel.shouldDismiss == true) #expect(messageBus.currentMessage != nil) #expect(messageBus.currentMessage?.level == .success) - #expect(messageBus.currentMessage?.message == .passwordUpdated) + #expect(messageBus.currentMessage?.message == Strings.passwordUpdated) } @Test diff --git a/NativeAppTemplateTests/UI/Settings/SettingsViewModelTest.swift b/NativeAppTemplateTests/UI/Settings/SettingsViewModelTest.swift index 474ea01..954a063 100644 --- a/NativeAppTemplateTests/UI/Settings/SettingsViewModelTest.swift +++ b/NativeAppTemplateTests/UI/Settings/SettingsViewModelTest.swift @@ -82,7 +82,7 @@ struct SettingsViewModelTest { @Test func signOutSuccess() async { sessionController.userState = .loggedIn - tabViewModel.selectedTab = .scan + tabViewModel.selectedTab = .settings let viewModel = SettingsViewModel( sessionController: sessionController, @@ -100,14 +100,14 @@ struct SettingsViewModelTest { #if DEBUG #expect(messageBus.currentMessage != nil) #expect(messageBus.currentMessage?.level == .success) - #expect(messageBus.currentMessage?.message == .signedOut) + #expect(messageBus.currentMessage?.message == Strings.signedOut) #endif } @Test func signOutWithError() async { sessionController.userState = .loggedIn - tabViewModel.selectedTab = .scan + tabViewModel.selectedTab = .settings // Force an error by setting the session state to make logout fail // Note: TestSessionController doesn't naturally throw errors, so this test diff --git a/NativeAppTemplateTests/UI/Settings/ShopkeeperEditViewModelTest.swift b/NativeAppTemplateTests/UI/Settings/ShopkeeperEditViewModelTest.swift index 6ff9a67..a26b83d 100644 --- a/NativeAppTemplateTests/UI/Settings/ShopkeeperEditViewModelTest.swift +++ b/NativeAppTemplateTests/UI/Settings/ShopkeeperEditViewModelTest.swift @@ -234,7 +234,7 @@ struct ShopkeeperEditViewModelTest { // swiftlint:disable:this type_body_length #expect(viewModel.shouldDismiss == true) #expect(messageBus.currentMessage != nil) #expect(messageBus.currentMessage?.level == .success) - #expect(messageBus.currentMessage?.message == .reconfirmDescription) + #expect(messageBus.currentMessage?.message == Strings.reconfirmDescription) #expect(messageBus.currentMessage?.autoDismiss == false) #expect(sessionController.userState == .notLoggedIn) // Should be logged out } @@ -260,7 +260,7 @@ struct ShopkeeperEditViewModelTest { // swiftlint:disable:this type_body_length #expect(messageBus.currentMessage != nil) #expect(messageBus.currentMessage?.level == .success) - #expect(messageBus.currentMessage?.message == .shopkeeperUpdated) + #expect(messageBus.currentMessage?.message == Strings.shopkeeperUpdated) #expect(sessionController.userState == .loggedIn) // Should remain logged in } @@ -318,7 +318,7 @@ struct ShopkeeperEditViewModelTest { // swiftlint:disable:this type_body_length @Test func destroyShopkeeperSuccess() async { sessionController.shopkeeper = testShopkeeper - tabViewModel.selectedTab = .scan + tabViewModel.selectedTab = .settings let viewModel = ShopkeeperEditViewModel( signUpRepository: signUpRepository, @@ -338,7 +338,7 @@ struct ShopkeeperEditViewModelTest { // swiftlint:disable:this type_body_length #expect(tabViewModel.selectedTab == .shops) #expect(messageBus.currentMessage != nil) #expect(messageBus.currentMessage?.level == .success) - #expect(messageBus.currentMessage?.message == .shopkeeperDeleted) + #expect(messageBus.currentMessage?.message == Strings.shopkeeperDeleted) #expect(sessionController.shopkeeper == nil) } diff --git a/NativeAppTemplateTests/UI/Shop Detail/ShopDetailViewModelTest.swift b/NativeAppTemplateTests/UI/Shop Detail/ShopDetailViewModelTest.swift index dd835f9..71a8d19 100644 --- a/NativeAppTemplateTests/UI/Shop Detail/ShopDetailViewModelTest.swift +++ b/NativeAppTemplateTests/UI/Shop Detail/ShopDetailViewModelTest.swift @@ -19,9 +19,9 @@ struct ShopDetailViewModelTest { // swiftlint:disable:this type_body_length var itemTags: [ItemTag] { [ - mockItemTag(id: "1", shopId: "1", queueNumber: "A001"), - mockItemTag(id: "2", shopId: "1", queueNumber: "A002"), - mockItemTag(id: "3", shopId: "1", queueNumber: "A003") + mockItemTag(id: "1", shopId: "1", name: "A001"), + mockItemTag(id: "2", shopId: "1", name: "A002"), + mockItemTag(id: "3", shopId: "1", name: "A003") ] } @@ -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) } @@ -143,45 +143,7 @@ struct ShopDetailViewModelTest { // swiftlint:disable:this type_body_length } await completeTagTask.value - let message = String.itemTagCompleted - - #expect(viewModel.messageBus.currentMessage?.message == message) - } - - @Test - func completeTagWhenAlreadyCompleted() async throws { - shopRepository.setShops(shops: shops) - var modifiedItemTags = itemTags - modifiedItemTags[0].alreadyCompleted = true - - itemTagRepository.setItemTags(itemTags: modifiedItemTags) - - let viewModel = ShopDetailViewModel( - sessionController: sessionController, - shopRepository: shopRepository, - itemTagRepository: itemTagRepository, - tabViewModel: tabViewModel, - mainTab: mainTab, - messageBus: messageBus, - shopId: shopId - ) - - let reloadTask = Task { - viewModel.reload() - } - await reloadTask.value - - let shop = try #require(shops.first { $0.id == shopId }) - #expect(viewModel.shop == shop) - - let completeTagTask = Task { - viewModel.completeTag(itemTagId: modifiedItemTags.first!.id) - } - await completeTagTask.value - - let message = String.itemTagAlreadyCompleted - - #expect(viewModel.messageBus.currentMessage?.message == message) + #expect(viewModel.messageBus.currentMessage == nil) } @Test @@ -220,7 +182,7 @@ struct ShopDetailViewModelTest { // swiftlint:disable:this type_body_length } @Test - func resetTag() async throws { + func idleTag() async throws { shopRepository.setShops(shops: shops) itemTagRepository.setItemTags(itemTags: itemTags) @@ -242,18 +204,16 @@ struct ShopDetailViewModelTest { // swiftlint:disable:this type_body_length let shop = try #require(shops.first { $0.id == shopId }) #expect(viewModel.shop == shop) - let resetTagTask = Task { - viewModel.resetTag(itemTagId: itemTags.first!.id) + let idleTagTask = Task { + viewModel.idleTag(itemTagId: itemTags.first!.id) } - await resetTagTask.value + await idleTagTask.value - let message = String.itemTagReset - - #expect(viewModel.messageBus.currentMessage?.message == message) + #expect(viewModel.messageBus.currentMessage == nil) } @Test - func resetTagFailed() async throws { + func idleTagFailed() async throws { shopRepository.setShops(shops: shops) itemTagRepository.setItemTags(itemTags: itemTags) @@ -279,10 +239,10 @@ struct ShopDetailViewModelTest { // swiftlint:disable:this type_body_length let httpResponseCode = 500 itemTagRepository.error = NativeAppTemplateAPIError.requestFailed(nil, httpResponseCode, message) - let resetTagTask = Task { - viewModel.resetTag(itemTagId: itemTags.first!.id) + let idleTagTask = Task { + viewModel.idleTag(itemTagId: itemTags.first!.id) } - await resetTagTask.value + await idleTagTask.value #expect(viewModel.messageBus.currentMessage?.level == .error) } @@ -330,16 +290,14 @@ struct ShopDetailViewModelTest { // swiftlint:disable:this type_body_length description: "This is a mock shop for testing", timeZone: "Tokyo", itemTagsCount: 10, - scannedItemTagsCount: 5, - completedItemTagsCount: 3, - displayShopServerPath: "https://api.nativeapptemplate.com/display/shops/\(id)?type=server" + completedItemTagsCount: 3 ) } private func mockItemTag( id: String = UUID().uuidString, shopId: String = UUID().uuidString, - queueNumber: String = "Mock ItemTag" + name: String = "Mock ItemTag" ) -> ItemTag { let dateString = "2025-05-18 18:00:00 UTC" let formatter = DateFormatter() @@ -349,14 +307,13 @@ struct ShopDetailViewModelTest { // swiftlint:disable:this type_body_length return ItemTag( id: id, shopId: shopId, - queueNumber: queueNumber, + name: name, + description: "", + position: 1, state: .idled, - scanState: .unscanned, createdAt: date, - customerReadAt: nil, completedAt: nil, - shopName: "Mock ItemTag", - alreadyCompleted: false + shopName: "Mock ItemTag" ) } } diff --git a/NativeAppTemplateTests/UI/Shop List/ShopCreateViewModelTest.swift b/NativeAppTemplateTests/UI/Shop List/ShopCreateViewModelTest.swift index ce489f0..cd7f383 100644 --- a/NativeAppTemplateTests/UI/Shop List/ShopCreateViewModelTest.swift +++ b/NativeAppTemplateTests/UI/Shop List/ShopCreateViewModelTest.swift @@ -27,8 +27,36 @@ struct ShopCreateViewModelTest { #expect(viewModel.isCreating == false) } - @Test("Has invalid data", arguments: ["", "Shop Name 1"]) - func hasInvalidData(name: String) { + @Test + func maximumNameLength() { + let viewModel = ShopCreateViewModel( + sessionController: sessionController, + shopRepository: shopRepository, + messageBus: messageBus + ) + + #expect(viewModel.maximumNameLength == 100) + } + + @Test + func maximumDescriptionLength() { + let viewModel = ShopCreateViewModel( + sessionController: sessionController, + shopRepository: shopRepository, + messageBus: messageBus + ) + + #expect(viewModel.maximumDescriptionLength == 1_000) + } + + @Test("Name validation", arguments: [ + ("", true), // blank β†’ invalid + ("a", false), // 1 char β†’ valid + ("Shop Name 1", false), // normal β†’ valid + (String(repeating: "a", count: 100), false), // exactly 100 β†’ valid + (String(repeating: "a", count: 101), true) // 101 β†’ invalid + ]) + func nameValidation(name: String, shouldBeInvalid: Bool) { let viewModel = ShopCreateViewModel( sessionController: sessionController, shopRepository: shopRepository, @@ -36,7 +64,54 @@ struct ShopCreateViewModelTest { ) viewModel.name = name - #expect(viewModel.hasInvalidData == (name == "" ? true : false)) + + #expect(viewModel.hasInvalidDataName == shouldBeInvalid) + } + + @Test("Description validation", arguments: [ + ("", false), // empty β†’ valid + ("Short note.", false), // short β†’ valid + (String(repeating: "x", count: 1000), false), // exactly 1000 β†’ valid + (String(repeating: "x", count: 1001), true) // 1001 β†’ invalid + ]) + func descriptionValidation(description: String, shouldBeInvalid: Bool) { + let viewModel = ShopCreateViewModel( + sessionController: sessionController, + shopRepository: shopRepository, + messageBus: messageBus + ) + + viewModel.description = description + + #expect(viewModel.hasInvalidDataDescription == shouldBeInvalid) + } + + @Test + func validateNameLengthTruncatesCorrectly() { + let viewModel = ShopCreateViewModel( + sessionController: sessionController, + shopRepository: shopRepository, + messageBus: messageBus + ) + + viewModel.name = String(repeating: "a", count: 100) + "EXTRA" + viewModel.validateNameLength() + + #expect(viewModel.name == String(repeating: "a", count: 100)) + } + + @Test + func validateDescriptionLengthTruncatesCorrectly() { + let viewModel = ShopCreateViewModel( + sessionController: sessionController, + shopRepository: shopRepository, + messageBus: messageBus + ) + + viewModel.description = String(repeating: "x", count: 1500) + viewModel.validateDescriptionLength() + + #expect(viewModel.description.count == 1_000) } @Test @@ -65,7 +140,7 @@ struct ShopCreateViewModelTest { let latestShop = try #require(shopRepository.shops.last) - let message = String.shopCreated + let message = Strings.shopCreated #expect(viewModel.messageBus.currentMessage?.message == message) #expect(viewModel.isCreating) @@ -105,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) @@ -140,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) diff --git a/NativeAppTemplateTests/UI/Shop List/ShopListViewModelTest.swift b/NativeAppTemplateTests/UI/Shop List/ShopListViewModelTest.swift index d28436a..3c9feaf 100644 --- a/NativeAppTemplateTests/UI/Shop List/ShopListViewModelTest.swift +++ b/NativeAppTemplateTests/UI/Shop List/ShopListViewModelTest.swift @@ -193,9 +193,7 @@ struct ShopListViewModelTest { description: "This is a mock shop for testing", timeZone: "Tokyo", itemTagsCount: 10, - scannedItemTagsCount: 5, - completedItemTagsCount: 3, - displayShopServerPath: "https://api.nativeapptemplate.com/display/shops/\(id)?type=server" + completedItemTagsCount: 3 ) } } diff --git a/NativeAppTemplateTests/UI/Shop Settings/ItemTag Detail/ItemTagDetailViewModelTest.swift b/NativeAppTemplateTests/UI/Shop Settings/ItemTag Detail/ItemTagDetailViewModelTest.swift index f3f3fcb..2c83fd5 100644 --- a/NativeAppTemplateTests/UI/Shop Settings/ItemTag Detail/ItemTagDetailViewModelTest.swift +++ b/NativeAppTemplateTests/UI/Shop Settings/ItemTag Detail/ItemTagDetailViewModelTest.swift @@ -9,13 +9,12 @@ import Testing @MainActor @Suite -struct ItemTagDetailViewModelTest { // swiftlint:disable:this type_body_length +struct ItemTagDetailViewModelTest { let sessionController = TestSessionController() let itemTagRepository = TestItemTagRepository( itemTagsService: ItemTagsService() ) let messageBus = MessageBus() - let nfcManager = NFCManager() var shop: Shop { mockShop(id: "1", name: "Test Shop") } @@ -26,14 +25,13 @@ struct ItemTagDetailViewModelTest { // swiftlint:disable:this type_body_length ItemTag( id: itemTagId, shopId: shop.id, - queueNumber: "A01", + name: "A01", + description: "", + position: 1, state: .idled, - scanState: .unscanned, createdAt: Date(), - customerReadAt: nil, completedAt: nil, - shopName: shop.name, - alreadyCompleted: false + shopName: shop.name ) } @@ -43,18 +41,15 @@ struct ItemTagDetailViewModelTest { // swiftlint:disable:this type_body_length itemTagRepository: itemTagRepository, messageBus: messageBus, sessionController: sessionController, - nfcManager: nfcManager, shop: shop, itemTagId: itemTagId ) - #expect(viewModel.isLocked == false) #expect(viewModel.isShowingEditSheet == false) #expect(viewModel.isShowingDeleteConfirmationDialog == false) #expect(viewModel.isFetching == true) - #expect(viewModel.isGeneratingQrCode == false) + #expect(viewModel.isToggling == false) #expect(viewModel.isDeleting == false) - #expect(viewModel.customerTagQrCodeImage == nil) #expect(viewModel.shouldDismiss == false) #expect(viewModel.itemTag == nil) #expect(viewModel.shop.id == shop.id) @@ -67,26 +62,21 @@ struct ItemTagDetailViewModelTest { // swiftlint:disable:this type_body_length itemTagRepository: itemTagRepository, messageBus: messageBus, sessionController: sessionController, - nfcManager: nfcManager, shop: shop, itemTagId: itemTagId ) - // Initially fetching #expect(viewModel.isBusy == true) #expect(viewModel.isFetching == true) - // When generating QR code - viewModel.isGeneratingQrCode = true + viewModel.isFetching = false + viewModel.isToggling = true #expect(viewModel.isBusy == true) - // When deleting - viewModel.isFetching = false - viewModel.isGeneratingQrCode = false + viewModel.isToggling = false viewModel.isDeleting = true #expect(viewModel.isBusy == true) - // When none are busy viewModel.isDeleting = false #expect(viewModel.isBusy == false) } @@ -99,7 +89,6 @@ struct ItemTagDetailViewModelTest { // swiftlint:disable:this type_body_length itemTagRepository: itemTagRepository, messageBus: messageBus, sessionController: sessionController, - nfcManager: nfcManager, shop: shop, itemTagId: itemTagId ) @@ -112,7 +101,7 @@ struct ItemTagDetailViewModelTest { // swiftlint:disable:this type_body_length #expect(viewModel.isFetching == false) #expect(viewModel.itemTag != nil) #expect(viewModel.itemTag?.id == itemTagId) - #expect(viewModel.itemTag?.queueNumber == "A01") + #expect(viewModel.itemTag?.name == "A01") } @Test @@ -125,7 +114,6 @@ struct ItemTagDetailViewModelTest { // swiftlint:disable:this type_body_length itemTagRepository: itemTagRepository, messageBus: messageBus, sessionController: sessionController, - nfcManager: nfcManager, shop: shop, itemTagId: itemTagId ) @@ -143,165 +131,229 @@ struct ItemTagDetailViewModelTest { // swiftlint:disable:this type_body_length } @Test - func generateCustomerQrCode() async { + func completeItemTagSuccess() async { itemTagRepository.setItemTags(itemTags: [testItemTag]) let viewModel = ItemTagDetailViewModel( itemTagRepository: itemTagRepository, messageBus: messageBus, sessionController: sessionController, - nfcManager: nfcManager, shop: shop, itemTagId: itemTagId ) - // Load the item tag first let reloadTask = Task { viewModel.reload() } await reloadTask.value - viewModel.generateCustomerQrCode() + let completeTask = Task { + viewModel.completeItemTag() + } + await completeTask.value - #expect(viewModel.isGeneratingQrCode == false) - #expect(viewModel.customerTagQrCodeImage != nil) + #expect(viewModel.isToggling == false) + #expect(viewModel.itemTag?.state == .completed) } @Test - func generateCustomerQrCodeWithoutItemTag() { + func completeItemTagFailure() async throws { + itemTagRepository.setItemTags(itemTags: [testItemTag]) + let viewModel = ItemTagDetailViewModel( itemTagRepository: itemTagRepository, messageBus: messageBus, sessionController: sessionController, - nfcManager: nfcManager, shop: shop, itemTagId: itemTagId ) - // itemTag is nil - viewModel.generateCustomerQrCode() + let reloadTask = Task { + viewModel.reload() + } + await reloadTask.value + + itemTagRepository.error = NativeAppTemplateAPIError.requestFailed(nil, 500, "Boom") + + let completeTask = Task { + viewModel.completeItemTag() + } + await completeTask.value - #expect(viewModel.customerTagQrCodeImage == nil) + #expect(viewModel.isToggling == false) + #expect(messageBus.currentMessage?.level == .error) + let errorMessage = try #require(messageBus.currentMessage?.message) + #expect(errorMessage.contains(Strings.itemTagCompletedError)) } @Test - func destroyItemTagSuccess() async { - itemTagRepository.setItemTags(itemTags: [testItemTag]) + func idleItemTagSuccess() async { + var completedItemTag = testItemTag + completedItemTag.state = .completed + completedItemTag.completedAt = .now + itemTagRepository.setItemTags(itemTags: [completedItemTag]) let viewModel = ItemTagDetailViewModel( itemTagRepository: itemTagRepository, messageBus: messageBus, sessionController: sessionController, - nfcManager: nfcManager, shop: shop, itemTagId: itemTagId ) - // Load the item tag first let reloadTask = Task { viewModel.reload() } await reloadTask.value - let destroyTask = Task { - viewModel.destroyItemTag() + let idleTask = Task { + viewModel.idleItemTag() } - await destroyTask.value + await idleTask.value - #expect(viewModel.shouldDismiss == true) - #expect(messageBus.currentMessage != nil) - #expect(messageBus.currentMessage?.level == .success) - #expect(messageBus.currentMessage?.message == .itemTagDeleted) - #expect(itemTagRepository.itemTags.count == 0) // Item should be deleted + #expect(viewModel.isToggling == false) + #expect(viewModel.itemTag?.state == .idled) } @Test - func destroyItemTagFailure() async throws { - itemTagRepository.setItemTags(itemTags: [testItemTag]) + func idleItemTagFailure() async throws { + var completedItemTag = testItemTag + completedItemTag.state = .completed + completedItemTag.completedAt = .now + itemTagRepository.setItemTags(itemTags: [completedItemTag]) let viewModel = ItemTagDetailViewModel( itemTagRepository: itemTagRepository, messageBus: messageBus, sessionController: sessionController, - nfcManager: nfcManager, shop: shop, itemTagId: itemTagId ) - // Load the item tag first let reloadTask = Task { viewModel.reload() } await reloadTask.value - // Set error after loading - let message = "Delete failed" - let httpResponseCode = 500 - itemTagRepository.error = NativeAppTemplateAPIError.requestFailed(nil, httpResponseCode, message) + itemTagRepository.error = NativeAppTemplateAPIError.requestFailed(nil, 500, "Boom") - let destroyTask = Task { - viewModel.destroyItemTag() + let idleTask = Task { + viewModel.idleItemTag() } - await destroyTask.value + await idleTask.value - #expect(viewModel.shouldDismiss == true) - #expect(messageBus.currentMessage != nil) + #expect(viewModel.isToggling == false) #expect(messageBus.currentMessage?.level == .error) - #expect(messageBus.currentMessage?.autoDismiss == false) let errorMessage = try #require(messageBus.currentMessage?.message) - #expect(errorMessage.contains(String.itemTagDeletedError)) - #expect(itemTagRepository.itemTags.count == 1) // Item should still exist + #expect(errorMessage.contains(Strings.itemTagIdledError)) } @Test - func destroyItemTagWithoutItemTag() async { + func toggleWithoutItemTagDoesNothing() async { let viewModel = ItemTagDetailViewModel( itemTagRepository: itemTagRepository, messageBus: messageBus, sessionController: sessionController, - nfcManager: nfcManager, shop: shop, itemTagId: itemTagId ) - // itemTag is nil + let completeTask = Task { + viewModel.completeItemTag() + } + await completeTask.value + + let idleTask = Task { + viewModel.idleItemTag() + } + await idleTask.value + + #expect(viewModel.isToggling == false) + #expect(messageBus.currentMessage == nil) + } + + @Test + func destroyItemTagSuccess() async { + itemTagRepository.setItemTags(itemTags: [testItemTag]) + + let viewModel = ItemTagDetailViewModel( + itemTagRepository: itemTagRepository, + messageBus: messageBus, + sessionController: sessionController, + shop: shop, + itemTagId: itemTagId + ) + + let reloadTask = Task { + viewModel.reload() + } + await reloadTask.value + let destroyTask = Task { viewModel.destroyItemTag() } await destroyTask.value - #expect(viewModel.isDeleting == false) - #expect(viewModel.shouldDismiss == false) - #expect(messageBus.currentMessage == nil) // No message should be posted + #expect(viewModel.shouldDismiss == true) + #expect(messageBus.currentMessage != nil) + #expect(messageBus.currentMessage?.level == .success) + #expect(messageBus.currentMessage?.message == Strings.itemTagDeleted) + #expect(itemTagRepository.itemTags.count == 0) } @Test - func busyStateDuringDeletion() async { + func destroyItemTagFailure() async throws { itemTagRepository.setItemTags(itemTags: [testItemTag]) let viewModel = ItemTagDetailViewModel( itemTagRepository: itemTagRepository, messageBus: messageBus, sessionController: sessionController, - nfcManager: nfcManager, shop: shop, itemTagId: itemTagId ) - // Load the item tag first let reloadTask = Task { viewModel.reload() } await reloadTask.value + let message = "Delete failed" + let httpResponseCode = 500 + itemTagRepository.error = NativeAppTemplateAPIError.requestFailed(nil, httpResponseCode, message) + let destroyTask = Task { viewModel.destroyItemTag() } + await destroyTask.value - // Check busy state immediately after starting - #expect(viewModel.isBusy == viewModel.isDeleting) + #expect(viewModel.shouldDismiss == true) + #expect(messageBus.currentMessage != nil) + #expect(messageBus.currentMessage?.level == .error) + #expect(messageBus.currentMessage?.autoDismiss == false) + let errorMessage = try #require(messageBus.currentMessage?.message) + #expect(errorMessage.contains(Strings.itemTagDeletedError)) + #expect(itemTagRepository.itemTags.count == 1) + } + + @Test + func destroyItemTagWithoutItemTag() async { + let viewModel = ItemTagDetailViewModel( + itemTagRepository: itemTagRepository, + messageBus: messageBus, + sessionController: sessionController, + shop: shop, + itemTagId: itemTagId + ) + let destroyTask = Task { + viewModel.destroyItemTag() + } await destroyTask.value + + #expect(viewModel.isDeleting == false) + #expect(viewModel.shouldDismiss == false) + #expect(messageBus.currentMessage == nil) } @Test @@ -310,25 +362,18 @@ struct ItemTagDetailViewModelTest { // swiftlint:disable:this type_body_length itemTagRepository: itemTagRepository, messageBus: messageBus, sessionController: sessionController, - nfcManager: nfcManager, shop: shop, itemTagId: itemTagId ) - // Test initial state #expect(viewModel.isShowingEditSheet == false) #expect(viewModel.isShowingDeleteConfirmationDialog == false) - #expect(viewModel.isLocked == false) - // Test state changes viewModel.isShowingEditSheet = true #expect(viewModel.isShowingEditSheet == true) viewModel.isShowingDeleteConfirmationDialog = true #expect(viewModel.isShowingDeleteConfirmationDialog == true) - - viewModel.isLocked = true - #expect(viewModel.isLocked == true) } private func mockShop(id: String = UUID().uuidString, name: String = "Mock Shop") -> Shop { @@ -338,9 +383,7 @@ struct ItemTagDetailViewModelTest { // swiftlint:disable:this type_body_length description: "This is a mock shop for testing", timeZone: "Tokyo", itemTagsCount: 10, - scannedItemTagsCount: 5, - completedItemTagsCount: 3, - displayShopServerPath: "https://api.nativeapptemplate.com/display/shops/\(id)?type=server" + completedItemTagsCount: 3 ) } } diff --git a/NativeAppTemplateTests/UI/Shop Settings/ItemTag Detail/ItemTagEditViewModelTest.swift b/NativeAppTemplateTests/UI/Shop Settings/ItemTag Detail/ItemTagEditViewModelTest.swift index f11782c..5c9c294 100644 --- a/NativeAppTemplateTests/UI/Shop Settings/ItemTag Detail/ItemTagEditViewModelTest.swift +++ b/NativeAppTemplateTests/UI/Shop Settings/ItemTag Detail/ItemTagEditViewModelTest.swift @@ -9,8 +9,7 @@ import Testing @MainActor @Suite -struct ItemTagEditViewModelTest { // swiftlint:disable:this type_body_length - let sessionController = TestSessionController() +struct ItemTagEditViewModelTest { let itemTagRepository = TestItemTagRepository( itemTagsService: ItemTagsService() ) @@ -21,14 +20,13 @@ struct ItemTagEditViewModelTest { // swiftlint:disable:this type_body_length ItemTag( id: itemTagId, shopId: "test-shop-id", - queueNumber: "A01", + name: "Original", + description: "Original description", + position: 1, state: .idled, - scanState: .unscanned, createdAt: Date(), - customerReadAt: nil, completedAt: nil, - shopName: "Test Shop", - alreadyCompleted: false + shopName: "Test Shop" ) } @@ -37,11 +35,11 @@ struct ItemTagEditViewModelTest { // swiftlint:disable:this type_body_length let viewModel = ItemTagEditViewModel( itemTagRepository: itemTagRepository, messageBus: messageBus, - sessionController: sessionController, itemTagId: itemTagId ) - #expect(viewModel.queueNumber == "") + #expect(viewModel.name == "") + #expect(viewModel.description == "") #expect(viewModel.isFetching == true) #expect(viewModel.isUpdating == false) #expect(viewModel.shouldDismiss == false) @@ -49,17 +47,26 @@ struct ItemTagEditViewModelTest { // swiftlint:disable:this type_body_length } @Test - func maximumQueueNumberLength() { - sessionController.maximumQueueNumberLength = 6 + func maximumNameLength() { let viewModel = ItemTagEditViewModel( itemTagRepository: itemTagRepository, messageBus: messageBus, - sessionController: sessionController, itemTagId: itemTagId ) - #expect(viewModel.maximumQueueNumberLength == 6) + #expect(viewModel.maximumNameLength == 100) + } + + @Test + func maximumDescriptionLength() { + let viewModel = ItemTagEditViewModel( + itemTagRepository: itemTagRepository, + messageBus: messageBus, + itemTagId: itemTagId + ) + + #expect(viewModel.maximumDescriptionLength == 1000) } @Test @@ -67,15 +74,12 @@ struct ItemTagEditViewModelTest { // swiftlint:disable:this type_body_length let viewModel = ItemTagEditViewModel( itemTagRepository: itemTagRepository, messageBus: messageBus, - sessionController: sessionController, itemTagId: itemTagId ) - // Initially fetching #expect(viewModel.isBusy == true) #expect(viewModel.isFetching == true) - // When updating viewModel.isUpdating = true #expect(viewModel.isBusy == true) @@ -91,7 +95,6 @@ struct ItemTagEditViewModelTest { // swiftlint:disable:this type_body_length let viewModel = ItemTagEditViewModel( itemTagRepository: itemTagRepository, messageBus: messageBus, - sessionController: sessionController, itemTagId: itemTagId ) @@ -103,7 +106,8 @@ struct ItemTagEditViewModelTest { // swiftlint:disable:this type_body_length #expect(viewModel.isFetching == false) #expect(viewModel.itemTag != nil) #expect(viewModel.itemTag?.id == itemTagId) - #expect(viewModel.queueNumber == "A01") + #expect(viewModel.name == "Original") + #expect(viewModel.description == "Original description") } @Test @@ -115,7 +119,6 @@ struct ItemTagEditViewModelTest { // swiftlint:disable:this type_body_length let viewModel = ItemTagEditViewModel( itemTagRepository: itemTagRepository, messageBus: messageBus, - sessionController: sessionController, itemTagId: itemTagId ) @@ -131,84 +134,123 @@ struct ItemTagEditViewModelTest { // swiftlint:disable:this type_body_length #expect(messageBus.currentMessage?.autoDismiss == false) } - @Test("Queue number validation", arguments: [ - ("", true), // blank - ("a", true), // too short - ("ab", false), // minimum valid - ("abc", false), // valid - ("abcd", false), // valid - ("ab!", true), // non-alphanumeric - ("a b", true), // contains space - ("12", false), // numbers are valid - ("a1", false) // alphanumeric is valid + @Test("Name validation", arguments: [ + ("", true), // blank β†’ invalid + ("a", false), // 1 char β†’ valid + ("Buy milk πŸ₯›", false), // unicode + space β†’ valid + ("Item with !@#$%^&* symbols", false), // symbols β†’ valid + (String(repeating: "x", count: 100), false), // exactly 100 β†’ valid + (String(repeating: "x", count: 101), true) // 101 β†’ invalid ]) - func queueNumberValidation(queueNumber: String, shouldBeInvalid: Bool) async { - sessionController.maximumQueueNumberLength = 5 + func nameValidation(name: String, shouldBeInvalid: Bool) async { itemTagRepository.setItemTags(itemTags: [testItemTag]) let viewModel = ItemTagEditViewModel( itemTagRepository: itemTagRepository, messageBus: messageBus, - sessionController: sessionController, itemTagId: itemTagId ) - // Load the item tag first let reloadTask = Task { viewModel.reload() } await reloadTask.value - viewModel.queueNumber = queueNumber + viewModel.name = name - #expect(viewModel.hasInvalidDataQueueNumber == shouldBeInvalid) + #expect(viewModel.hasInvalidDataName == shouldBeInvalid) + } + + @Test("Description validation", arguments: [ + ("", false), + ("Some notes.", false), + (String(repeating: "x", count: 1000), false), + (String(repeating: "x", count: 1001), true) + ]) + func descriptionValidation(description: String, shouldBeInvalid: Bool) async { + itemTagRepository.setItemTags(itemTags: [testItemTag]) + + let viewModel = ItemTagEditViewModel( + itemTagRepository: itemTagRepository, + messageBus: messageBus, + itemTagId: itemTagId + ) + + let reloadTask = Task { + viewModel.reload() + } + await reloadTask.value + + viewModel.description = description + + #expect(viewModel.hasInvalidDataDescription == shouldBeInvalid) } @Test - func hasInvalidDataWithUnchangedQueueNumber() async { + func hasInvalidDataChangeDetection() async { itemTagRepository.setItemTags(itemTags: [testItemTag]) let viewModel = ItemTagEditViewModel( itemTagRepository: itemTagRepository, messageBus: messageBus, - sessionController: sessionController, itemTagId: itemTagId ) - // Load the item tag first let reloadTask = Task { viewModel.reload() } await reloadTask.value - // Queue number is same as original - should be invalid - #expect(viewModel.queueNumber == "A01") + // Both unchanged β†’ invalid (no changes to save) + #expect(viewModel.name == "Original") + #expect(viewModel.description == "Original description") #expect(viewModel.hasInvalidData == true) - // Change to different valid queue number - should be valid - viewModel.queueNumber = "B01" + // Name changed only β†’ valid + viewModel.name = "Updated" #expect(viewModel.hasInvalidData == false) - // Change to invalid queue number - should be invalid - viewModel.queueNumber = "!" + // Reset name; description changed only β†’ valid + viewModel.name = "Original" + viewModel.description = "Updated description" + #expect(viewModel.hasInvalidData == false) + + // Both changed β†’ valid + viewModel.name = "Updated" + #expect(viewModel.hasInvalidData == false) + + // Name invalid (blank) β†’ invalid regardless of description + viewModel.name = "" #expect(viewModel.hasInvalidData == true) } @Test - func validateQueueNumberLengthTruncatesCorrectly() { - sessionController.maximumQueueNumberLength = 3 + func validateNameLengthTruncatesCorrectly() { + + let viewModel = ItemTagEditViewModel( + itemTagRepository: itemTagRepository, + messageBus: messageBus, + itemTagId: itemTagId + ) + viewModel.name = String(repeating: "A", count: 100) + "EXTRA" + viewModel.validateNameLength() + + #expect(viewModel.name == String(repeating: "A", count: 100)) + } + + @Test + func validateDescriptionLengthTruncatesCorrectly() { let viewModel = ItemTagEditViewModel( itemTagRepository: itemTagRepository, messageBus: messageBus, - sessionController: sessionController, itemTagId: itemTagId ) - viewModel.queueNumber = "ABCDEFGH" - viewModel.validateQueueNumberLength() + viewModel.description = String(repeating: "x", count: 1500) + viewModel.validateDescriptionLength() - #expect(viewModel.queueNumber == "ABC") + #expect(viewModel.description.count == 1000) } @Test @@ -218,18 +260,16 @@ struct ItemTagEditViewModelTest { // swiftlint:disable:this type_body_length let viewModel = ItemTagEditViewModel( itemTagRepository: itemTagRepository, messageBus: messageBus, - sessionController: sessionController, itemTagId: itemTagId ) - // Load the item tag first let reloadTask = Task { viewModel.reload() } await reloadTask.value - // Change to new queue number - viewModel.queueNumber = "B02" + viewModel.name = "Updated name" + viewModel.description = "Updated description" let updateTask = Task { viewModel.updateItemTag() @@ -240,11 +280,11 @@ struct ItemTagEditViewModelTest { // swiftlint:disable:this type_body_length #expect(viewModel.shouldDismiss == true) #expect(messageBus.currentMessage != nil) #expect(messageBus.currentMessage?.level == .success) - #expect(messageBus.currentMessage?.message == .itemTagUpdated) + #expect(messageBus.currentMessage?.message == Strings.itemTagUpdated) - // Check that repository was updated let updatedItemTag = itemTagRepository.findBy(id: itemTagId) - #expect(updatedItemTag.queueNumber == "B02") + #expect(updatedItemTag.name == "Updated name") + #expect(updatedItemTag.description == "Updated description") } @Test @@ -254,22 +294,19 @@ struct ItemTagEditViewModelTest { // swiftlint:disable:this type_body_length let viewModel = ItemTagEditViewModel( itemTagRepository: itemTagRepository, messageBus: messageBus, - sessionController: sessionController, itemTagId: itemTagId ) - // Load the item tag first let reloadTask = Task { viewModel.reload() } await reloadTask.value - // Set error after loading let message = "Update failed" let httpResponseCode = 500 itemTagRepository.error = NativeAppTemplateAPIError.requestFailed(nil, httpResponseCode, message) - viewModel.queueNumber = "B02" + viewModel.name = "Updated" let updateTask = Task { viewModel.updateItemTag() @@ -290,23 +327,20 @@ struct ItemTagEditViewModelTest { // swiftlint:disable:this type_body_length let viewModel = ItemTagEditViewModel( itemTagRepository: itemTagRepository, messageBus: messageBus, - sessionController: sessionController, itemTagId: itemTagId ) - // Load the item tag first let reloadTask = Task { viewModel.reload() } await reloadTask.value - viewModel.queueNumber = "B02" + viewModel.name = "Updated" let updateTask = Task { viewModel.updateItemTag() } - // Check busy state immediately after starting #expect(viewModel.isBusy == viewModel.isUpdating) await updateTask.value @@ -314,37 +348,4 @@ struct ItemTagEditViewModelTest { // swiftlint:disable:this type_body_length #expect(viewModel.isBusy == false) #expect(viewModel.isUpdating == false) } - - @Test("Form validation with different maximum lengths", arguments: [2, 4, 6, 8]) - func formValidationWithDifferentMaxLengths(maxLength: Int) async { - sessionController.maximumQueueNumberLength = maxLength - itemTagRepository.setItemTags(itemTags: [testItemTag]) - - let viewModel = ItemTagEditViewModel( - itemTagRepository: itemTagRepository, - messageBus: messageBus, - sessionController: sessionController, - itemTagId: itemTagId - ) - - // Load the item tag first - let reloadTask = Task { - viewModel.reload() - } - await reloadTask.value - - // Test exactly at the limit - viewModel.queueNumber = String(repeating: "B", count: maxLength) - #expect(viewModel.hasInvalidDataQueueNumber == false) - - // Test one over the limit - viewModel.queueNumber = String(repeating: "B", count: maxLength + 1) - #expect(viewModel.hasInvalidDataQueueNumber == true) - - // Test truncation - viewModel.validateQueueNumberLength() - #expect(viewModel.queueNumber.count == maxLength) - #expect(viewModel.hasInvalidDataQueueNumber == false) - #expect(viewModel.hasInvalidData == false) // Should be valid since it's different from original - } } diff --git a/NativeAppTemplateTests/UI/Shop Settings/ItemTag List/ItemTagCreateViewModelTest.swift b/NativeAppTemplateTests/UI/Shop Settings/ItemTag List/ItemTagCreateViewModelTest.swift index 683b30e..f516cac 100644 --- a/NativeAppTemplateTests/UI/Shop Settings/ItemTag List/ItemTagCreateViewModelTest.swift +++ b/NativeAppTemplateTests/UI/Shop Settings/ItemTag List/ItemTagCreateViewModelTest.swift @@ -10,7 +10,6 @@ import Testing @MainActor @Suite struct ItemTagCreateViewModelTest { - let sessionController = TestSessionController() let itemTagRepository = TestItemTagRepository( itemTagsService: ItemTagsService() ) @@ -22,74 +21,106 @@ struct ItemTagCreateViewModelTest { let viewModel = ItemTagCreateViewModel( itemTagRepository: itemTagRepository, messageBus: messageBus, - sessionController: sessionController, shopId: shopId ) - #expect(viewModel.queueNumber == "") + #expect(viewModel.name == "") + #expect(viewModel.description == "") #expect(viewModel.isCreating == false) #expect(viewModel.shouldDismiss == false) #expect(viewModel.isBusy == false) } @Test - func maximumQueueNumberLength() { - sessionController.maximumQueueNumberLength = 5 + func maximumNameLength() { let viewModel = ItemTagCreateViewModel( itemTagRepository: itemTagRepository, messageBus: messageBus, - sessionController: sessionController, shopId: shopId ) - #expect(viewModel.maximumQueueNumberLength == 5) + #expect(viewModel.maximumNameLength == 100) } - @Test("Queue number validation - invalid cases", arguments: [ - ("", true), // blank - ("a", true), // too short - ("ab", false), // minimum valid - ("abc", false), // valid - ("abcd", false), // valid - ("abcde", false), // maximum valid (assuming max length 5) - ("abcdef", true), // too long (will be truncated but still invalid in this test) - ("ab!", true), // non-alphanumeric - ("a b", true), // contains space - ("12", false), // numbers are valid - ("a1", false) // alphanumeric is valid + @Test + func maximumDescriptionLength() { + let viewModel = ItemTagCreateViewModel( + itemTagRepository: itemTagRepository, + messageBus: messageBus, + shopId: shopId + ) + + #expect(viewModel.maximumDescriptionLength == 1000) + } + + @Test("Name validation", arguments: [ + ("", true), // blank β†’ invalid + ("a", false), // 1 char β†’ valid (was invalid pre-2A-3) + ("ab", false), // 2 chars β†’ valid + ("Buy milk πŸ₯›", false), // unicode + spaces β†’ valid (was invalid pre-2A-3) + ("Item with !@#$%^&* symbols", false), // symbols β†’ valid (was invalid pre-2A-3) + (String(repeating: "a", count: 100), false), // exactly 100 β†’ valid + (String(repeating: "a", count: 101), true) // 101 β†’ invalid ]) - func queueNumberValidation(queueNumber: String, shouldBeInvalid: Bool) { - sessionController.maximumQueueNumberLength = 5 + func nameValidation(name: String, shouldBeInvalid: Bool) { + + let viewModel = ItemTagCreateViewModel( + itemTagRepository: itemTagRepository, + messageBus: messageBus, + shopId: shopId + ) + viewModel.name = name + + #expect(viewModel.hasInvalidDataName == shouldBeInvalid) + } + + @Test("Description validation", arguments: [ + ("", false), // empty β†’ valid + ("Short note.", false), // short β†’ valid + (String(repeating: "x", count: 1000), false), // exactly 1000 β†’ valid + (String(repeating: "x", count: 1001), true) // 1001 β†’ invalid + ]) + func descriptionValidation(description: String, shouldBeInvalid: Bool) { let viewModel = ItemTagCreateViewModel( itemTagRepository: itemTagRepository, messageBus: messageBus, - sessionController: sessionController, shopId: shopId ) - viewModel.queueNumber = queueNumber + viewModel.description = description - #expect(viewModel.hasInvalidDataQueueNumber == shouldBeInvalid) - #expect(viewModel.hasInvalidData == shouldBeInvalid) + #expect(viewModel.hasInvalidDataDescription == shouldBeInvalid) } @Test - func validateQueueNumberLengthTruncatesCorrectly() { - sessionController.maximumQueueNumberLength = 4 + func validateNameLengthTruncatesCorrectly() { + + let viewModel = ItemTagCreateViewModel( + itemTagRepository: itemTagRepository, + messageBus: messageBus, + shopId: shopId + ) + viewModel.name = String(repeating: "a", count: 100) + "EXTRA" + viewModel.validateNameLength() + + #expect(viewModel.name == String(repeating: "a", count: 100)) + } + + @Test + func validateDescriptionLengthTruncatesCorrectly() { let viewModel = ItemTagCreateViewModel( itemTagRepository: itemTagRepository, messageBus: messageBus, - sessionController: sessionController, shopId: shopId ) - viewModel.queueNumber = "abcdefgh" - viewModel.validateQueueNumberLength() + viewModel.description = String(repeating: "x", count: 1500) + viewModel.validateDescriptionLength() - #expect(viewModel.queueNumber == "abcd") + #expect(viewModel.description.count == 1000) } @Test @@ -97,11 +128,11 @@ struct ItemTagCreateViewModelTest { let viewModel = ItemTagCreateViewModel( itemTagRepository: itemTagRepository, messageBus: messageBus, - sessionController: sessionController, shopId: shopId ) - viewModel.queueNumber = "ABC1" + viewModel.name = "Buy milk" + viewModel.description = "From the corner store." let createTask = Task { viewModel.createItemTag() @@ -111,9 +142,10 @@ struct ItemTagCreateViewModelTest { #expect(viewModel.shouldDismiss == true) #expect(messageBus.currentMessage != nil) #expect(messageBus.currentMessage?.level == .success) - #expect(messageBus.currentMessage?.message == .itemTagCreated) + #expect(messageBus.currentMessage?.message == Strings.itemTagCreated) #expect(itemTagRepository.itemTags.count == 1) - #expect(itemTagRepository.itemTags.first?.queueNumber == "ABC1") + #expect(itemTagRepository.itemTags.first?.name == "Buy milk") + #expect(itemTagRepository.itemTags.first?.description == "From the corner store.") } @Test @@ -125,11 +157,10 @@ struct ItemTagCreateViewModelTest { let viewModel = ItemTagCreateViewModel( itemTagRepository: itemTagRepository, messageBus: messageBus, - sessionController: sessionController, shopId: shopId ) - viewModel.queueNumber = "ABC1" + viewModel.name = "Buy milk" let createTask = Task { viewModel.createItemTag() @@ -148,44 +179,17 @@ struct ItemTagCreateViewModelTest { let viewModel = ItemTagCreateViewModel( itemTagRepository: itemTagRepository, messageBus: messageBus, - sessionController: sessionController, shopId: shopId ) - viewModel.queueNumber = "ABC1" + viewModel.name = "Buy milk" let createTask = Task { viewModel.createItemTag() } - // Check busy state immediately after starting #expect(viewModel.isBusy == viewModel.isCreating) await createTask.value } - - @Test("Form validation with different maximum lengths", arguments: [2, 4, 6, 8]) - func formValidationWithDifferentMaxLengths(maxLength: Int) { - sessionController.maximumQueueNumberLength = maxLength - - let viewModel = ItemTagCreateViewModel( - itemTagRepository: itemTagRepository, - messageBus: messageBus, - sessionController: sessionController, - shopId: shopId - ) - - // Test exactly at the limit - viewModel.queueNumber = String(repeating: "A", count: maxLength) - #expect(viewModel.hasInvalidData == false) - - // Test one over the limit - viewModel.queueNumber = String(repeating: "A", count: maxLength + 1) - #expect(viewModel.hasInvalidData == true) - - // Test truncation - viewModel.validateQueueNumberLength() - #expect(viewModel.queueNumber.count == maxLength) - #expect(viewModel.hasInvalidData == false) - } } diff --git a/NativeAppTemplateTests/UI/Shop Settings/ItemTag List/ItemTagListViewModelTest.swift b/NativeAppTemplateTests/UI/Shop Settings/ItemTag List/ItemTagListViewModelTest.swift index 85c5228..c6ce285 100644 --- a/NativeAppTemplateTests/UI/Shop Settings/ItemTag List/ItemTagListViewModelTest.swift +++ b/NativeAppTemplateTests/UI/Shop Settings/ItemTag List/ItemTagListViewModelTest.swift @@ -12,11 +12,11 @@ import Testing struct ItemTagListViewModelTest { var itemTags: [ItemTag] { [ - mockItemTag(id: "1", queueNumber: "A01"), - mockItemTag(id: "2", queueNumber: "A02"), - mockItemTag(id: "3", queueNumber: "A03"), - mockItemTag(id: "4", queueNumber: "B01"), - mockItemTag(id: "5", queueNumber: "B02") + mockItemTag(id: "1", name: "A01"), + mockItemTag(id: "2", name: "A02"), + mockItemTag(id: "3", name: "A03"), + mockItemTag(id: "4", name: "B01"), + mockItemTag(id: "5", name: "B02") ] } @@ -77,7 +77,7 @@ struct ItemTagListViewModelTest { ) #expect(viewModel.itemTags.count == 5) - #expect(viewModel.itemTags.first?.queueNumber == "A01") + #expect(viewModel.itemTags.first?.name == "A01") #expect(viewModel.isEmpty == false) } @@ -248,7 +248,7 @@ struct ItemTagListViewModelTest { #expect(viewModel.isDeleting == false) #expect(messageBus.currentMessage != nil) #expect(messageBus.currentMessage?.level == .success) - #expect(messageBus.currentMessage?.message == .itemTagDeleted) + #expect(messageBus.currentMessage?.message == Strings.itemTagDeleted) #expect(itemTagRepository.itemTags.count == 4) // One deleted #expect(itemTagRepository.itemTags.first { $0.id == itemTagIdToDelete } == nil) } @@ -277,7 +277,7 @@ struct ItemTagListViewModelTest { #expect(messageBus.currentMessage?.level == .error) #expect(messageBus.currentMessage?.autoDismiss == false) let errorMessage = try #require(messageBus.currentMessage?.message) - #expect(errorMessage.contains(String.itemTagDeletedError)) + #expect(errorMessage.contains(Strings.itemTagDeletedError)) #expect(itemTagRepository.itemTags.count == 5) // Nothing deleted } @@ -326,10 +326,10 @@ struct ItemTagListViewModelTest { #expect(viewModel.isShowingDeleteConfirmationDialog == true) } - private func mockItemTag(id: String = UUID().uuidString, queueNumber: String = "A01") -> ItemTag { + private func mockItemTag(id: String = UUID().uuidString, name: String = "A01") -> ItemTag { ItemTag( id: id, - queueNumber: queueNumber + name: name ) } @@ -340,9 +340,7 @@ struct ItemTagListViewModelTest { description: "This is a mock shop for testing", timeZone: "Tokyo", itemTagsCount: 10, - scannedItemTagsCount: 5, - completedItemTagsCount: 3, - displayShopServerPath: "https://api.nativeapptemplate.com/display/shops/\(id)?type=server" + completedItemTagsCount: 3 ) } } diff --git a/NativeAppTemplateTests/UI/Shop Settings/NumberTagsWebpageListViewModelTest.swift b/NativeAppTemplateTests/UI/Shop Settings/NumberTagsWebpageListViewModelTest.swift deleted file mode 100644 index b75a85e..0000000 --- a/NativeAppTemplateTests/UI/Shop Settings/NumberTagsWebpageListViewModelTest.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// NumberTagsWebpageListViewModelTest.swift -// NativeAppTemplate -// - -import Foundation -@testable import NativeAppTemplate -import Testing - -@MainActor -@Suite -struct NumberTagsWebpageListViewModelTest { - let messageBus = MessageBus() - - var shop: Shop { - mockShop(id: "test-shop-id", name: "Shop 1") - } - - @Test - func initializesCorrectly() { - let viewModel = NumberTagsWebpageListViewModel( - shop: shop, - messageBus: messageBus - ) - - #expect(viewModel.shop.id == shop.id) - #expect(viewModel.shop.name == shop.name) - #expect(viewModel.shop.displayShopServerPath == shop.displayShopServerPath) - } - - @Test - func shopPropertyIsAccessible() { - let viewModel = NumberTagsWebpageListViewModel( - shop: shop, - messageBus: messageBus - ) - - #expect(viewModel.shop == shop) - #expect(viewModel.shop.displayShopServerUrl.absoluteString.contains("test-shop-id")) - } - - @Test - func copyWebpageUrlPostsSuccessMessage() { - let viewModel = NumberTagsWebpageListViewModel( - shop: shop, - messageBus: messageBus - ) - - let testUrl = "https://example.com/test-url" - - viewModel.copyWebpageUrl(testUrl) - - #expect(messageBus.currentMessage != nil) - #expect(messageBus.currentMessage?.level == .success) - #expect(messageBus.currentMessage?.message == .webpageUrlCopied) - } - - @Test("Copy webpage URL with different URLs", arguments: [ - "https://api.nativeapptemplate.com/display/shops/123?type=server", - "https://example.com/test", - "http://localhost:3000/path", - "https://shop.example.com/page?param=value" - ]) - func copyWebpageUrlWithDifferentUrls(url: String) { - let localMessageBus = MessageBus() - let viewModel = NumberTagsWebpageListViewModel( - shop: shop, - messageBus: localMessageBus - ) - - viewModel.copyWebpageUrl(url) - - #expect(localMessageBus.currentMessage != nil) - #expect(localMessageBus.currentMessage?.level == .success) - #expect(localMessageBus.currentMessage?.message == .webpageUrlCopied) - } - - @Test - func copyWebpageUrlWithEmptyString() { - let viewModel = NumberTagsWebpageListViewModel( - shop: shop, - messageBus: messageBus - ) - - viewModel.copyWebpageUrl("") - - #expect(messageBus.currentMessage != nil) - #expect(messageBus.currentMessage?.level == .success) - #expect(messageBus.currentMessage?.message == .webpageUrlCopied) - } - - @Test - func multipleMessagesClearPrevious() { - let viewModel = NumberTagsWebpageListViewModel( - shop: shop, - messageBus: messageBus - ) - - // First copy - viewModel.copyWebpageUrl("https://first.com") - let firstMessage = messageBus.currentMessage - - // Second copy - viewModel.copyWebpageUrl("https://second.com") - let secondMessage = messageBus.currentMessage - - #expect(firstMessage != nil) - #expect(secondMessage != nil) - #expect(firstMessage?.message == .webpageUrlCopied) - #expect(secondMessage?.message == .webpageUrlCopied) - #expect(firstMessage?.level == .success) - #expect(secondMessage?.level == .success) - } - - private func mockShop(id: String = UUID().uuidString, name: String = "Mock Shop") -> Shop { - Shop( - id: id, - name: name, - description: "This is a mock shop for testing", - timeZone: "Tokyo", - itemTagsCount: 10, - scannedItemTagsCount: 5, - completedItemTagsCount: 3, - displayShopServerPath: "/display/shops/\(id)?type=server" - ) - } -} diff --git a/NativeAppTemplateTests/UI/Shop Settings/ShopBasicSettingsViewModelTest.swift b/NativeAppTemplateTests/UI/Shop Settings/ShopBasicSettingsViewModelTest.swift index 71a4284..4d75ace 100644 --- a/NativeAppTemplateTests/UI/Shop Settings/ShopBasicSettingsViewModelTest.swift +++ b/NativeAppTemplateTests/UI/Shop Settings/ShopBasicSettingsViewModelTest.swift @@ -85,6 +85,127 @@ struct ShopBasicSettingsViewModelTest { #expect(viewModel.hasInvalidData == (name == "Shop 1" ? true : false)) } + @Test + func maximumNameLength() { + let viewModel = ShopBasicSettingsViewModel( + sessionController: sessionController, + shopRepository: shopRepository, + messageBus: messageBus, + shopId: shopId + ) + + #expect(viewModel.maximumNameLength == 100) + } + + @Test + func maximumDescriptionLength() { + let viewModel = ShopBasicSettingsViewModel( + sessionController: sessionController, + shopRepository: shopRepository, + messageBus: messageBus, + shopId: shopId + ) + + #expect(viewModel.maximumDescriptionLength == 1_000) + } + + @Test("Name validation", arguments: [ + ("", true), // blank β†’ invalid + ("a", false), // 1 char β†’ valid + ("Shop Name 1", false), // normal β†’ valid + (String(repeating: "a", count: 100), false), // exactly 100 β†’ valid + (String(repeating: "a", count: 101), true) // 101 β†’ invalid + ]) + func nameValidation(name: String, shouldBeInvalid: Bool) async { + shopRepository.setShops(shops: shops) + + let viewModel = ShopBasicSettingsViewModel( + sessionController: sessionController, + shopRepository: shopRepository, + messageBus: messageBus, + shopId: shopId + ) + + let reloadTask = Task { + viewModel.reload() + } + await reloadTask.value + + viewModel.name = name + + #expect(viewModel.hasInvalidDataName == shouldBeInvalid) + } + + @Test("Description validation", arguments: [ + ("", false), // empty β†’ valid + ("Short note.", false), // short β†’ valid + (String(repeating: "x", count: 1000), false), // exactly 1000 β†’ valid + (String(repeating: "x", count: 1001), true) // 1001 β†’ invalid + ]) + func descriptionValidation(description: String, shouldBeInvalid: Bool) async { + shopRepository.setShops(shops: shops) + + let viewModel = ShopBasicSettingsViewModel( + sessionController: sessionController, + shopRepository: shopRepository, + messageBus: messageBus, + shopId: shopId + ) + + let reloadTask = Task { + viewModel.reload() + } + await reloadTask.value + + viewModel.description = description + + #expect(viewModel.hasInvalidDataDescription == shouldBeInvalid) + } + + @Test + func validateNameLengthTruncatesCorrectly() async { + shopRepository.setShops(shops: shops) + + let viewModel = ShopBasicSettingsViewModel( + sessionController: sessionController, + shopRepository: shopRepository, + messageBus: messageBus, + shopId: shopId + ) + + let reloadTask = Task { + viewModel.reload() + } + await reloadTask.value + + viewModel.name = String(repeating: "a", count: 100) + "EXTRA" + viewModel.validateNameLength() + + #expect(viewModel.name == String(repeating: "a", count: 100)) + } + + @Test + func validateDescriptionLengthTruncatesCorrectly() async { + shopRepository.setShops(shops: shops) + + let viewModel = ShopBasicSettingsViewModel( + sessionController: sessionController, + shopRepository: shopRepository, + messageBus: messageBus, + shopId: shopId + ) + + let reloadTask = Task { + viewModel.reload() + } + await reloadTask.value + + viewModel.description = String(repeating: "x", count: 1500) + viewModel.validateDescriptionLength() + + #expect(viewModel.description.count == 1_000) + } + @Test func reload() async throws { shopRepository.setShops(shops: shops) @@ -129,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) } @@ -170,7 +291,7 @@ struct ShopBasicSettingsViewModelTest { #expect(latestShop.timeZone == newTimeZone) #expect(latestShop.description == newDescription) - let message = String.basicSettingsUpdated + let message = Strings.basicSettingsUpdated #expect(viewModel.messageBus.currentMessage?.message == message) #expect(viewModel.isUpdating == false) @@ -214,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) @@ -227,9 +348,7 @@ struct ShopBasicSettingsViewModelTest { description: "This is a mock shop for testing", timeZone: "Tokyo", itemTagsCount: 10, - scannedItemTagsCount: 5, - completedItemTagsCount: 3, - displayShopServerPath: "https://api.nativeapptemplate.com/display/shops/\(id)?type=server" + completedItemTagsCount: 3 ) } } diff --git a/NativeAppTemplateTests/UI/Shop Settings/ShopSettingsViewModelTest.swift b/NativeAppTemplateTests/UI/Shop Settings/ShopSettingsViewModelTest.swift index a255869..0a3901a 100644 --- a/NativeAppTemplateTests/UI/Shop Settings/ShopSettingsViewModelTest.swift +++ b/NativeAppTemplateTests/UI/Shop Settings/ShopSettingsViewModelTest.swift @@ -96,85 +96,12 @@ 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) } - @Test - func resetShop() async throws { - shopRepository.setShops(shops: shops) - - let viewModel = ShopSettingsViewModel( - sessionController: sessionController, - shopRepository: shopRepository, - itemTagRepository: itemTagRepository, - messageBus: messageBus, - shopId: shopId - ) - - // https://stackoverflow.com/a/75618551/1160200 - let reloadTask = Task { - viewModel.reload() - } - await reloadTask.value - - let shop = try #require(shops.first { $0.id == shopId }) - #expect(viewModel.shop == shop) - - // https://stackoverflow.com/a/75618551/1160200 - let resetShopTask = Task { - viewModel.resetShop() - } - await resetShopTask.value - - let message = String.shopReset - - #expect(viewModel.messageBus.currentMessage?.message == message) - #expect(viewModel.isResetting) - #expect(viewModel.isBusy) - #expect(viewModel.shouldDismiss) - } - - @Test - func resetShopFailed() async throws { - shopRepository.setShops(shops: shops) - - let viewModel = ShopSettingsViewModel( - sessionController: sessionController, - shopRepository: shopRepository, - itemTagRepository: itemTagRepository, - messageBus: messageBus, - shopId: shopId - ) - - // https://stackoverflow.com/a/75618551/1160200 - let reloadTask = Task { - viewModel.reload() - } - await reloadTask.value - - let shop = try #require(shops.first { $0.id == shopId }) - #expect(viewModel.shop == shop) - - let message = "Internal server error." - let httpResponseCode = 500 - shopRepository.error = NativeAppTemplateAPIError.requestFailed(nil, httpResponseCode, message) - - // https://stackoverflow.com/a/75618551/1160200 - let resetShopTask = Task { - viewModel.resetShop() - } - await resetShopTask.value - - #expect(viewModel.messageBus.currentMessage?.message == - "\(String.shopResetError) [NATI-2001] \(message) [Status: \(httpResponseCode)]") - #expect(viewModel.isResetting) - #expect(viewModel.isBusy) - #expect(viewModel.shouldDismiss) - } - @Test func destroyShop() async throws { shopRepository.setShops(shops: shops) @@ -241,7 +168,7 @@ struct ShopSettingsViewModelTest { await destroyShopTask.value #expect(viewModel.messageBus.currentMessage?.message == - "\(String.shopDeletedError) [NATI-2001] \(message) [Status: \(httpResponseCode)]") + "\(Strings.shopDeletedError) [NATIVEAPPTEMPLATE-2001] \(message) [Status: \(httpResponseCode)]") #expect(viewModel.isDeleting) #expect(viewModel.isBusy) #expect(sessionController.userState == .notLoggedIn) @@ -254,9 +181,7 @@ struct ShopSettingsViewModelTest { description: "This is a mock shop for testing", timeZone: "Tokyo", itemTagsCount: 10, - scannedItemTagsCount: 5, - completedItemTagsCount: 3, - displayShopServerPath: "https://api.nativeapptemplate.com/display/shops/\(id)?type=server" + completedItemTagsCount: 3 ) } } diff --git a/NativeAppTemplateTests/Utilities/AppErrorTest.swift b/NativeAppTemplateTests/Utilities/AppErrorTest.swift index 64403a6..d1198d8 100644 --- a/NativeAppTemplateTests/Utilities/AppErrorTest.swift +++ b/NativeAppTemplateTests/Utilities/AppErrorTest.swift @@ -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 @@ -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 @@ -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")) @@ -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") } } diff --git a/NativeAppTemplateTests/Utilities/CodedErrorTest.swift b/NativeAppTemplateTests/Utilities/CodedErrorTest.swift index bce9ad5..c68198c 100644 --- a/NativeAppTemplateTests/Utilities/CodedErrorTest.swift +++ b/NativeAppTemplateTests/Utilities/CodedErrorTest.swift @@ -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 @@ -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") } } diff --git a/NativeAppTemplateTests/Utilities/MessageBusTest.swift b/NativeAppTemplateTests/Utilities/MessageBusTest.swift index df158b8..0839d85 100644 --- a/NativeAppTemplateTests/Utilities/MessageBusTest.swift +++ b/NativeAppTemplateTests/Utilities/MessageBusTest.swift @@ -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) } diff --git a/NativeAppTemplateTests/Utilities/NFCErrorTest.swift b/NativeAppTemplateTests/Utilities/NFCErrorTest.swift deleted file mode 100644 index e6812b2..0000000 --- a/NativeAppTemplateTests/Utilities/NFCErrorTest.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// 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 index dce12bf..c156e3a 100644 --- a/NativeAppTemplateTests/Utilities/NativeAppTemplateAPIErrorTest.swift +++ b/NativeAppTemplateTests/Utilities/NativeAppTemplateAPIErrorTest.swift @@ -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 @@ -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]") } } diff --git a/NativeAppTemplateTests/Utilities/UtilityTest.swift b/NativeAppTemplateTests/Utilities/UtilityTest.swift deleted file mode 100644 index 61e123f..0000000 --- a/NativeAppTemplateTests/Utilities/UtilityTest.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// UtilityTest.swift -// NativeAppTemplate -// - -import Foundation -@testable import NativeAppTemplate -import Testing - -struct UtilityTest { - @Test - func scanUrlContainsExpectedParams() { - let url = Utility.scanUrl(itemTagId: "abc-123", itemTagType: "server") - - #expect(url.absoluteString.contains("item_tag_id=abc-123")) - #expect(url.absoluteString.contains("type=server")) - } - - @Test - func scanUrlCustomerType() { - let url = Utility.scanUrl(itemTagId: "xyz-456", itemTagType: "customer") - - #expect(url.absoluteString.contains("item_tag_id=xyz-456")) - #expect(url.absoluteString.contains("type=customer")) - } - - @Test(arguments: [ - ("", true), - (" ", true), - ("\n", true), - (" \t\n ", true), - ("hello", false), - (" hello ", false) - ]) - func isBlank(text: String, expected: Bool) { - #expect(Utility.isBlank(text) == expected) - } - - @Test(arguments: [ - ("test@example.com", true), - ("user.name+tag@domain.co", true), - ("user@domain.com", true), - ("", false), - ("notanemail", false), - ("@domain.com", false), - ("user@", false), - ("user@.com", false) - ]) - func validateEmail(email: String, expected: Bool) { - #expect(Utility.validateEmail(email) == expected) - } - -} diff --git a/README.md b/README.md index d7457be..188b2f3 100644 --- a/README.md +++ b/README.md @@ -35,12 +35,10 @@ NativeAppTemplate-Free-iOS uses modern iOS development tools and practices, incl - Email Confirmation - Forgot Password - CRUD Operations for Shops (Create/Read/Update/Delete) -- CRUD Operations for Shops' Nested Resource, Number Tags (ItemTags) (Create/Read/Update/Delete) +- CRUD Operations for Shops' Nested Resource, Item Tags (Create/Read/Update/Delete) - Force App Version Update - Force Privacy Policy Version Update - Force Terms of Use Version Update -- Generate QR Code Image for Number Tags (ItemTags) with a Centered Number -- NFC features for Number Tags (ItemTags): Write Application Info to a Tag, Read a Tag, Background Tag Reading - And more! ## NFC Tag Operations @@ -144,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 = -NATEMPLATE_API_PORT = 3000 +NATIVEAPPTEMPLATE_API_SCHEME = http +NATIVEAPPTEMPLATE_API_DOMAIN = +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/.xcuserdatad/xcschemes/` so your API settings are not committed. If Xcode staged a deletion of the previously shared scheme, restore it with: @@ -159,6 +157,8 @@ git restore --source=HEAD --staged --worktree NativeAppTemplate.xcodeproj/xcshar Debug builds read these at launch via `ProcessInfo.processInfo.environment` in `Constants.swift`; when unset, they fall back to the production defaults (`https://api.nativeapptemplate.com`). Release builds always use the production defaults. +In practice, only Xcode injects these env vars (via the scheme), so a Debug build launched any other way β€” tapped from the Home Screen (SpringBoard), opened on a physical device after Xcode disconnects, etc. β€” sees them unset and falls through to the production defaults. The hardcoded fallbacks are what keep the app working without Xcode in the loop. + ## SwiftLint SwiftLint runs as part of the build process in Xcode, and errors/warnings are surfaced in Xcode as well. Please ensure that you run SwiftLint before submitting a pull request. diff --git a/docs/phase2-prestep-number-tag-rename.md b/docs/phase2-prestep-number-tag-rename.md deleted file mode 100644 index 1d24c4a..0000000 --- a/docs/phase2-prestep-number-tag-rename.md +++ /dev/null @@ -1,252 +0,0 @@ -# Phase 2 Pre-step: Rename "Number Tag" Labels to "Item Tag" (iOS Paid) - -**Repo:** `~/pg/iphone/NativeAppTemplate` -**Branch:** `main` only (NOT `v1-with-nfc`) -**Goal:** Align UI labels with the `ItemTag` identifier, so the agent's humanize-based string-literal rename logic (Phase 6) can rewrite them consistently alongside the identifier. - -## Context - -The Rails API substrate (Phase 1) renamed `queue_number` β†’ `name` at the data layer. The iOS client still shows "Number Tag" and "Tag Number" labels in its UI, which do not align with the `ItemTag` identifier. For the agent to rename UI strings automatically by applying humanize/pluralize rules to the identifier, the UI label must match: - -- Identifier: `ItemTag` β†’ humanized: `"Item Tag"` (singular) / `"Item Tags"` (plural) -- Identifier: `itemTag.name` β†’ UI label: `"Name"` - -## Scope - -**In scope** (rename in this commit): -- Generic "Number Tag" labels that describe the ItemTag entity itself -- The field label "Tag Number" that corresponds to `itemTag.name` - -**Out of scope** (keep as-is; will be deleted in Phase 2 Part A alongside NFC/scan removal): -- Queue-flow specific messages ("Swipe a number tag below", "Server Number Tags Webpage", etc.) -- Onboarding descriptions explaining the queue flow -- Scan-related strings ("Read a NFC Number Tag...") -- Reset-related strings ("Reset Number Tags", "All number tags reset") - -The rationale: queue-specific strings will be removed entirely when the corresponding NFC/scan/reset code is deleted in Phase 2. Renaming them now would be churn. - -## v1-with-nfc branch - -**Do NOT modify.** The `v1-with-nfc` branch is preserved as an immutable queue-template snapshot for potential future use. Queue-specific "Number Tag" labels are semantically correct in that context. - ---- - -## Execution Steps - -### Step 1: Baseline check - -```bash -cd ~/pg/iphone/NativeAppTemplate - -# Confirm on main and clean -git branch --show-current -git status - -# Confirm baseline build is green -xcodebuild build -scheme NativeAppTemplate \ - -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' 2>&1 | tail -3 -``` - -Expected: `** BUILD SUCCEEDED **` - -### Step 2: Pre-check grep - -Find all occurrences of "Number Tag" labels and the `tagNumber*` identifiers in the codebase (NOT just Constants.swift). This reveals if any Swift file uses these strings directly, bypassing the Constants.swift pattern. - -```bash -# Swift files using "Number Tag" as string literal -grep -rn "Number Tag" --include="*.swift" . | grep -v "\.build\|DerivedData" - -# Swift files using "Tag Number" or "tag number" as string literal -grep -rn "Tag Number\|tag number" --include="*.swift" . | grep -v "\.build\|DerivedData" - -# Identifier usage -grep -rn "shopSettingsManageNumberTagsLabel\|tagNumber\b\|addTagDescription\b\|tagNumberIsInvalid\b" \ - --include="*.swift" . | grep -v "\.build\|DerivedData" -``` - -Save the output for reference. The rename in later steps must cover every match from these greps (except those in out-of-scope Constants.swift entries). - -### Step 3: Edit Constants.swift (Category B β€” in-scope labels only) - -In `NativeAppTemplate/Constants.swift`, apply these exact changes: - -**Line 176** (identifier + value): -```swift -// BEFORE -static let shopSettingsManageNumberTagsLabel = "Manage Number Tags" - -// AFTER -static let shopSettingsManageItemTagsLabel = "Manage Item Tags" -``` - -**Line 188** (value only β€” identifier stays as `tagNumber` for now to minimize churn; will be reconsidered if needed): -```swift -// BEFORE -static let tagNumber = "Tag Number" - -// AFTER β€” value only changes; identifier may be updated if it doesn't break too many references -static let tagNumber = "Name" -``` - -**Note on line 188 identifier**: Renaming the identifier `tagNumber` β†’ something else (e.g. `itemTagName`) would cascade into many files. Keeping the identifier and changing only the displayed string is safer for this small commit. The identifier rename can be tackled as a follow-up commit or during Phase 2 Part A's ItemTag refactor. Document this decision as a comment or in the commit message. - -**Line 191** (value only): -```swift -// BEFORE -static let addTagDescription = "Add a new number tag and start changing the tag status." - -// AFTER -static let addTagDescription = "Add a new item tag and start changing the tag status." -``` - -**Line 194** (value only): -```swift -// BEFORE -static let tagNumberIsInvalid = "Tag number is invalid." - -// AFTER -static let tagNumberIsInvalid = "Item tag name is invalid." -``` - -**Do NOT change these** (Category A β€” out of scope, will be deleted in Phase 2): -- Line 166: `swipeNumberTagBelow` -- Line 168: `serverNumberTagsWebpageWillBeUpdated` -- Line 170: `serverNumberTagsWebpage` -- Line 177: `shopSettingsNumberTagsWebpageLabel` -- Line 178: `resetNumberTagsDescription` -- Line 179: `resetNumberTags` -- Line 181: `// MARK: Number Tags Web Pages` (comment β€” delete with section in Phase 2) -- Line 209: `completeScanHelp` -- Line 210: `showTagInfoScanHelp` -- Line 296: `shopReset` -- Line 297: `shopResetError` -- Line 396, 403, 404, 406: `onboardingDescription*` (queue flow) - -### Step 4: Update callers of `shopSettingsManageNumberTagsLabel` - -Since this identifier changed, all callers need updating. Grep and replace: - -```bash -grep -rn "shopSettingsManageNumberTagsLabel" --include="*.swift" . | grep -v "\.build\|DerivedData" -``` - -For each match, replace `shopSettingsManageNumberTagsLabel` with `shopSettingsManageItemTagsLabel`. - -Typical places to check: -- `NativeAppTemplate/UI/Shop Settings/` (multiple Swift files likely reference this) -- Any test files under `NativeAppTemplateTests/UI/Shop Settings/` - -### Step 5: Verify no other "Number Tag" or "Tag Number" direct literals in scope - -```bash -# Any Swift file still contains "Number Tag" outside of Constants.swift's intentional kept strings -grep -rn "Number Tag" --include="*.swift" . | grep -v "\.build\|DerivedData" | grep -v "Constants.swift" - -# Any Swift file still contains "Tag Number" as a literal -grep -rn "Tag Number" --include="*.swift" . | grep -v "\.build\|DerivedData" -``` - -If matches are found, evaluate each: -- If the context is queue-specific (scan flow, reset flow, NFC) β†’ leave it (out of scope) -- If the context is generic (ItemTag management) β†’ update to "Item Tag" or "Name" - -### Step 6: Build green - -```bash -xcodebuild build -scheme NativeAppTemplate \ - -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' 2>&1 | tail -10 -``` - -Expected: `** BUILD SUCCEEDED **` - -If build fails, most likely cause is an unresolved reference to the renamed identifier. Re-run Step 4's grep and fix missed callers. - -### Step 7: Test green - -```bash -xcodebuild test -scheme NativeAppTemplate \ - -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' 2>&1 | tail -20 -``` - -Expected: all tests pass. If any test asserts the old string values (e.g. `#expect(label == "Manage Number Tags")`), update the test to assert `"Manage Item Tags"`. - -### Step 8: Commit - -```bash -git add -A -git diff --cached --stat # Verify scope is what you expect -git commit -m "Rename Number Tag labels to Item Tag for identifier alignment - -- 'Manage Number Tags' β†’ 'Manage Item Tags' -- 'Tag Number' β†’ 'Name' (aligns with ItemTag.name field after Phase 1 API refactor) -- 'Add a new number tag...' β†’ 'Add a new item tag...' -- 'Tag number is invalid.' β†’ 'Item tag name is invalid.' -- Identifier: shopSettingsManageNumberTagsLabel β†’ shopSettingsManageItemTagsLabel - -Queue-specific strings (swipeNumberTagBelow, serverNumberTagsWebpage*, -resetNumberTags*, onboardingDescription*) are intentionally kept for now; -they will be deleted alongside NFC/scan/reset code in Phase 2 Part A." -``` - -### Step 9: create PR - -create PR - -### Step 10: Verify v1-with-nfc is untouched - -```bash -git log v1-with-nfc --oneline -3 -``` - -The v1-with-nfc branch should NOT have this rename commit. Confirmed by the output showing only the original queue-template commits. - ---- - -## Completion Checklist - -- [ ] Baseline build was green before changes -- [ ] Constants.swift: 4 values updated, 1 identifier renamed -- [ ] All callers of `shopSettingsManageNumberTagsLabel` updated to new name -- [ ] `grep -rn "Number Tag" --include="*.swift"` in main app code returns only out-of-scope Constants entries -- [ ] `grep -rn "Tag Number" --include="*.swift"` returns only the `tagNumber` identifier (not string literals) -- [ ] Build green after changes (`** BUILD SUCCEEDED **`) -- [ ] Tests green -- [ ] 1 commit to `main` with descriptive message -- [ ] Pushed to `origin/main` -- [ ] `v1-with-nfc` branch unchanged - ---- - -## Common Pitfalls - -### 1. Forgetting to update test fixtures - -If any test file hard-codes `"Manage Number Tags"` as an expected label, the test will fail after the Constants value change. Update these to `"Manage Item Tags"`. - -### 2. String catalog (.xcstrings) files - -The audit showed no `.xcstrings` or `Localizable.strings` files in this repo β€” strings are directly in Constants.swift. If these are discovered during grep (e.g., in build artifacts), ignore them. - -### 3. Identifier rename cascades - -The Swift compiler will catch missed references immediately via build errors. If build fails after Step 4, read the error location and update that caller. Do not revert the change β€” fix forward. - -### 4. Commit message length - -The commit message above is descriptive. Feel free to shorten if preferred β€” but keeping the "Queue-specific strings kept for Phase 2" note helps future readers understand why some "Number Tag" strings remain. - -### 5. Accidentally changing v1-with-nfc - -If at any point you're unsure which branch you're on, run `git branch --show-current`. All work for this checklist must happen on `main`. Never run `git push origin v1-with-nfc` during this work. - ---- - -## After this Pre-step - -Proceed to Phase 2 Part A (to be written after this is merged): -- Delete NFCManager.swift, QRCodeGenerator.swift, ScanView, ScanViewModel -- Refactor ItemTag model (remove queueNumber, scanState, customerReadAt, alreadyCompleted; add description, position) -- Remove NFC entries from Info.plist and entitlements -- Remove queue-specific Constants entries (the Category A ones kept in this pre-step) -- Update ItemTag UI to use new schema