Skip to content

Add native SwiftUI iOS client - #936

Open
devin-ai-integration[bot] wants to merge 4 commits into
mainfrom
devin/1788264405-ios-client
Open

devin-ai-integration[bot] wants to merge 4 commits into
mainfrom
devin/1788264405-ios-client

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a native iOS client for the existing Express API under a new top-level ios/ directory. Nothing in backend/ or frontend/ changes. SwiftUI + Swift Concurrency only — no third-party dependencies — iOS 17+, project generated by XcodeGen (ios/project.yml is the source of truth; the generated .xcodeproj is committed so the repo opens in Xcode without extra tooling).

Feature parity with the web app: email sign-in (lazy registration via POST /api/auth/login), client CRUD, work-entry CRUD with a per-client filter, per-client reports with CSV/PDF export shared through ShareLink, plus a dashboard (hours this week/month, 7-day bar chart via Swift Charts, last-30-days-by-client breakdown) that the web app doesn't have.

Wiring worth knowing:

  • Auth is the backend's x-user-email header, injected centrally by LiveAPIClient.makeRequest; the signed-in email and the server base URL live in UserDefaults (timesheet.userEmail, timesheet.baseURL) and the base URL is editable from Login → Advanced and Settings, so the app can point at a deployed backend. SessionStore publishes both (rather than @AppStorage, which doesn't fire objectWillChange from a class), so changing the server URL re-keys RootTabView and every tab picks up the new APIClient without a relaunch.
  • The API is asymmetric — responses are snake_case (client_id, client_name), request bodies are camelCase (clientId) — so models declare explicit CodingKeys per direction rather than using a global key strategy.
  • date is a calendar date on the wire but arrives in three shapes: YYYY-MM-DD, an ISO8601 datetime, or an epoch-millisecond number — the last one because Joi.date().iso() coerces to a JS Date that node-sqlite3 stores numerically, which is what GET /api/work-entries actually returns. DateCoding.decodeDate(from:forKey:) accepts a string or a number and normalizes UTC-anchored values to local midnight of the same calendar day, so views and dashboard math can keep using Calendar.current without a one-day shift; writes always send YYYY-MM-DD.
  • Status codes map to a typed APIError (401 → .unauthorized, 404 → .notFound, 429 → .rateLimited for the backend's rate limiter, otherwise the { "error": ... } body), surfaced in a retryable ErrorBanner.
  • Info.plist carries an ATS localhost exception so the simulator can talk to http://localhost:3001.

Tests: TimesheetAppTests runs against a URLProtocol stub (no network) covering header injection, snake_case decoding, camelCase/date-only request encoding, all three date shapes plus a malformed one, and error mapping, plus view-model load/failure/delete transitions against a stubbed APIClient.

xcodebuild -project ios/TimesheetApp.xcodeproj -scheme TimesheetApp \
  -destination 'platform=iOS Simulator,name=iPhone 17 Pro' test
# ** TEST SUCCEEDED **

Link to Devin session: https://partner-workshops.devinenterprise.com/sessions/02463fcbd5c54ca8877be08f51d69449
Open in Devin Desktop: https://partner-workshops.devinenterprise.com/desktop/session/02463fcbd5c54ca8877be08f51d69449?variant=devin
Requested by: @sumitshatwara

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@sonarqubecloud

sonarqubecloud Bot commented Sep 1, 2026

Copy link
Copy Markdown

@devin-ai-integration

devin-ai-integration Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

iOS client — simulator end-to-end test (iPhone 17 Pro, iOS 26.5, UTC+05:30, backend on :3001)

I drove the real UI in the simulator against the running Express backend. Two defects surfaced and were fixed on this branch (c6c5254, a59de78); everything below is the state after those commits.

Fixed and verified: work entries never decoded. GET /api/work-entries returns date as an epoch-millisecond number (Joi.date().iso() → JS Date → node-sqlite3 stores it numerically), while the client only decoded a String, so Entries, Dashboard and reports all showed "The data couldn't be read because it isn't in the correct format." DateCoding.decodeDate(from:forKey:) now accepts a string or a number and normalizes UTC-anchored values to local midnight of the same calendar day.

Fixed and verified: changing the server URL required a relaunch. .id(session.baseURLString) sat on the inner TabView, so the @StateObject view models (and their captured LiveAPIClient) survived the re-key. Moving the .id to RootTabView's call site rebuilds them. Proven at the network level with a request-logging probe server on :3999 plus the backend access log — after saving :3999 the probe logged 6 hits and the backend logged zero app requests; restoring :3001 recovered immediately, both without a relaunch.

Entries showing the error banner while pointed at the unreachable port

Dashboard recovered immediately after restoring :3001 in Settings

What passed
  • Login: Sign in disabled for empty and invalid email ("Enter a valid email address."); valid email lands on the Dashboard.
  • Clients: create, swipe-edit, search filter, swipe-delete with a confirmation dialog that really removes the client; pull-to-refresh, empty states.
  • Work-entry validation: no client / 0 / 24.5 → "Choose a client and enter hours greater than 0 and no more than 24."; 1.234 → "Hours can have at most two decimal places."; the sheet stays open in every case. Fractional hours 7.5 and 1.25 save successfully.
  • Dates on a UTC+05:30 box: picking 1 Sep and 25 Aug stores and renders those exact days; entries group under 1 Sep 2026 / 25 Aug 2026; editing hours without touching the date picker leaves the stored date unchanged; a 10:00Z datetime normalizes to the same calendar day.
  • Entry edit and delete via the UI, including backend-side removal.
  • Dashboard figures reconcile with the logged data (9.50h week/month, 3 entries, correct 7-day bar, "Last 30 days by client" 11.75 h = 7.5 + 2.0 + 2.25); "Log hours" from a fresh launch has a populated client picker without ever opening the Clients tab.
  • Client report totals and entry count match; CSV and PDF export share sheets render with correct totals, hours and descriptions.
  • Error handling: unreachable server shows a readable banner with a working Retry, no hang or crash.
  • Dark mode legibility and landscape rotation.

Entries grouped by date after the fixes

🟡 Pre-existing, not this PR: export Date column is raw epoch ms

CSV/PDF totals, hours and descriptions are correct, but the Date column shows 1788220800000. curling /api/reports/export/csv/1 and /export/pdf/1 returns byte-identical output, so this lives in backend/src/routes/reports.js, outside this PR's scope.

Exported PDF

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