Skip to content

Substrate v2: Generic CRUD app foundation#47

Open
dadachi wants to merge 25 commits intomainfrom
substrate-v2
Open

Substrate v2: Generic CRUD app foundation#47
dadachi wants to merge 25 commits intomainfrom
substrate-v2

Conversation

@dadachi
Copy link
Copy Markdown
Contributor

@dadachi dadachi commented Apr 26, 2026

Summary

Promotes the `substrate-v2` working branch to `main`. After this merge, the Free iOS app is a generic parent-child CRUD shell (Shops + ItemTags) with no NFC / QR / scan dependency, ready to fork for arbitrary domains.

This rolls up six already-reviewed PRs that have already landed on `substrate-v2`:

PR Subject
#42 Phase 2A-1: Port ItemTag schema update from Paid iOS
#43 Phase 2A-2: Remove NFC / QR / Scan + restore swipe + rename reset → idle
#44 Silence success toasts on item-tag complete/idle
#45 Phase 2A-3: Generic CRUD UI for ItemTag
#46 Display completedAt as yyyy/MM/dd HH:mm regardless of locale

High-level changes

  • NFC / QR / Scan removed: `NFCManager`, `QRCodeGenerator`, `AppSingletons`, `NFCError`, the entire `UI/Scan/` directory, and NFC-only models (`ItemTagType`, `ItemTagData`, `ScanState`, `CompleteScanResult`, `ShowTagInfoScanResult`, `ItemTagInfoFromNdefMessage`) are gone. `Info.plist` and entitlements no longer request NFC / Photo-library permissions. The Scan tab is gone — 2-tab layout (Shops, Settings).
  • ItemTag is now a generic entity: `name` (1–100 chars, any string incl. unicode/symbols), optional `description` (0–1000 chars, multi-line), `state` (idled / completed), `position` (server-assigned, non-optional). No queue-number / alphanumeric validation.
  • ItemTagDetailView rewritten with state badge, description display, completedAt timestamp, and `Mark as completed` / `Mark as idled` toggle button. Edit/Delete in toolbar.
  • ShopDetailView keeps swipe-to-complete and swipe-to-idle (idle replaces the old reset terminology); success toasts silenced — error toasts retained.
  • API rename: `ItemTagRepository.reset(id:)` → `idle(id:)`, `IdleItemTagRequest`, HTTP path `/item_tags/:id/reset` → `/item_tags/:id/idle`. Backend must expose the new path.
  • Locale-stable timestamps: completedAt renders as `yyyy/MM/dd HH:mm` everywhere via new `Date.cardDateTimeString`. `DateFormatter.formatter(for:)` factory pins `Locale(identifier: "en_US_POSIX")` to defend against non-Gregorian calendars.
  • Permissions meta: `maximumQueueNumberLength` → `maximumNameLength` (default 100, was 256). `PermissionsRequest` reads `meta["maximum_name_length"]`.

Backend coordination

The Rails backend must be on a release that:

  • Exposes `PATCH /shopkeeper/item_tags/:id/idle` (in addition to / instead of `/reset`).
  • Returns `maximum_name_length` in the permissions meta payload.
  • Auto-assigns `position` (the iOS adapter throws if missing).

Test plan

  • `xcodebuild build` — `** BUILD SUCCEEDED **`
  • `xcodebuild build-for-testing` — `** TEST BUILD SUCCEEDED **`
  • `make lint` — `0 violations` (SwiftLint + SwiftFormat)
  • `xcodebuild test` passes (please verify in Xcode with Cmd+U)
  • Manual simulator smoke test: sign in, create shop, create item tag with unicode/symbols + multi-line description, swipe to complete, swipe to idle, view detail (state badge + completedAt formatted as `yyyy/MM/dd HH:mm`), edit, delete; confirm 2-tab layout (Shops, Settings).

🤖 Generated with Claude Code

dadachi and others added 25 commits April 24, 2026 19:38
Ports nativeapptemplate/NativeAppTemplate-iOS#49
- ItemTag model: queueNumber → name; add description/position;
  remove scanState/customerReadAt/alreadyCompleted
- ItemTagAdapter: parse name/description/position; drop legacy attrs
- PermissionsRequest: maximum_queue_number_length meta now optional
  with default 256 (full removal deferred to 2A-3)
- SessionController / App.swift null stub: maximumQueueNumberLength
  default 0 → 256
- ViewModels: queueNumber → name; validateQueueNumberLength →
  validateNameLength; hasInvalidDataQueueNumber → hasInvalidDataName
- Drop 5 production call sites of the deleted fields
  (ShowTagInfoScanResultView, ScanViewModel, ShopDetailViewModel,
  ShopDetailCardView, MainViewModel) — each flagged
  // TODO: removed in Phase 2A-2
- Shop model: drop displayShopServerPath / displayShopServerUrl; drop
  "Open Server Number Tags Webpage" link from ShopDetailView; delete
  NumberTagsWebpageListView/ViewModel and the Shop Settings section
  that linked to it
- Tests: update helpers and adapter fixtures for the new schema;
  delete ScanViewModelTest (queue/NFC-specific, goes away in 2A-2),
  completeTagWhenAlreadyCompleted test (asserted the removed branch),
  and NumberTagsWebpageListViewModelTest

Main target and test target both compile. Some tests may fail at
runtime (deferred to Phase 2A-4 per plan).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-ios-paid-itemtag-schema

Phase 2A-1: Port ItemTag schema update from paid iOS
Ports nativeapptemplate/NativeAppTemplate-iOS#50
to the Free iOS app. Drops all NFC, QR-code, and Scan-tab functionality;
ItemTagDetailView becomes a placeholder until Phase 2A-3 lands the full
generic CRUD UI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reverts the upstream PR's removal of the complete/idle swipe actions on
ShopDetailView so users can still mark items completed or send them back
to idled without going through Settings.

Renames the user-facing "reset" terminology to "idle" since the action
just transitions a completed tag back to the idled state:

- ItemTag UI/ViewModel/strings: resetTag → idleTag, isResetting → isIdling,
  itemTagReset[Error] → itemTagIdled[Error], button label "Reset" → "Idle".
- ItemTag repository layer: ItemTagRepositoryProtocol.reset → idle,
  ItemTagRepository / DemoItemTagRepository / TestItemTagRepository updated.
- ItemTag network layer: ResetItemTagRequest → IdleItemTagRequest,
  ItemTagsService.resetItemTag → idleItemTag, HTTP path
  /shopkeeper/item_tags/:id/reset → /shopkeeper/item_tags/:id/idle.

Backend must expose PATCH /shopkeeper/item_tags/:id/idle.

Shop repository / network layer left as `reset` (no UI surfaces it).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…an-removal

Phase 2A-2: Remove NFC / QR / Scan
Ports nativeapptemplate/NativeAppTemplate-iOS#52.
The swipe action already reflects the state change via ShopDetailCardView's
re-render after reload(); the extra success toast was redundant noise.
Errors still post to the message bus so users still see feedback when
something actually fails.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ag-success-toasts

Silence success toasts on item-tag complete/idle in ShopDetailView
Ports nativeapptemplate/NativeAppTemplate-iOS#53
to the Free iOS app. Replaces the Phase 2A-2 ItemTagDetailView placeholder
with a full generic CRUD UI; relaxes Create/Edit validation; adds a
description field; makes ItemTag.position non-optional.

Schema:
- ItemTag.position: Int? -> Int = 0; toJson() excludes position (server
  auto-assigns).
- ItemTagAdapter strict-guards position; throws invalidOrMissingAttributes
  when missing.
- Mock ItemTag(position: nil) -> position: 1 across tests.

Rename:
- maximumQueueNumberLength -> maximumNameLength across SessionController,
  protocol, App.swift NullSessionController, TestSessionController,
  PermissionsRequest. Default fallback 256 -> 100.
- PermissionsRequest reads meta["maximum_name_length"].

Create + Edit:
- Validation: name 1-100 chars (any string), description 0-1000 chars.
  Drops alphanumeric and count >= 2 checks; standard keyboard.
- New description field with multi-line TextEditor.
- Edit's hasInvalidData also requires name OR description changed.

Detail view full rewrite:
- State badge (IdlingTag / CompletedTag), description display,
  completedAt timestamp.
- Toggle button (Mark as completed / Mark as idled) calling
  ItemTagRepository.complete(id:) / idle(id:).
- New isToggling, completeItemTag(), idleItemTag(); failure posts error
  toast, success silently re-renders (matching PR #52).

ItemTagListCardView: name + description preview (2 lines) + state badge +
completedAt.

Constants: removed tagNumber/tagNumberIsInvalid; added nameLabel,
descriptionLabel, itemTagNamePlaceholder, itemTagNameIsInvalid,
itemTagDescriptionIsInvalid, completedAtLabel, markAsCompleted,
markAsIdled, itemTagNameHelp/itemTagDescriptionHelp helpers, and
NativeAppTemplateConstants.maximumItemTagDescriptionLength = 1_000.

Deviations from upstream PR #53:
- Skipped permission gates (canUpdateShops/canDeleteShops/canCreateShops)
  - Free repo has no Permission system. Edit/delete/toggle/create are
  unconditionally available. Skipped the upstream permissionChecking
  parameterized tests for the same reason.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The new hasInvalidDataChangeDetection test in ItemTagEditViewModelTest
sets name = "Updated" (7 chars), which tripped the old default of 4
(carried over from the queue-number-length era). Match upstream Paid PR's
default of 100.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-ios-paid-generic-crud-ui

Phase 2A-3: Generic CRUD UI for ItemTag
Ports nativeapptemplate/NativeAppTemplate-iOS#54.
ItemTagDetailView's completed-timestamp row was using
Text(completedAt.formatted()), which is locale-dependent — same instant
rendered as 4/26/2026, 10:30 AM (en_US), 26/04/2026, 10:30 (en_GB), or
2026/04/26 10:30 (ja_JP). Non-Gregorian calendars (Buddhist, Japanese
imperial) could shift the year value entirely.

Introduces Date.cardDateTimeString that stitches existing cardDateString
+ cardTimeString format constants, and pins Locale(identifier:
"en_US_POSIX") on the shared DateFormatter.formatter(for:) factory so
every fixed-format formatter is calendar/locale-stable.

- DateFormatter+Extensions.swift:
  - cardDateString: "MMM dd yyyy" → "yyyy/MM/dd" (the MMM token was
    locale-aware; the constant had zero callers).
  - formatter(for:) factory: pins Locale(identifier: "en_US_POSIX").
- Date+Extensions.swift: new cardDateTimeString computed property.
- ItemTagDetailView, ShopDetailCardView, ItemTagListCardView: switched
  to cardDateTimeString.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-time-string

Display completedAt as yyyy/MM/dd HH:mm regardless of locale
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The associated-domains entitlement is no longer needed; the entitlements
file is now empty and dropped. CODE_SIGN_ENTITLEMENTS unset for both
Debug and Release configs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ports nativeapptemplate/NativeAppTemplate-iOS#59.

Two-step cleanup of maximumNameLength:

1. Stop reading maximum_name_length from /shopkeeper/permissions. The
   client already tolerated its absence via a "?? 100" fallback, so this
   is dead plumbing. Drops the field from PermissionsResponse, the meta
   read in PermissionsRequest.handle, and the assignment in
   SessionController.fetchPermissions.

2. Move maximumNameLength from SessionController to Constants. Now that
   it's a fixed value, it's just a constant — like
   maximumItemTagDescriptionLength. Adds
   NativeAppTemplateConstants.maximumItemTagNameLength = 100, drops
   maximumNameLength from SessionControllerProtocol / SessionController /
   NullSessionController / TestSessionController. ItemTagCreateViewModel
   and ItemTagEditViewModel now read the constant directly and no longer
   take sessionController. Updates the call sites in ItemTagListView /
   ItemTagDetailView. Tests rewritten to drop sessionController field
   and update truncation tests to use 100+ char strings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…mum-name-length-from-permissions

Drop maximum_name_length from permissions; move to Constants
Ports nativeapptemplate/NativeAppTemplate-iOS#60.
Mirror the ItemTag pattern from PR #49 for Shop. Server has no caps on
Shop name/description; this is a client-only UX guard.

- Constants: maximumShopNameLength = 100, maximumShopDescriptionLength
  = 1_000. New strings: shopNameIsInvalid, shopDescriptionIsInvalid,
  plus shopNameHelp(maximumLength:) / shopDescriptionHelp(maximumLength:)
  parametric helpers.
- ShopCreateViewModel + ShopBasicSettingsViewModel: split hasInvalidData
  into hasInvalidDataName + hasInvalidDataDescription, expose
  maximumNameLength / maximumDescriptionLength, add validateNameLength()
  / validateDescriptionLength().
- ShopCreateView + ShopBasicSettingsView: wire .onChange truncation on
  Name and Description; switch to two-line footer (always-visible help
  + conditional red "is invalid" text), matching ItemTagCreateView.
- Tests: added maximumNameLength, maximumDescriptionLength, parametric
  nameValidation / descriptionValidation, and the two truncation tests
  on both viewmodel test suites; replaced the old simple
  hasInvalidData(name:) parametric test in ShopCreateViewModelTest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-description-caps

Add client-side length caps + truncation for Shop name/description
- Drop scannedItemTagsCount from Shop model, adapter, demo repo, and tests; remove the "tags scanned by customers" stat from ShopListCardView.
- Replace ShopDetailView "Learn More" link with a left-aligned shopDetailInstruction ("Swipe an item tag to change its status.").
- Swap the OnboardingView "How to use" link for the Support Website link; trim onboarding from 13 to 8 descriptions and drop unused String(localized:) wrap.
- Normalize ItemTag string keys: editTag/addTag/deleteTag/etc. -> editItemTag/addItemTag/deleteItemTag/etc.
- Drop unused constants: howToUseUrl, learnMore, createShops, createTags.
…-scanned-tighten-itemtag-labels

Drop unused tags-scanned count and howToUse; tighten ItemTag labels
Convert the large `extension String` block in `Constants.swift` to a
caseless `enum Strings` namespace, matching the existing
`NativeAppTemplateConstants` idiom in the same file. Inline the
two `cardDateString`/`cardTimeString` literals in
`DateFormatter+Extensions.swift` and remove the small extension
they lived in. Update call sites across the app and tests from
`String.foo` to `Strings.foo`, including dot-shorthand uses for
`message:`, `text:`, `buttonTitle:`, and `?.message ==` parameters.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ng-constants-with-enum

Wrap String constants in enum Strings namespace
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant