Add mobile app, map-core, nearby sync, and activity recording specs
mobile-app: Unified React Native + Expo app combining Planner and Journal. OAuth2 PKCE auth, MapLibre maps, versioned REST API with Zod schemas, configurable server URL, offline SQLite, Web Push relay notifications. TanStack Query + Zustand state management. Jest + Maestro testing. 76 tasks across 5 phases. map-core-package: Extract renderer-agnostic map definitions (tiles, color palettes, POI categories, z-index) into @trails-cool/map-core. Pure refactor preparing for MapLibre on mobile. 27 tasks. mobile-activity-recording: GPS recording, live stats, HealthKit/Health Connect export. Separated from mobile-app for independent scheduling. mobile-nearby-sync: BLE route sync between nearby devices for offline group riding. QR waypoint sharing as simpler v1. TXQR noted as future. journal-rest-api spec: Full API contract — endpoints, auth, pagination, errors, discovery, versioning, BRouter proxy. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e288630a12
commit
fff77a2ed2
23 changed files with 996 additions and 0 deletions
2
openspec/changes/mobile-app/.openspec.yaml
Normal file
2
openspec/changes/mobile-app/.openspec.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-04-12
|
||||
156
openspec/changes/mobile-app/design.md
Normal file
156
openspec/changes/mobile-app/design.md
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
## Context
|
||||
|
||||
trails.cool is a two-app web platform: Planner (stateless, collaborative, Yjs) and Journal (accounts, PostgreSQL, ActivityPub). The Planner uses Leaflet for maps, Yjs for real-time sync, and BRouter for routing. The Journal stores routes, activities, and user data. Shared packages (`types`, `gpx`, `i18n`) are pure TypeScript.
|
||||
|
||||
The mobile app combines both apps into a single React Native + Expo experience, communicating with the Journal as its backend. The user is an experienced React Native/Expo developer.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Unified mobile app (plan + journal in one)
|
||||
- Authenticate with existing Journal account
|
||||
- View and edit routes on the go
|
||||
- Record GPS activities
|
||||
- Work offline for viewing routes
|
||||
- Reuse shared TypeScript packages
|
||||
|
||||
**Non-Goals:**
|
||||
- Full collaborative Yjs editing on mobile (too complex for v1 — use direct API)
|
||||
- Self-hosting the mobile app (App Store distribution only)
|
||||
- Replacing the web Planner (mobile editing is simplified)
|
||||
- ActivityPub federation from mobile (handled by Journal server)
|
||||
- Offline route computation (BRouter requires server)
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1: Expo managed workflow with monorepo integration
|
||||
|
||||
Place the app at `apps/mobile/` in the existing pnpm monorepo. Use Expo managed workflow (no native code ejection) with EAS Build for CI/CD. Share packages via pnpm workspace — `@trails-cool/types`, `@trails-cool/gpx`, `@trails-cool/i18n` imported directly.
|
||||
|
||||
The `gpx` package uses `linkedom` for Node.js XML parsing. On mobile, use the built-in `DOMParser` (available in React Native's JSC/Hermes via a polyfill or the existing browser code path in `parseGpx`).
|
||||
|
||||
### D2: Authentication via OAuth2 PKCE
|
||||
|
||||
The Journal currently uses passkeys + magic links. Neither works well in a native app context. Add an OAuth2 authorization code flow with PKCE to the Journal:
|
||||
|
||||
- Mobile app opens Journal's `/oauth/authorize` in an in-app browser
|
||||
- User authenticates via existing passkey/magic link
|
||||
- Journal redirects back with auth code
|
||||
- Mobile app exchanges code for access + refresh tokens
|
||||
- Tokens stored in Expo SecureStore
|
||||
|
||||
This keeps the auth UI on the web (no need to implement passkey in native code) while giving the mobile app long-lived tokens.
|
||||
|
||||
The mobile app sends a device name (e.g., model + OS version) during token exchange. The Journal stores this alongside the token, enabling a "Connected Devices" list on the Journal web settings page where users can see active sessions and revoke individual devices. This is primarily relevant for the hosted version — self-hosted instances with a single user would rarely use it, but the data is stored regardless.
|
||||
|
||||
### D2b: Configurable server URL
|
||||
|
||||
The mobile app defaults to `https://trails.cool` but allows connecting to any self-hosted Journal instance. On the login screen, a "Connect to a different server" option lets the user enter a custom URL (e.g., `https://trails.example.org`). The app validates the URL by fetching `/.well-known/trails-cool` (or a similar discovery endpoint) to confirm it's a compatible instance.
|
||||
|
||||
The server URL is stored persistently and used as the base for all API calls and the OAuth2 login flow. Users can switch instances from the Profile tab — this logs them out and clears local data.
|
||||
|
||||
The `/.well-known/trails-cool` discovery endpoint returns an `apiVersion` semver string (e.g., `"1.0.0"`). The mobile app declares a `requiredApiVersion` (minimum semver it needs). On connect and on app foreground, the app checks:
|
||||
|
||||
- Server `apiVersion` satisfies app's required range → normal operation.
|
||||
- Server `apiVersion` too low → **Block**: "This server needs to be updated. Please ask the administrator to upgrade." The app still allows read-only access to cached offline data.
|
||||
|
||||
The API follows semver: patch versions for fixes, minor versions for additive changes (new endpoints, new optional fields), major versions only for breaking changes. Since the API is backwards compatible by design, a newer server always works with an older app — no "server too new" check needed.
|
||||
|
||||
### D2c: Shared API contract package
|
||||
|
||||
A new `packages/api/` workspace package (`@trails-cool/api`) serves as the single source of truth for the REST API:
|
||||
|
||||
- `API_VERSION` constant (semver string)
|
||||
- TypeScript types for all request/response shapes (e.g., `RouteListResponse`, `CreateActivityRequest`)
|
||||
- Endpoint path constants (e.g., `ENDPOINTS.routes.list = "/api/v1/routes"`)
|
||||
- Error response type
|
||||
|
||||
The package uses Zod schemas as the source of truth — TypeScript types are inferred via `z.infer<>`. The Journal server uses schemas to validate incoming request bodies. The mobile client can optionally validate responses (useful for self-hosted instances on older versions). Both sides share the same schemas — runtime validation and type safety from one definition.
|
||||
|
||||
### D3: MapLibre GL for mobile maps
|
||||
|
||||
Leaflet is web-only. Use `react-native-maplibre-gl` for mobile — OSM vector tiles, GPU-accelerated, free, consistent with the web's OSM data source. Requires an Expo config plugin but is well-supported.
|
||||
|
||||
The web Planner stays on Leaflet for now, but map logic should be structured for future convergence:
|
||||
|
||||
- **`@trails-cool/map-core`** (new package): Renderer-agnostic definitions — tile source URLs, overlay configs, color palettes (route coloring, POI categories), z-index layering order. Pure data, no rendering code.
|
||||
- **`@trails-cool/map`** (existing): Leaflet-specific components for web, imports from `map-core`
|
||||
- **`apps/mobile/`**: MapLibre-specific components for mobile, imports from `map-core`
|
||||
|
||||
This way, switching the web to MapLibre later means replacing `@trails-cool/map` internals while `map-core` stays unchanged. The mobile app validates that `map-core` abstractions work before the web migration.
|
||||
|
||||
### D4: Simplified route editing (no Yjs)
|
||||
|
||||
Mobile route editing talks directly to the Journal API:
|
||||
1. Fetch route GPX → parse waypoints
|
||||
2. User adds/moves/deletes waypoints on the map
|
||||
3. Compute route via BRouter (through a proxy endpoint on the Journal, or direct to the Planner's BRouter instance)
|
||||
4. Save updated GPX back to Journal API
|
||||
|
||||
No real-time collaboration on mobile. If the user needs full collaborative editing, deep-link to the Planner web app.
|
||||
|
||||
### D5: Offline with SQLite + tile cache
|
||||
|
||||
- Routes: Download GPX + parsed waypoints into Expo SQLite
|
||||
- Map tiles: Use `react-native-maps`' built-in tile caching, or download tile packages for specific regions
|
||||
- Activities: Stored locally first, synced to Journal when online
|
||||
- Edit queue: Changes made offline queued and synced on reconnect (conflict resolution: last-write-wins, with a warning if the server version changed)
|
||||
|
||||
### D6: Activity recording with expo-location
|
||||
|
||||
Use `expo-location` for GPS tracking in the foreground/background:
|
||||
- Start/stop recording from the active route view
|
||||
- Live stats: distance, duration, current speed, elevation gain
|
||||
- Track points stored locally, converted to GPX on stop
|
||||
- Save as Journal activity linked to the route
|
||||
- Export to HealthKit (iOS) / Health Connect (Android) via `expo-health`
|
||||
|
||||
### D7: Navigation structure
|
||||
|
||||
Tab bar with 4 tabs:
|
||||
- **Map**: Current route on map, quick edit, start recording
|
||||
- **Routes**: List of user's routes from Journal
|
||||
- **Activities**: List of recorded activities
|
||||
- **Profile**: Account settings, offline data management, sync status
|
||||
|
||||
### D8: BRouter routing via Journal API proxy
|
||||
|
||||
The mobile app computes routes through the Journal API: `POST /api/v1/routes/compute`. The Journal forwards the request to its BRouter instance and returns the enriched route. This avoids exposing BRouter publicly and works with self-hosted instances where the BRouter container isn't publicly accessible.
|
||||
|
||||
### D9: Photo uploads via presigned URLs
|
||||
|
||||
Route and activity photos are stored in S3/Garage (existing infrastructure). The API returns presigned upload URLs — the mobile app uploads directly to storage, then confirms the upload via the API. This avoids proxying large files through the Journal server.
|
||||
|
||||
### D10: Cursor-based pagination
|
||||
|
||||
All list endpoints (routes, activities) use cursor-based pagination. The response includes a `nextCursor` field — the client passes it back to get the next page. This handles feeds that change (new items, deletions) without skipping or duplicating entries.
|
||||
|
||||
### D11: Expo monorepo support
|
||||
|
||||
Use Expo's built-in `experiments.monorepo: true` in `app.config.ts` to resolve pnpm workspace packages in Metro. This handles symlink resolution and shared package discovery automatically.
|
||||
|
||||
### D12: Push notifications via Web Push relay
|
||||
|
||||
Following Mastodon's proven architecture for federated push notifications:
|
||||
|
||||
- **Journal instances implement standard Web Push API** (RFC 8030) — no Apple/Google dependencies on the server side
|
||||
- **trails.cool hosts a relay service** that translates Web Push → APNs (iOS) and FCM (Android). Self-hosted instances send standard Web Push to this relay.
|
||||
- **Notifications are end-to-end encrypted** — the relay forwards encrypted payloads without reading them
|
||||
- **Self-hosted admins need zero configuration** — no Apple Developer account, no Firebase project. Standard Web Push just works.
|
||||
- **The relay is open-source** — self-hosters who want full independence can run their own relay with their own APNs/FCM credentials
|
||||
|
||||
Flow: `Journal instance → Web Push (encrypted) → trails.cool relay → APNs/FCM → phone`
|
||||
|
||||
This aligns with the ActivityPub federation direction and matches how Mastodon clients (Toot!, Ice Cubes, etc.) handle push for thousands of self-hosted instances through a single relay.
|
||||
|
||||
### D13: State management
|
||||
|
||||
TBD — to be discussed.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **BRouter access from mobile**: The BRouter instance runs in Docker alongside the Planner. The mobile app needs a route computation endpoint. Options: (a) proxy through Journal API, (b) expose BRouter publicly with auth, (c) use a public BRouter instance. Option (a) is safest.
|
||||
- **Map tile licensing**: Google Maps requires an API key and has usage-based pricing. Apple Maps is free on iOS. Consider MapLibre with OpenStreetMap tiles for a free, consistent cross-platform solution.
|
||||
- **React Native Maps vs MapLibre**: `react-native-maps` is more mature but ties to Google/Apple. `react-native-maplibre-gl` uses vector tiles and is more consistent with the web's OSM-based approach. Worth evaluating.
|
||||
- **Offline storage size**: Map tiles for a region can be hundreds of MB. Need a download manager with progress and storage budget.
|
||||
- **App Store review**: GPS background tracking + health data access need careful privacy descriptions.
|
||||
35
openspec/changes/mobile-app/proposal.md
Normal file
35
openspec/changes/mobile-app/proposal.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
## Why
|
||||
|
||||
trails.cool is currently web-only: the Planner runs in a desktop browser and the Journal is a separate web app. On the road — at a campsite, at a trailhead, at a crossroads — users need their phone. The mobile browser experience works but is limited: no offline access, no HealthKit/GPS integration, no push notifications for shared route updates, and the two-app split (Planner + Journal) makes no sense on a phone where you want a single app that does both. A native mobile app (React Native + Expo) would unify planning and journaling, enable on-the-go route editing, and open the door to activity recording (GPS tracking) and health platform integration.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Unified mobile app**: A single React Native + Expo app that combines Planner and Journal functionality. Users authenticate with their Journal account and can browse routes, edit them, and record activities.
|
||||
- **Shared packages**: Reuse `@trails-cool/types`, `@trails-cool/gpx`, `@trails-cool/i18n` in the mobile app. These are already pure TypeScript with no DOM dependencies.
|
||||
- **Mobile route editing**: Simplified map-based route editing (add/move/delete waypoints, toggle overnight stops). Not full collaborative Yjs — instead, direct API edits to the Journal.
|
||||
- **Offline route access**: Download route GPX + map tiles for offline use. View routes and waypoint details without connectivity.
|
||||
- **Journal API client**: The mobile app talks to the Journal's existing API (auth, routes, activities). No new backend — the Journal is the source of truth.
|
||||
- **Testing**: Jest + React Native Testing Library for unit/component tests, Maestro for E2E flows
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `mobile-app-shell`: Expo app scaffold, navigation (tab bar: Map, Routes, Activities, Profile), authentication flow with Journal account
|
||||
- `mobile-route-editor`: Map-based route editing on mobile — add/move/delete waypoints, overnight stops, POI snap. Direct saves to Journal API (no Yjs).
|
||||
- `mobile-offline`: Download routes + map tile regions for offline viewing. Offline queue for edits synced when back online.
|
||||
- `mobile-testing`: Jest + RNTL for unit/component tests, Maestro YAML flows for E2E
|
||||
- `mobile-journal-client`: API client for Journal endpoints — auth, routes CRUD, activities CRUD, route versions
|
||||
|
||||
### Modified Capabilities
|
||||
- `journal-auth`: Add OAuth2/PKCE flow for mobile app token exchange (existing passkey/magic-link auth doesn't work natively on mobile)
|
||||
- `planner-journal-handoff`: Mobile app can open Planner web sessions via deep link for full collaborative editing when needed
|
||||
- `shared-packages`: Verify and adapt `@trails-cool/types`, `@trails-cool/gpx`, `@trails-cool/i18n` for React Native compatibility (no DOM, no Node.js APIs)
|
||||
|
||||
## Impact
|
||||
|
||||
- **New repo/workspace**: `apps/mobile/` in the monorepo, React Native + Expo managed workflow
|
||||
- **Shared packages**: `packages/types`, `packages/gpx`, `packages/i18n` need to work in React Native (linkedom dependency in gpx is Node-only — need browser DOMParser or a RN-compatible XML parser)
|
||||
- **Journal API**: Existing routes/activities/auth endpoints used as-is. May need an OAuth2 token endpoint for mobile auth.
|
||||
- **Maps**: React Native Maps (Google Maps/Apple Maps) instead of Leaflet (web-only). New map components, not shared with web.
|
||||
- **Dependencies**: expo, react-native-maps, expo-location, expo-health, expo-file-system, expo-sqlite (offline cache)
|
||||
- **App Store**: iOS App Store + Google Play distribution via EAS Build
|
||||
27
openspec/changes/mobile-app/specs/journal-auth/spec.md
Normal file
27
openspec/changes/mobile-app/specs/journal-auth/spec.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: OAuth2 PKCE authorization flow
|
||||
The Journal SHALL support OAuth2 authorization code flow with PKCE for mobile app token exchange, in addition to existing passkey and magic link authentication.
|
||||
|
||||
#### Scenario: Authorization endpoint
|
||||
- **WHEN** a client requests `GET /oauth/authorize` with client_id, redirect_uri, code_challenge, and code_challenge_method
|
||||
- **THEN** the Journal shows the existing login UI and, upon successful authentication, redirects to the redirect_uri with an authorization code
|
||||
|
||||
#### Scenario: Token exchange
|
||||
- **WHEN** a client sends `POST /oauth/token` with the authorization code, code_verifier, client_id, and redirect_uri
|
||||
- **THEN** the Journal validates the PKCE challenge, issues an access token and refresh token, and returns them
|
||||
|
||||
#### Scenario: Token refresh
|
||||
- **WHEN** a client sends `POST /oauth/token` with grant_type=refresh_token and a valid refresh token
|
||||
- **THEN** the Journal issues a new access token and optionally a new refresh token
|
||||
|
||||
#### Scenario: Invalid PKCE challenge
|
||||
- **WHEN** a client sends a code_verifier that does not match the stored code_challenge
|
||||
- **THEN** the Journal rejects the token exchange with a 400 error
|
||||
|
||||
### Requirement: OAuth2 client registration
|
||||
The Journal SHALL register the mobile app as a trusted first-party OAuth2 client.
|
||||
|
||||
#### Scenario: Mobile app client
|
||||
- **WHEN** the mobile app initiates an OAuth2 flow with client_id `trails-cool-mobile`
|
||||
- **THEN** the Journal recognizes it as a trusted client and allows the `trailscool://` redirect URI scheme
|
||||
124
openspec/changes/mobile-app/specs/journal-rest-api/spec.md
Normal file
124
openspec/changes/mobile-app/specs/journal-rest-api/spec.md
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: REST API namespace
|
||||
The Journal SHALL expose a versioned REST API under `/api/v1/` for external clients.
|
||||
|
||||
#### Scenario: API base path
|
||||
- **WHEN** a client sends a request to `/api/v1/*`
|
||||
- **THEN** the Journal processes it as an API request with JSON responses and bearer token auth
|
||||
|
||||
#### Scenario: Non-API routes unaffected
|
||||
- **WHEN** a browser accesses the Journal's existing web routes
|
||||
- **THEN** they continue to work via React Router loaders/actions with cookie sessions
|
||||
|
||||
### Requirement: Instance discovery
|
||||
The Journal SHALL expose a discovery endpoint for clients to identify the instance.
|
||||
|
||||
#### Scenario: Discovery endpoint
|
||||
- **WHEN** a client fetches `GET /.well-known/trails-cool`
|
||||
- **THEN** the response includes `apiVersion` (semver), instance name, and API base URL
|
||||
|
||||
#### Scenario: API version compatibility
|
||||
- **WHEN** the client's minimum required API version exceeds the server's `apiVersion`
|
||||
- **THEN** the client blocks with an upgrade prompt (offline data still accessible)
|
||||
|
||||
### Requirement: Shared API contract package
|
||||
The API contract SHALL be defined in `@trails-cool/api` using Zod schemas, shared between server and clients.
|
||||
|
||||
#### Scenario: Type-safe requests
|
||||
- **WHEN** the Journal server receives a request body
|
||||
- **THEN** it validates the body using the Zod schema from `@trails-cool/api`
|
||||
|
||||
#### Scenario: Version in one place
|
||||
- **WHEN** the API version needs to be bumped
|
||||
- **THEN** it is changed in `@trails-cool/api` — both server and clients see it at compile time
|
||||
|
||||
### Requirement: Authentication
|
||||
The API SHALL use OAuth2 bearer tokens for authentication.
|
||||
|
||||
#### Scenario: Authenticated request
|
||||
- **WHEN** a client sends a request with `Authorization: Bearer <token>`
|
||||
- **THEN** the server validates the token and processes the request as the authenticated user
|
||||
|
||||
#### Scenario: Unauthenticated request
|
||||
- **WHEN** a client sends a request without a valid bearer token to a protected endpoint
|
||||
- **THEN** the server responds with 401 Unauthorized
|
||||
|
||||
#### Scenario: Token refresh
|
||||
- **WHEN** an access token expires
|
||||
- **THEN** the client exchanges its refresh token at `POST /api/v1/auth/token` for a new access token
|
||||
|
||||
### Requirement: Routes endpoints
|
||||
The API SHALL provide CRUD endpoints for routes.
|
||||
|
||||
#### Scenario: List routes
|
||||
- **WHEN** `GET /api/v1/routes?cursor=<cursor>`
|
||||
- **THEN** returns paginated route list with id, name, distance, elevationGain, thumbnail geojson, updatedAt, and `nextCursor`
|
||||
|
||||
#### Scenario: Get route detail
|
||||
- **WHEN** `GET /api/v1/routes/:id`
|
||||
- **THEN** returns full route with metadata, GPX, waypoints, dayBreaks, day stats, geojson, and version history
|
||||
|
||||
#### Scenario: Update route
|
||||
- **WHEN** `PUT /api/v1/routes/:id` with GPX body
|
||||
- **THEN** creates a new version, updates stats and dayBreaks, returns updated route
|
||||
|
||||
#### Scenario: Create route
|
||||
- **WHEN** `POST /api/v1/routes` with name and optional GPX
|
||||
- **THEN** creates a new route, returns the route with id
|
||||
|
||||
#### Scenario: Delete route
|
||||
- **WHEN** `DELETE /api/v1/routes/:id`
|
||||
- **THEN** deletes the route and all versions, returns 204
|
||||
|
||||
### Requirement: Activities endpoints
|
||||
The API SHALL provide CRUD endpoints for activities.
|
||||
|
||||
#### Scenario: List activities
|
||||
- **WHEN** `GET /api/v1/activities?cursor=<cursor>`
|
||||
- **THEN** returns paginated activity list with id, name, routeId, distance, duration, startedAt, and `nextCursor`
|
||||
|
||||
#### Scenario: Get activity detail
|
||||
- **WHEN** `GET /api/v1/activities/:id`
|
||||
- **THEN** returns full activity with stats, GPX, linked route info, geojson
|
||||
|
||||
#### Scenario: Create activity
|
||||
- **WHEN** `POST /api/v1/activities` with name, GPX, optional routeId
|
||||
- **THEN** creates activity, computes stats from GPX, returns activity with id
|
||||
|
||||
#### Scenario: Delete activity
|
||||
- **WHEN** `DELETE /api/v1/activities/:id`
|
||||
- **THEN** deletes the activity, returns 204
|
||||
|
||||
### Requirement: Route computation proxy
|
||||
The API SHALL proxy BRouter route computation requests.
|
||||
|
||||
#### Scenario: Compute route
|
||||
- **WHEN** `POST /api/v1/routes/compute` with waypoints array and profile
|
||||
- **THEN** the Journal forwards to its BRouter instance and returns the enriched route (geojson, coordinates, segmentBoundaries, surfaces, highways, etc.)
|
||||
|
||||
### Requirement: Cursor-based pagination
|
||||
All list endpoints SHALL use cursor-based pagination.
|
||||
|
||||
#### Scenario: First page
|
||||
- **WHEN** a list endpoint is called without a cursor
|
||||
- **THEN** returns the first page of results with `nextCursor` (null if no more results)
|
||||
|
||||
#### Scenario: Next page
|
||||
- **WHEN** a list endpoint is called with `?cursor=<nextCursor>`
|
||||
- **THEN** returns the next page of results
|
||||
|
||||
### Requirement: Error responses
|
||||
The API SHALL return structured error responses.
|
||||
|
||||
#### Scenario: Validation error
|
||||
- **WHEN** a request body fails Zod validation
|
||||
- **THEN** returns 400 with `{ error: "Validation failed", code: "VALIDATION_ERROR", fields: [...] }`
|
||||
|
||||
#### Scenario: Not found
|
||||
- **WHEN** a resource doesn't exist
|
||||
- **THEN** returns 404 with `{ error: "Not found", code: "NOT_FOUND" }`
|
||||
|
||||
#### Scenario: Server error
|
||||
- **WHEN** an unexpected error occurs
|
||||
- **THEN** returns 500 with `{ error: "Internal server error", code: "INTERNAL_ERROR" }` (no stack traces)
|
||||
45
openspec/changes/mobile-app/specs/mobile-app-shell/spec.md
Normal file
45
openspec/changes/mobile-app/specs/mobile-app-shell/spec.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Expo app scaffold
|
||||
The system SHALL provide a React Native + Expo managed workflow app at `apps/mobile/` in the monorepo, sharing workspace packages.
|
||||
|
||||
#### Scenario: App boots on iOS and Android
|
||||
- **WHEN** the app is launched on iOS or Android
|
||||
- **THEN** the Expo managed app loads, displays the tab navigation, and renders the Map tab by default
|
||||
|
||||
#### Scenario: Monorepo integration
|
||||
- **WHEN** the mobile app is built
|
||||
- **THEN** it resolves `@trails-cool/types`, `@trails-cool/gpx`, and `@trails-cool/i18n` from pnpm workspace dependencies
|
||||
|
||||
### Requirement: Tab navigation
|
||||
The system SHALL provide a bottom tab bar with four tabs: Map, Routes, Activities, and Profile.
|
||||
|
||||
#### Scenario: Tab switching
|
||||
- **WHEN** the user taps a tab (Map, Routes, Activities, or Profile)
|
||||
- **THEN** the corresponding screen is displayed and the active tab is visually highlighted
|
||||
|
||||
#### Scenario: Tab state preservation
|
||||
- **WHEN** the user switches between tabs
|
||||
- **THEN** each tab preserves its scroll position and navigation stack
|
||||
|
||||
### Requirement: Authentication flow
|
||||
The system SHALL require Journal account authentication before accessing protected screens.
|
||||
|
||||
#### Scenario: First launch
|
||||
- **WHEN** the user opens the app for the first time
|
||||
- **THEN** a login screen is shown with the option to enter their Journal instance URL and authenticate
|
||||
|
||||
#### Scenario: Session persistence
|
||||
- **WHEN** the user has previously authenticated and reopens the app
|
||||
- **THEN** stored tokens are loaded from SecureStore and the user is logged in automatically
|
||||
|
||||
### Requirement: Deep linking
|
||||
The system SHALL handle deep links for opening routes and activities.
|
||||
|
||||
#### Scenario: Open route via deep link
|
||||
- **WHEN** the app receives a deep link like `trailscool://routes/:id`
|
||||
- **THEN** the app navigates to the route detail screen for that route
|
||||
|
||||
#### Scenario: Open Planner for collaborative editing
|
||||
- **WHEN** the user taps "Edit in Planner" on a route
|
||||
- **THEN** the system opens the Planner web URL in the device browser with the appropriate session callback
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Journal authentication client
|
||||
The system SHALL provide an API client that handles OAuth2 PKCE authentication with a Journal instance.
|
||||
|
||||
#### Scenario: Login flow
|
||||
- **WHEN** the user enters their Journal instance URL and taps "Sign in"
|
||||
- **THEN** the app opens the Journal's `/oauth/authorize` endpoint in an in-app browser, receives the auth code on redirect, and exchanges it for access + refresh tokens
|
||||
|
||||
#### Scenario: Token storage
|
||||
- **WHEN** tokens are received from the Journal
|
||||
- **THEN** the access token and refresh token are stored in Expo SecureStore
|
||||
|
||||
#### Scenario: Token refresh
|
||||
- **WHEN** an API request fails with a 401
|
||||
- **THEN** the client automatically uses the refresh token to obtain a new access token and retries the request
|
||||
|
||||
### Requirement: Routes API client
|
||||
The system SHALL provide methods for listing, fetching, creating, and updating routes on the Journal.
|
||||
|
||||
#### Scenario: List user routes
|
||||
- **WHEN** the Routes tab is opened
|
||||
- **THEN** the client fetches the user's routes from `GET /api/routes` and returns them as typed Route objects
|
||||
|
||||
#### Scenario: Fetch route detail
|
||||
- **WHEN** a route is selected
|
||||
- **THEN** the client fetches the route with GPX data from `GET /api/routes/:id` including waypoints and geometry
|
||||
|
||||
#### Scenario: Save route
|
||||
- **WHEN** the user saves an edited route
|
||||
- **THEN** the client sends the updated GPX to `PUT /api/routes/:id` and returns the new version
|
||||
|
||||
### Requirement: Activities API client
|
||||
The system SHALL provide methods for listing, fetching, and creating activities on the Journal.
|
||||
|
||||
#### Scenario: List user activities
|
||||
- **WHEN** the Activities tab is opened
|
||||
- **THEN** the client fetches the user's activities from `GET /api/activities`
|
||||
|
||||
#### Scenario: Create activity
|
||||
- **WHEN** a recording is saved
|
||||
- **THEN** the client sends the activity data (GPX, distance, duration, routeId) to `POST /api/activities`
|
||||
|
||||
### Requirement: Network error handling
|
||||
The system SHALL handle network failures gracefully across all API calls.
|
||||
|
||||
#### Scenario: Request timeout or network error
|
||||
- **WHEN** an API request fails due to network issues
|
||||
- **THEN** the client returns a typed error and the UI shows an appropriate message with a retry option
|
||||
42
openspec/changes/mobile-app/specs/mobile-offline/spec.md
Normal file
42
openspec/changes/mobile-app/specs/mobile-offline/spec.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Download routes for offline
|
||||
The system SHALL allow users to download route data (GPX, waypoints, metadata) for offline access.
|
||||
|
||||
#### Scenario: Download a route
|
||||
- **WHEN** the user taps "Download for offline" on a route
|
||||
- **THEN** the route GPX, parsed waypoints, and metadata are stored in the local SQLite database
|
||||
|
||||
#### Scenario: View offline route
|
||||
- **WHEN** the user views a downloaded route without network connectivity
|
||||
- **THEN** the route is loaded from the local database and displayed on the map with cached tiles
|
||||
|
||||
#### Scenario: Delete offline data
|
||||
- **WHEN** the user removes a route from offline storage
|
||||
- **THEN** all cached data for that route (GPX, tiles, metadata) is deleted from the device
|
||||
|
||||
### Requirement: Map tile caching
|
||||
The system SHALL cache map tiles for downloaded route regions so maps are viewable offline.
|
||||
|
||||
#### Scenario: Tile download for route region
|
||||
- **WHEN** a route is downloaded for offline use
|
||||
- **THEN** map tiles covering the route bounding box at relevant zoom levels are cached locally
|
||||
|
||||
#### Scenario: Storage budget
|
||||
- **WHEN** the total offline storage exceeds the configured budget (shown on Profile tab)
|
||||
- **THEN** the system warns the user and suggests removing older offline routes
|
||||
|
||||
### Requirement: Offline edit queue
|
||||
The system SHALL queue route edits made offline and sync them when connectivity returns.
|
||||
|
||||
#### Scenario: Queue edits while offline
|
||||
- **WHEN** the user edits a route without network connectivity
|
||||
- **THEN** the changes are saved to a local edit queue and a sync-pending indicator is shown
|
||||
|
||||
#### Scenario: Sync on reconnect
|
||||
- **WHEN** the device regains network connectivity
|
||||
- **THEN** queued edits are sent to the Journal API in order and the sync indicator clears
|
||||
|
||||
#### Scenario: Conflict on sync
|
||||
- **WHEN** the server version of a route changed while the user was offline
|
||||
- **THEN** the system warns the user and applies last-write-wins, preserving the local version
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Map-based waypoint editing
|
||||
The system SHALL allow users to add, move, and delete waypoints on a native map view.
|
||||
|
||||
#### Scenario: Add waypoint
|
||||
- **WHEN** the user long-presses on the map
|
||||
- **THEN** a new waypoint is added at that location and the route is recomputed through it
|
||||
|
||||
#### Scenario: Move waypoint
|
||||
- **WHEN** the user drags an existing waypoint marker
|
||||
- **THEN** the waypoint position updates and the route segments connected to it are recomputed
|
||||
|
||||
#### Scenario: Delete waypoint
|
||||
- **WHEN** the user taps a waypoint and confirms deletion
|
||||
- **THEN** the waypoint is removed and the route is recomputed without it
|
||||
|
||||
### Requirement: Overnight stops and POI snap
|
||||
The system SHALL support marking waypoints as overnight stops and snapping to nearby POIs.
|
||||
|
||||
#### Scenario: Toggle overnight stop
|
||||
- **WHEN** the user taps a waypoint and toggles "Overnight stop"
|
||||
- **THEN** the waypoint is marked with an overnight icon and day segments update accordingly
|
||||
|
||||
#### Scenario: POI snap suggestion
|
||||
- **WHEN** the user adds a waypoint near a known POI (campsite, shelter, etc.)
|
||||
- **THEN** the system suggests snapping to the POI location and applying its name
|
||||
|
||||
### Requirement: BRouter routing
|
||||
The system SHALL compute route segments between waypoints via the BRouter routing engine.
|
||||
|
||||
#### Scenario: Route computation
|
||||
- **WHEN** two or more waypoints exist
|
||||
- **THEN** the system requests a route from BRouter (proxied through the Journal API) and displays the resulting polyline on the map
|
||||
|
||||
#### Scenario: Routing error
|
||||
- **WHEN** BRouter fails to compute a route between waypoints
|
||||
- **THEN** the system shows an error and falls back to displaying a straight line between waypoints
|
||||
|
||||
### Requirement: Save route to Journal
|
||||
The system SHALL save edited routes back to the Journal via its API.
|
||||
|
||||
#### Scenario: Save after editing
|
||||
- **WHEN** the user taps "Save" after editing a route
|
||||
- **THEN** the updated GPX is generated from current waypoints and route geometry, and saved to the Journal API as a new version
|
||||
|
||||
#### Scenario: Unsaved changes warning
|
||||
- **WHEN** the user navigates away from the editor with unsaved changes
|
||||
- **THEN** the system prompts the user to save or discard changes
|
||||
26
openspec/changes/mobile-app/specs/mobile-testing/spec.md
Normal file
26
openspec/changes/mobile-app/specs/mobile-testing/spec.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Unit and component testing with Jest
|
||||
The mobile app SHALL use Jest + jest-expo + React Native Testing Library for unit and component tests.
|
||||
|
||||
#### Scenario: Unit tests run
|
||||
- **WHEN** `pnpm test` is run in the mobile workspace
|
||||
- **THEN** Jest executes all `*.test.ts(x)` files with jest-expo preset
|
||||
|
||||
#### Scenario: Component rendering tests
|
||||
- **WHEN** a component test renders a screen with React Native Testing Library
|
||||
- **THEN** the test can query by accessibility label, text, and testID without a device
|
||||
|
||||
### Requirement: E2E testing with Maestro
|
||||
The mobile app SHALL use Maestro for end-to-end flow testing on real/emulated devices.
|
||||
|
||||
#### Scenario: Maestro flow execution
|
||||
- **WHEN** a Maestro YAML flow is run against a development build
|
||||
- **THEN** the flow interacts with the app via the accessibility layer (tap, scroll, assert text)
|
||||
|
||||
#### Scenario: E2E in CI
|
||||
- **WHEN** a PR is opened
|
||||
- **THEN** Maestro E2E flows run against an EAS preview build
|
||||
|
||||
### Requirement: No Vitest for mobile
|
||||
Vitest SHALL NOT be used for the mobile app. React Native Testing Library has incomplete Vitest support — Jest with jest-expo is the stable, Expo-recommended choice.
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Deep link from mobile to Planner
|
||||
The system SHALL allow the mobile app to open the Planner web app for full collaborative editing of a route.
|
||||
|
||||
#### Scenario: Open in Planner from mobile
|
||||
- **WHEN** the user taps "Edit in Planner" on a route in the mobile app
|
||||
- **THEN** the device browser opens the Planner with the route loaded and a JWT callback URL for saving back to the Journal
|
||||
|
||||
#### Scenario: Return to mobile after Planner save
|
||||
- **WHEN** the user saves a route in the Planner web session that was opened from the mobile app
|
||||
- **THEN** the Planner triggers a deep link back to the mobile app, which refreshes the route to show the updated version
|
||||
|
||||
#### Scenario: Planner session without save
|
||||
- **WHEN** the user returns to the mobile app without saving in the Planner
|
||||
- **THEN** the mobile app shows the route unchanged (no stale data)
|
||||
30
openspec/changes/mobile-app/specs/shared-packages/spec.md
Normal file
30
openspec/changes/mobile-app/specs/shared-packages/spec.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: React Native compatibility for types package
|
||||
The `@trails-cool/types` package SHALL work in React Native without modification.
|
||||
|
||||
#### Scenario: Types import in mobile app
|
||||
- **WHEN** the mobile app imports interfaces from `@trails-cool/types`
|
||||
- **THEN** all types resolve correctly with no DOM or Node.js API dependencies
|
||||
|
||||
### Requirement: React Native compatibility for gpx package
|
||||
The `@trails-cool/gpx` package SHALL work in React Native by using a platform-appropriate XML parser.
|
||||
|
||||
#### Scenario: GPX parsing on mobile
|
||||
- **WHEN** the mobile app calls `parseGpx()` with a GPX string
|
||||
- **THEN** the package uses the React Native runtime's DOMParser (or a polyfill) instead of `linkedom`, and returns the same typed output as on Node.js
|
||||
|
||||
#### Scenario: GPX generation on mobile
|
||||
- **WHEN** the mobile app calls `generateGpx()` with route data
|
||||
- **THEN** the package produces valid GPX XML without relying on Node.js APIs
|
||||
|
||||
### Requirement: React Native compatibility for i18n package
|
||||
The `@trails-cool/i18n` package SHALL work in React Native with the same translation files.
|
||||
|
||||
#### Scenario: i18n initialization on mobile
|
||||
- **WHEN** the mobile app initializes i18n using `@trails-cool/i18n`
|
||||
- **THEN** translations load correctly and `useTranslation()` works in React Native components
|
||||
|
||||
#### Scenario: Language switching
|
||||
- **WHEN** the user changes language in the mobile app's Profile tab
|
||||
- **THEN** all strings update to the selected language using the shared translation files
|
||||
149
openspec/changes/mobile-app/tasks.md
Normal file
149
openspec/changes/mobile-app/tasks.md
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
## Phase 1: Foundation
|
||||
|
||||
### 1.1 App Scaffold
|
||||
|
||||
- [ ] 1.1.1 Initialize Expo managed project at `apps/mobile/` with TypeScript template
|
||||
- [ ] 1.1.2 Configure pnpm workspace to include `apps/mobile` and resolve shared packages (`@trails-cool/types`, `@trails-cool/gpx`, `@trails-cool/i18n`)
|
||||
- [ ] 1.1.3 Set up Expo Router with bottom tab navigation (Map, Routes, Activities, Profile) and placeholder screens
|
||||
- [ ] 1.1.4 Add app icon, splash screen, and `app.config.ts` with bundle identifiers for iOS and Android
|
||||
- [ ] 1.1.5 Configure EAS Build for development and preview profiles
|
||||
|
||||
### 1.2 API Contract Package
|
||||
|
||||
- [ ] 1.2.1 Create `packages/api/` with `API_VERSION` constant, endpoint path constants, and TypeScript types for all request/response shapes
|
||||
- [ ] 1.2.2 Define route types: `RouteListResponse`, `RouteDetailResponse`, `CreateRouteRequest`, `UpdateRouteRequest`
|
||||
- [ ] 1.2.3 Define activity types: `ActivityListResponse`, `ActivityDetailResponse`, `CreateActivityRequest`
|
||||
- [ ] 1.2.4 Define auth types: `TokenExchangeRequest`, `TokenResponse`, `DiscoveryResponse`
|
||||
- [ ] 1.2.5 Define shared error type: `ApiErrorResponse` with code, message, and optional field errors
|
||||
- [ ] 1.2.6 Export everything from package index, add to pnpm workspace and Turborepo pipeline
|
||||
|
||||
### 1.3 Shared Package Compatibility
|
||||
|
||||
- [ ] 1.2.1 Audit `@trails-cool/types` for DOM/Node.js dependencies — confirm it's pure TypeScript interfaces
|
||||
- [ ] 1.2.2 Refactor `@trails-cool/gpx` to use a platform-agnostic XML parser: use `linkedom` on Node.js and `DOMParser` on React Native/browser
|
||||
- [ ] 1.2.3 Write unit tests for `parseGpx()` and `generateGpx()` running in a jsdom-free environment to verify no DOM dependency
|
||||
- [ ] 1.2.4 Verify `@trails-cool/i18n` initializes in React Native — add an `initMobile()` export if the current init assumes a browser environment
|
||||
- [ ] 1.2.5 Add `apps/mobile` to Turborepo pipeline (`turbo.json`) for build and typecheck
|
||||
|
||||
### 1.3 Journal Auth (OAuth2 PKCE)
|
||||
|
||||
- [ ] 1.3.1 Add `journal.oauth_clients` table with client_id, redirect_uri, and trusted flag
|
||||
- [ ] 1.3.2 Add `journal.oauth_codes` table (code, userId, clientId, codeChallenge, codeChallengeMethod, expiresAt)
|
||||
- [ ] 1.3.3 Add `journal.oauth_tokens` table (accessToken, refreshToken, userId, clientId, expiresAt)
|
||||
- [ ] 1.3.4 Implement `GET /oauth/authorize` endpoint — show login UI, generate auth code on success, redirect to client
|
||||
- [ ] 1.3.5 Implement `POST /oauth/token` endpoint — validate PKCE code_verifier, issue access + refresh tokens
|
||||
- [ ] 1.3.6 Implement refresh token grant in `POST /oauth/token`
|
||||
- [ ] 1.3.7 Add middleware to validate OAuth2 bearer tokens on existing API routes
|
||||
- [ ] 1.3.8 Seed `trails-cool-mobile` as a trusted first-party OAuth2 client with `trailscool://` redirect URI
|
||||
- [ ] 1.3.9 Write unit tests for PKCE challenge validation, token issuance, and token refresh
|
||||
|
||||
### 1.4 Server Configuration
|
||||
|
||||
- [ ] 1.4.1 Add server URL input on login screen with "Connect to a different server" toggle (defaults to `https://trails.cool`)
|
||||
- [ ] 1.4.2 Add `GET /.well-known/trails-cool` discovery endpoint on the Journal — returns instance name, version, API base URL
|
||||
- [ ] 1.4.3 Validate entered server URL by fetching discovery endpoint before proceeding to login
|
||||
- [ ] 1.4.4 Check `apiVersion` semver from discovery against app's required minimum — block with upgrade prompt if server is too old
|
||||
- [ ] 1.4.5 Persist server URL in Expo SecureStore, use as base for all API calls and OAuth2 flow
|
||||
- [ ] 1.4.6 Re-check API version on app foreground (after background) — show banner if version mismatch detected
|
||||
- [ ] 1.4.5 Add "Switch server" option on Profile tab — logs out, clears local data, returns to login
|
||||
|
||||
### 1.5 Journal API Client (Mobile)
|
||||
|
||||
- [ ] 1.4.1 Create `apps/mobile/src/lib/api-client.ts` with base HTTP client, auth header injection, and 401 auto-refresh logic
|
||||
- [ ] 1.4.2 Create `apps/mobile/src/lib/auth.ts` with OAuth2 PKCE login flow using `expo-auth-session` and token storage via `expo-secure-store`
|
||||
- [ ] 1.4.3 Implement routes API methods: listRoutes, getRoute, updateRoute, createRoute
|
||||
- [ ] 1.4.4 Implement activities API methods: listActivities, getActivity, createActivity
|
||||
- [ ] 1.4.5 Add typed error handling for network failures, 401, and server errors
|
||||
- [ ] 1.4.6 Write unit tests for API client with mocked fetch responses
|
||||
|
||||
## Phase 2: Routes
|
||||
|
||||
### 2.1 Route List
|
||||
|
||||
- [ ] 2.1.1 Build Routes tab screen with paginated route list fetched from Journal API
|
||||
- [ ] 2.1.2 Display route cards with name, distance, elevation, and thumbnail map preview
|
||||
- [ ] 2.1.3 Add pull-to-refresh and loading/error states
|
||||
- [ ] 2.1.4 Add i18n keys for route list strings (en + de)
|
||||
|
||||
### 2.2 Route Detail
|
||||
|
||||
- [ ] 2.2.1 Build route detail screen with `react-native-maps` showing the route polyline and waypoint markers
|
||||
- [ ] 2.2.2 Display route metadata: name, distance, elevation gain, number of days, waypoint list
|
||||
- [ ] 2.2.3 Add "Edit", "Download Offline", and "Edit in Planner" action buttons
|
||||
- [ ] 2.2.4 Style map markers and polyline colors to match the web Planner
|
||||
|
||||
### 2.3 Route Editing
|
||||
|
||||
- [ ] 2.3.1 Implement add-waypoint via long-press on map, inserting at the nearest route segment
|
||||
- [ ] 2.3.2 Implement drag-to-move for waypoint markers
|
||||
- [ ] 2.3.3 Implement waypoint deletion with confirmation
|
||||
- [ ] 2.3.4 Add overnight stop toggle in waypoint detail sheet
|
||||
- [ ] 2.3.5 Add POI snap suggestions when adding waypoints near known POIs
|
||||
- [ ] 2.3.6 Integrate BRouter routing via Journal API proxy — recompute route segments on waypoint changes
|
||||
- [ ] 2.3.7 Implement save: generate GPX from current waypoints + geometry, PUT to Journal API
|
||||
- [ ] 2.3.8 Add unsaved-changes guard when navigating away from the editor
|
||||
|
||||
## Phase 3: Testing
|
||||
|
||||
### 3.1 Unit & Component Tests
|
||||
|
||||
- [ ] 3.1.1 Set up Jest + jest-expo + React Native Testing Library in `apps/mobile/`
|
||||
- [ ] 3.1.2 Write unit tests for API client (mocked fetch, auth refresh, error handling)
|
||||
- [ ] 3.1.3 Write component tests for route list, route detail, and route editor screens
|
||||
- [ ] 3.1.4 Write unit tests for offline SQLite storage layer
|
||||
- [ ] 3.1.5 Add test script to Turborepo pipeline
|
||||
|
||||
### 3.2 E2E Tests (Maestro)
|
||||
|
||||
- [ ] 3.2.1 Install Maestro CLI and create `apps/mobile/.maestro/` test directory
|
||||
- [ ] 3.2.2 Write Maestro flow: login → see route list → open route detail
|
||||
- [ ] 3.2.3 Write Maestro flow: edit route → add waypoint → save
|
||||
- [ ] 3.2.4 Write Maestro flow: download route for offline → toggle airplane mode → view cached route
|
||||
- [ ] 3.2.5 Configure Maestro CI integration with EAS Build (run E2E on preview builds)
|
||||
|
||||
## Phase 4: Offline
|
||||
|
||||
### 4.1 Route Download
|
||||
|
||||
- [ ] 4.1.1 Set up Expo SQLite database with tables for offline routes, waypoints, and edit queue
|
||||
- [ ] 4.1.2 Implement route download: fetch GPX + metadata from API, store in SQLite
|
||||
- [ ] 4.1.3 Build download progress UI with cancel support
|
||||
- [ ] 4.1.4 Implement offline route loading — detect network state and load from SQLite when offline
|
||||
|
||||
### 4.2 Tile Cache
|
||||
|
||||
- [ ] 4.2.1 Implement tile download manager: given a route bounding box, download tiles at zoom levels 10-15
|
||||
- [ ] 4.2.2 Store tiles in the file system via `expo-file-system` with a lookup index in SQLite
|
||||
- [ ] 4.2.3 Configure `react-native-maps` to use cached tiles when offline
|
||||
- [ ] 4.2.4 Add storage budget display and management on Profile tab (total cached size, delete individual routes)
|
||||
|
||||
### 4.3 Offline Edit Queue
|
||||
|
||||
- [ ] 4.3.1 Implement edit queue in SQLite: store pending route edits with timestamps
|
||||
- [ ] 4.3.2 Add sync-pending indicator on routes with queued edits
|
||||
- [ ] 4.3.3 Implement sync-on-reconnect: process queued edits in order via Journal API
|
||||
- [ ] 4.3.4 Handle sync conflicts: warn user if server version changed, apply last-write-wins
|
||||
|
||||
## Phase 5: Polish
|
||||
|
||||
### 5.1 Deep Links
|
||||
|
||||
- [ ] 5.1.1 Configure `trailscool://` URL scheme in `app.config.ts` for iOS and Android
|
||||
- [ ] 5.1.2 Implement deep link handler: parse `trailscool://routes/:id` and `trailscool://activities/:id`, navigate to detail screens
|
||||
- [ ] 5.1.3 Implement "Edit in Planner" flow: open Planner URL in browser with JWT callback, handle return deep link
|
||||
- [ ] 5.1.4 Add universal links / App Links for `https://trails.cool/routes/:id` (optional, requires associated domains)
|
||||
|
||||
### 5.2 Push Notifications
|
||||
|
||||
- [ ] 5.2.1 Set up `expo-notifications` with push token registration
|
||||
- [ ] 5.2.2 Add Journal API endpoint to register device push tokens
|
||||
- [ ] 5.2.3 Send push notifications for route updates on shared routes (Journal server-side)
|
||||
- [ ] 5.2.4 Handle notification taps — deep link to the relevant route or activity
|
||||
|
||||
### 5.3 App Store Preparation
|
||||
|
||||
- [ ] 5.3.1 Write App Store and Play Store descriptions (en + de) with screenshots
|
||||
- [ ] 5.3.2 Add privacy nutrition labels for iOS (location, health, network) and Android data safety section
|
||||
- [ ] 5.3.3 Configure EAS Submit for iOS App Store and Google Play
|
||||
- [ ] 5.3.4 Set up over-the-air updates via EAS Update for non-native JS changes
|
||||
- [ ] 5.3.5 Test full app flow end-to-end on physical iOS and Android devices
|
||||
Loading…
Add table
Add a link
Reference in a new issue