Add native SwiftUI iOS client - #936
devin-ai-integration[bot] wants to merge 4 commits into
Conversation
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
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 ( Fixed and verified: work entries never decoded. Fixed and verified: changing the server URL required a relaunch. What passed
🟡 Pre-existing, not this PR: export Date column is raw epoch msCSV/PDF totals, hours and descriptions are correct, but the Date column shows |



Summary
Adds a native iOS client for the existing Express API under a new top-level
ios/directory. Nothing inbackend/orfrontend/changes. SwiftUI + Swift Concurrency only — no third-party dependencies — iOS 17+, project generated by XcodeGen (ios/project.ymlis the source of truth; the generated.xcodeprojis 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 throughShareLink, plus a dashboard (hours this week/month, 7-day bar chart via SwiftCharts, last-30-days-by-client breakdown) that the web app doesn't have.Wiring worth knowing:
x-user-emailheader, injected centrally byLiveAPIClient.makeRequest; the signed-in email and the server base URL live inUserDefaults(timesheet.userEmail,timesheet.baseURL) and the base URL is editable from Login → Advanced and Settings, so the app can point at a deployed backend.SessionStorepublishes both (rather than@AppStorage, which doesn't fireobjectWillChangefrom a class), so changing the server URL re-keysRootTabViewand every tab picks up the newAPIClientwithout a relaunch.client_id,client_name), request bodies are camelCase (clientId) — so models declare explicitCodingKeysper direction rather than using a global key strategy.dateis 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 becauseJoi.date().iso()coerces to a JSDatethat node-sqlite3 stores numerically, which is whatGET /api/work-entriesactually 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 usingCalendar.currentwithout a one-day shift; writes always sendYYYY-MM-DD.APIError(401 → .unauthorized,404 → .notFound,429 → .rateLimitedfor the backend's rate limiter, otherwise the{ "error": ... }body), surfaced in a retryableErrorBanner.Info.plistcarries an ATS localhost exception so the simulator can talk tohttp://localhost:3001.Tests:
TimesheetAppTestsruns against aURLProtocolstub (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 stubbedAPIClient.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