Skip to content

feat: iOS feature parity (Phases 2–13) + documents folder-routing fix#2

Merged
Adron merged 19 commits into
mainfrom
dev
Jun 27, 2026
Merged

feat: iOS feature parity (Phases 2–13) + documents folder-routing fix#2
Adron merged 19 commits into
mainfrom
dev

Conversation

@Adron

@Adron Adron commented Jun 27, 2026

Copy link
Copy Markdown
Member

Summary

Brings the dev branch up to main, shipping the bulk of the InterlinedList iOS app (Phases 2–13) plus a correctness fix for document folders and a refresh of the repo's docs/tooling. 19 commits.

What's included

  • Documents — fix folder reads/writes to use the folder-scoped endpoints (/api/documents/folders/{id}/documents) and camelCase bodies, so documents created in a folder no longer land at root and folders no longer show unrelated root documents (6d7a4fb).
  • Compose & feed — cross-post, repost, scheduled-message editing, and feed search (423e0bb).
  • Lists — list watchers / collaboration (818dbb3).
  • Profile & social — follow lists, followers/following, public browse (a8355f1).
  • Organizations — organizations + member management (1cbfb3f).
  • Settings — settings panel, notification preferences, list schema editor (dd3e245).
  • Auth & accounts — hide native-unsupported GitHub sign-in, gate in-app provider linking (43fe38b); wire Change-email / close out Phase 2 (0ed9f68).
  • Subscription model — lock in the iOS free-app direction and hide subscriber-only UI (folders, compose controls, scheduling) for free users (159310c, 0a0d8c9, e9623a0, fc1fbf1).
  • API layer — APIClient endpoints + Codable models backing the GAP phases (fdfc5ad).
  • Docs & tooling — rewrite GAP-ENDPOINTS as under-documented contracts + GAP-APPLE ship guide (6e0f647); GAP-phase API/model test coverage (8e71192); README + CLAUDE.md overhaul, remove Codex assets, add /comment-and-commit + /commit-and-pr commands, disable xctestplan parallelization (6d7a4fb); build wiring + green suite (8d20d31).

Testing

  • xcodebuild ... test -only-testing:InterlinedListTests/APIClientDocumentsTests — 14 passed; build succeeded (this session).
  • Documents fix verified against the live /api/openapi.json contract; earlier phase commits landed with their own model/API test coverage.
  • Full-suite run not re-executed in this session — recommend a CI/⌘U pass before merge.

Caveats / follow-ups

  • The documents fix was validated against the spec + unit tests, not against the live API (creating documents would mutate production). Recommend a quick on-device retest: create folder → create doc inside → confirm placement on the website.
  • Known limitation: moving a document back to root via the Edit folder picker is a no-op (the encoder omits folderId: nil); moving into a folder works.

🤖 Generated with Claude Code

Adron and others added 19 commits June 24, 2026 10:32
iOS will not surface any subscription / billing UI. Subscriber-only
features are hidden for non-subscribers (no disable-with-paywall, no
"upgrade" CTA, no plan info). Subscription management lives entirely
on the web at interlinedlist.com.

- subscription-permissions-update.md: record user-confirmed answers
  (degraded subscribers see nothing; strict silence on copy; help
  links via SFSafariViewController OK; no subscription mention
  anywhere in the iOS bundle).
- GAP-NEXT-STEPS.md: add the principle to the top; drop "Subscriber
  CTA on profile" from Phase 3; switch Phase 4 cross-post controls
  from "disable + paywall" to "hide entirely"; remove the
  "Subscription status + manage subscription" row from Phase 12
  Settings.
- GAP-ENDPOINTS.md: withdraw B1 (subscription plans catalog) — the
  iOS app no longer needs a plans-catalog endpoint because it has
  no in-app paywall to render. Mark the Subscriptions (Stripe)
  endpoint row as intentionally unused by iOS.

The code changes that align with this direction land in the next
three commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Folders are a subscriber-only feature per the iOS-free-app direction.
Free users get no folder UI at all — no "New Folder" menu item, no
folder tree, no folder navigation. Any data they already had in
folders (e.g. from a prior subscription) still exists on the server;
the iOS UI just surfaces it at root.

ListsView:
- Add canCreateFolders predicate based on authState.user.isSubscriber.
- treeNodes passes an empty folder array for non-subscribers so lists
  that were nested in folders surface at root via buildTree's orphan
  rule.
- "New Folder" menu item gated on canCreateFolders.
- CreateListFolderView: drop the paywallMessage constant and the
  special 403 catch arm. The button is unreachable for free users;
  if a 403 ever sneaks through (e.g. subscription state shifts
  mid-session) we surface a generic "Failed to create folder."

DocumentsView (root + DocumentFolderView nested):
- allFolders returns [] for non-subscribers; rootDocuments returns
  every document (regardless of folderId) so nothing is hidden.
- "New Folder" menu item gated in both the root view and the nested
  folder view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ComposeView:
- New canUseSubscriberFeatures predicate based on
  authState.user.isSubscriber.
- Gear button (toggles the advanced bar) is hidden for free users —
  no point in a toggle that reveals nothing.
- The advanced HStack (image/video picker, M/BS/in cross-post stubs,
  calendar/schedule button) is gated as a whole. Free users only see
  the "characters remaining" label.
- schedulePicker section in the body is also gated; even if
  showSchedulePicker were toggled by some other code path, free
  users never see the picker.

FeedView:
- "Scheduled posts" calendar button in the toolbar is hidden for
  non-subscribers. Composing remains visible to everyone.

No paywall strings touched here — those come out in the next commit
once the UI paths that triggered them are all unreachable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
With the prior commits, every UI path that previously surfaced a
subscription paywall string is now hidden for free users. The
remaining 403 catch arms exist only as defensive belt-and-suspenders;
strict silence on subscription state means none of them should ever
expose subscription copy.

ComposeView (uploadVideo / uploadPhoto):
- Drop the `catch APIError.status(403)` arms that surfaced
  "Video upload requires an active subscription." /
  "Image upload requires an active subscription." The pickers are
  hidden for non-subscribers; 403 falls through to the generic
  "Failed to upload …" branch.

DocumentsView (CreateDocumentView.save / DocumentDetailView.save):
- Drop the `catch APIError.status(403)` arms that surfaced
  "Requires active subscription." `POST /api/documents` is not
  documented as subscriber-only, and free users can't pass a
  folderId anyway (folder UI is hidden). Falls through to the
  existing generic catch.

ScheduledMessagesView.load:
- Drop the 403 paywall string. The calendar entry point that
  launches this view is hidden in `FeedView` for free users, so
  this view is unreachable for them. 403 falls through to the
  generic "Could not load scheduled posts."

AppDataStore.refreshDocuments:
- Drop the 403 paywall string. `GET /api/documents` is not
  documented as subscriber-only; a 403 here is an unexpected
  condition treated as a generic load failure.

After this commit, `grep -rn "subscription" InterlinedList/` returns
only the explanatory doc-comment on `User.customerStatus` and no
user-facing string.

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

The Phase 2/3 auth + account-management work (committed in 067a057) didn't
build or test cleanly:

- Seven new source files were never registered in the Xcode target, so the
  app failed to compile (ResetPasswordView, OAuthCoordinator, ChangeEmailView,
  ForgotPasswordView, plus the three views added here). Added their
  PBXBuildFile / PBXFileReference / group / Sources-phase entries.

- MainTabView, LoginView and RegisterView referenced three views that were
  never created. Added them, matching existing patterns:
    - EmailVerificationBanner — resend-verification banner, hidden unless
      user.emailVerified == false.
    - LinkedIdentitiesView — list/disconnect linked OAuth providers and link
      new ones via the OAuth flow with link=true.
    - OAuthSignInButton — shared provider row used by Login and Register.

- Fixed five pre-existing/new broken tests whose assertions were wrong while
  the production code was correct:
    - Avatar/Image png-extension: decoded a binary multipart body as UTF-8
      (nil) — now search the raw Data for the filename.
    - Video 403: expected .status but checkResponse correctly returns
      .server when the 403 body carries an error message — stub an empty body
      to exercise the status path.
    - People encoding: inspected URL.path (percent-decoded) — now assert on
      absoluteString.
    - Profile update: expected camelCase displayName but /api/user/update uses
      the default snake_case encoder (like register) — assert display_name.

Build succeeds; full suite is 325 tests, 0 failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- GAP-NEXT-STEPS.md: add a "✅ Shipped phases" table, refresh the
  status snapshot with the new auth + profile capabilities, collapse the
  Phase 2/3 detail to shipped stubs, and update the effort summary. Notes
  the one loose end: the "Change email" entry row in EditProfileView is
  still a TODO (view + API + deep link exist; only the presenting row is
  unwired).
- GAP-ENDPOINTS.md: backend gaps unchanged (all still standing), but the
  "What backend has now" usage table now reflects the endpoints iOS
  consumes after Phases 2/3 (OAuth, reset/verify, identities, orgs,
  avatar, email change, delete account). §B7 notes the iOS re-fetch
  workaround is now in place.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Account section showed the email read-only behind a TODO. Replace it
with a tappable "Change" row that presents ChangeEmailView as a sheet
(posts to /api/user/change-email/request; the verify-email-change deep
link completes the change). ChangeEmailView, the API call, and the deep
link already existed — this wires the entry point.

Drops the "change-email entry row pending" caveats from GAP-NEXT-STEPS.md
and GAP-ENDPOINTS.md; Phase 2 is now fully closed.

Build succeeds; full suite is 325 tests, 0 failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the networking and model layer for the now-unblocked roadmap
(backend resolved B0/B2/B3/B5):

- Structured list schema (B0): updateListSchemaStructured + ?force,
  SchemaPropertyInput, DraftProperty round-trips defaultValue/helpText/
  placeholder, slugifyKey + structuredProperties serializer.
- Follow surface (5): followers/following (paginated), mutualCounts,
  removeFollower + FollowUser/MutualCounts models.
- Watchers (6): list/add/setRole/remove/search + ListWatcher, WatcherRole.
- Public browse (7): publicListDetail/Data/Documents/Document + tolerant
  PublicBrowse decoders (flat and {list,ancestors} shapes).
- Organizations (8): full org + member CRUD + join; Organization expanded,
  OrganizationMember, OrgRole.
- Compose (4): postMessage cross-post params + PostMessageResult,
  patchScheduledMessage, refreshMessageMetadata, cross-post body types.
- Settings/notifications (12): updateUserSettings, notificationPreferences
  GET/PATCH against the real catalog (push/inApp only) + models.
- Message search (13): searchMessages.
- APIError.conflict for 409 force-delete handling; patchCamel helper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…es 4, 13)

- ComposeView: real cross-post toggles (Mastodon multi-instance picker,
  Bluesky, LinkedIn, X) hidden entirely for non-subscribers; repost mode
  (pushedMessageId + optional commentary + quoted preview); crossPostResults
  summarized in the success toast.
- EditMessageView: edit a scheduled message's send time via PATCH.
- ScheduledMessagesView: rows open the editor; reload on dismiss.
- FeedView: Repost action on rows; .searchable feed backed by
  GET /api/messages/search (Phase 13 search half; tag discovery still
  blocked on §B6).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- FollowListView: paginated followers/following, swipe-to-remove on the
  current user's own followers, tap-through to a profile sheet.
- UserProfileView: tappable follower/following counts, mutual-count strip,
  public list rows navigate to PublicListDetailView, new Documents segment.
- PublicListDetailView: read-only rows (schema-aware, falls back to raw
  keys), sub-list navigation, Watch/Unwatch CTA (Phase 6 endpoint).
- PublicDocumentsView + PublicDocumentReader: read-only public documents.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WatchersListView, presented from the owner's ListDetailView: lists current
watchers with their role, a role picker (watcher/collaborator/manager),
swipe-to-remove, and an add-watcher flow backed by the candidate search
endpoint. Read-only public lists get the Watch CTA in Phase 7.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
OrganizationsListView / OrganizationDetailView / OrganizationMembersView
plus create/edit sheets: full org CRUD (owner-only edit/delete), member
role management (owner/admin/member), the last-owner-cannot-be-removed
guard enforced client-side, and join-public-org.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ases 12, B0)

- SettingsView: theme (PATCH /api/user/update, applied via
  RootView.preferredColorScheme), default visibility, advanced-post toggle,
  connected accounts, About links via SFSafariViewController, sign-out.
- NotificationPreferencesView: renders the real server catalog (dig/push/
  follow; push/inApp only, no email — per §B3 divergence), toggles persist.
- ListSchemaEditorView: saves via the structured endpoint (round-trips
  isVisible/isRequired/order) with a 409 force-delete confirmation (B0).
- MainTabView: Profile tab gains Settings (gear), Followers/Following, and
  Organizations entries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- APIClientGapPhasesTests: path/method/decode coverage for follow lists,
  watchers, public browse, organizations, structured schema (incl. 409 →
  conflict and ?force), notification prefs, message search, scheduled-edit,
  cross-post results, link metadata.
- GapModelsTests: schema serializer (slug/new-vs-existing/reorder), role
  ordering + capabilities, notification channel support, follow display.
- GAP-NEXT-STEPS.md: Phases 4–8, 12, B0 and feed search marked shipped.

Full suite green: 365 tests, 0 failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…PLE ship guide

- GAP-ENDPOINTS.md: repurposed from "missing endpoints" to a catalog of
  live-but-ambiguous contracts found while building the GAP phases
  (OpenAPI-vs-help shape disagreements, "no body" responses, watcher
  /me-has-no-role + non-standard pagination, cross-post/link-metadata
  shapes, avatar-returns-user), plus the still-blocked B4/B6/B8/B9.
- GAP-APPLE.md: step-by-step App Store signing & submission guide tailored
  to this project (bundle id, version/build, automatic signing, icon,
  Info.plist fixes for encryption + armv7, anti-steering/account-deletion/
  UGC compliance, archive+upload GUI and CLI paths, TestFlight, review).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er linking

Per the backend native-auth contract:

- GitHub's OAuth callback has no mobile/custom-scheme branch (it sets a web
  session cookie and redirects to /dashboard), so it can never return
  interlinedlist://oauth/callback?token=… and dead-ends inside
  ASWebAuthenticationSession. Added OAuthProvider.supportsNativeAuth and
  filtered it out of the sign-in lists in LoginView and RegisterView.
- In-app account linking (?link=true) authenticates via the web session
  cookie, not the Bearer token, so a Bearer-only native client can't link a
  new provider. Gated the "Link another provider" UI in LinkedIdentitiesView
  behind `linkingEnabled = false` (list + disconnect still work); shows a
  note to link via the web. Flip the flag when a Bearer link endpoint exists.

Build + 365 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Documents created "in a folder" landed at root, and opening a folder
showed unrelated root documents. The cause: `GET /api/documents` and
`POST /api/documents` are root-only — the GET ignores `?folderId=` and
the POST has no `folderId` field — so the iOS client must use the
folder-scoped endpoints and camelCase bodies (verified against the live
OpenAPI spec).

- documents(folderId:) reads `GET /api/documents/folders/{id}/documents`
- createDocument posts to the folder-scoped endpoint when a folder is set
  (folder conveyed by path, not body) via postCamel so isPublic survives
- updateDocument/createDocumentFolder use camelCase (patchCamel/postCamel)
  so folderId/isPublic/parentId aren't dropped server-side
- expand APIClientDocumentsTests to lock in the corrected paths and bodies

Bundled repository housekeeping in the same snapshot:

- docs: rewrite README to match the shipped feature set (OAuth, lists,
  documents, orgs, social, notifications, settings) instead of the stale
  "placeholder" copy; overhaul CLAUDE.md (DI/data-store/OAuth/deep-link
  architecture, the 401 re-validation contract, build/test, gotchas)
- test: disable parallelization in InterlinedList.xctestplan — the E2E
  suite shares a static login token that parallel sim clones defeat
- chore: remove Codex assets (AGENTS.md, .codex/) in favor of .claude
- chore: add /comment-and-commit and /commit-and-pr slash commands
- chore: extend .claude/settings.local.json permission allowlist

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Adron Adron merged commit 36e55da into main Jun 27, 2026
1 check passed
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