# SwimBuddy Swimming workout tracker with Apple Watch and WearOS companion apps. Expo 57 + React Native 0.86 + NativeWind v5 + Drizzle ORM + Swift Watch target + Kotlin WearOS target. ## Commands ```bash npx expo start # Dev server npx expo run:ios # Build & run on iOS npx expo prebuild -p ios # Regenerate ios/ dir (safe — targets/ persists) npx expo prebuild -p ios && node scripts/add-sentry-watch.js # Prebuild + add SentrySPM to watch target npx drizzle-kit generate # Regenerate SQL migrations after schema changes ``` **WearOS:** ```bash ./scripts/build-wearos.sh debug # Build debug APK (reads version from app.json) ./scripts/build-wearos.sh release # Build release APK (reads version from app.json) ./scripts/generate-wearos-icon.sh assets/icon.png # Generate launcher icons ``` No `npm test` or lint scripts exist yet. No `babel.config.js` — NativeWind v5 uses CSS-first config. ## Project Structure ``` src/ features//screens/ # Screen components (registered in navigators) features//components/ # Feature-specific UI features//hooks/ # Feature-specific hooks components/ # Shared UI primitives (flat) hooks/ # Shared hooks navigation/ # React Navigation (RootNav, TabNav, feature stacks) db/ # Drizzle schema, client, relations, operations providers/ # DatabaseProvider, SettingsProvider (MMKV) utils/ # formatTime, calculateSwolf, uuid, etc. constants/ # strokes, poolLengths, colors types/ # workout.ts, common.ts modules/watch-connectivity/ # Expo native module bridging WCSession (Apple Watch) modules/wear-connectivity/ # Expo native module bridging Wear Data Layer (WearOS) targets/watch/ # Apple Watch app (Swift/SwiftUI) — survives prebuild targets/wearos/ # WearOS app (Kotlin/Compose) — survives prebuild ``` ## Key Conventions - **Path aliases**: `@/`, `@features/`, `@components/`, `@db/`, etc. (tsconfig.json) - **Feature-based** folder structure with **type-based** shared layer - **NativeWind v5**: `className` prop, no babel config, CSS-first. Use `className` not `StyleSheet` - **`className` types**: `nativewind-env.d.ts` at root — don't delete - **lightningcss pinned** to 1.30.1 in package.json overrides — required by NativeWind v5 ## Watch Target (Apple Watch) - Lives in `targets/watch/`, NOT in `ios/` — survives `expo prebuild --clean` - Config: `targets/watch/expo-target.config.js` - Build: open `ios/SwimBuddy.xcworkspace` in Xcode, select Watch scheme - Uses App Groups (`group.nl.guido-it.swimbuddy`) for data sharing - WCSession for real-time messaging, `transferUserInfo` for reliable offline sync - HealthKit for swim workout tracking (indoor swimming) - Swift types `StoredWorkout`, `StoredSet`, `CompletedLap` in `WorkoutManager.swift` - SentrySPM: `@bacons/apple-targets` doesn't support `swiftPackages` yet. Run `node scripts/add-sentry-watch.js` after `expo prebuild` to add it automatically. For EAS builds, this runs automatically via the `eas-build-post-install` hook in package.json. ## WearOS Target - Lives in `targets/wearos/` — survives `expo prebuild --clean` - Kotlin + Jetpack Compose with Wear OS Compose libraries - Build: `./scripts/build-wearos.sh` or open `targets/wearos/` in Android Studio - Uses Wear Data Layer API for phone↔watch communication - Health Services Client for swim workout tracking - Namespace: `nl.guidoit.swimbuddy` ## Database - **expo-sqlite** + **Drizzle ORM** (SQLite dialect, expo driver) - Schema: `src/db/schema.ts` — tables: templates, workouts, workoutSessions, sets, laps (laps references workoutSessions, not workouts) - Relations: `src/db/relations.ts` - CRUD: `src/db/operations.ts` - Client: `src/db/client.ts` — exports `db` and `DATABASE_NAME` - Migrations in `drizzle/` — generated by `npx drizzle-kit generate` ## WCSession Gotchas (Critical) These are hard-won bugs that will waste hours if rediscovered: - **`NSNull` crashes `sendMessage`**: JS `null` becomes `NSNull` in ObjC, causing `WCErrorCodePayloadUnsupportedTypes` (Code 7010). Always strip `null`/`undefined` values before sending via `cleanForWCSession()`. - **Two delegate methods conflict**: Having both `session(_:didReceiveMessage:)` AND `session(_:didReceiveMessage:replyHandler:)` on the same delegate causes iOS to deliver neither. Watch uses `didReceiveMessage` (no replyHandler); phone uses `didReceiveMessage:replyHandler:` for bidirectional sync. - **`isReachable` is unreliable**: Only `true` when both apps are foreground. Check `session.activationState == .activated` instead for connection readiness. - **Phone `sendMessage` must use `replyHandler: nil`**: Otherwise JS thread blocks waiting for a reply that watch never sends. Use fire-and-forget for workout plans; use `transferUserInfo` for data sync. - **Watch `suiteName` vs `UserDefaults.standard`**: Watch only needs local storage, so use `UserDefaults.standard` — the App Groups suite name is only needed for phone↔watch shared data. - **Phone `pendingEvents` polling**: Phone polls `pollEvents()` every 2s because Expo Modules' `sendEvent` API doesn't work on `AppContext`. Watch data arrives via `didReceiveUserInfo` delegate → stored in `pendingEvents` array. ## Watch ↔ Phone Data Flow - **Phone → Watch**: `sendMessage` with workout plan (fire-and-forget, promise resolves immediately with `["status":"sent"]`) - **Watch → Phone**: `transferUserInfo` with completed workout data (reliable, works in background) - **Phone receives**: `pollEvents()` called from JS every 2s, `saveWatchCompletedWorkout()` writes to SQLite ## UI Conventions - Swipe-to-delete on lists uses `SwipeableRow` component (`react-native-gesture-handler`) - BuilderScreen pool length is a 25m/50m toggle (not read-only from settings) - Templates and Stats features removed from navigation (DB schema preserved but UI deleted) - Ionicons used for bottom tab icons (`@expo/vector-icons`) ## Gotchas - `metro.config.js` has `.sql` in `sourceExts` — required for Drizzle migration imports - `expo prebuild` clears and regenerates `ios/` by default. Use `--no-clean` to preserve - Watch target compiles as separate Xcode target — not a RN module - `react-native-css` is a NativeWind v5 peer dependency — don't remove - `@bacons/apple-targets` plugin must be in app.json plugins array - WearOS uses same `applicationId` as iOS bundle identifier — don't change without updating both ## Expo Docs Expo SDK 57: https://docs.expo.dev/versions/v57.0.0/