A water-tracking React Native + Expo app with a tiny droplet buddy named Drip that morphs with your hydration. Runs entirely on-device — no accounts, no backend.
Built from a Claude Design handoff. See “Design fidelity” below for the small places this implementation diverges from the original brief.
# 1. Install
npm install
# or
yarn install
# 2. Run on a device or simulator
npx expo start
# → press i for iOS simulator
# → press a for Android emulator
# → or scan the QR with Expo Go on a physical phone
Notifications + the Expo Go limitation. Local notifications work in Expo Go on Android but on iOS require a development build since SDK 53. If you want to test reminders end-to-end on iOS, run:
npx expo run:ios # builds a dev client
expo-notifications permsAll onboarding data is persisted to AsyncStorage. The flow is skipped on every
subsequent launch (see @hydrafy/onboarded).
src/utils/goal.js):
base = weight_kg × 33 ml
goal = base × (1 + activityBoost + climateBoost)
ceiling = 3 500 ml // more isn't better
Boosts: medium activity +10 %, high +20 %, hot climate +10 %.
src/hooks/useReminders.js): schedules a notification every
90 min between wake and sleep, picks copy based on whether you’re behind /
on track / ahead. Auto-cancels remaining reminders once the goal is hit.src/utils/tips.js + src/constants/tips.js): 30 hand-written
tips bucketed into 25 / 50 / 75 / 100 % milestone tiers. Each milestone hit
per day surfaces one fresh-or-recycled tip.src/
├─ components/ // Mascot, Button, Card, Chip, ProgressRing,
│ // LiquidFill, QuickAddTile, Dots, Icons, Text, …
├─ constants/ // colors (tokens), tips (30 tips), cosmetics
├─ hooks/ // useReminders
├─ navigation/ // RootNavigator (onboarding gate, tabs, modals)
├─ screens/
│ ├─ onboarding/ // 10 onboarding screens
│ ├─ Home.js
│ ├─ CustomAmount.js
│ ├─ History.js
│ ├─ VsLastWeek.js
│ ├─ Streaks.js
│ ├─ Wardrobe.js
│ ├─ Settings.js
│ ├─ Over120Warning.js
│ ├─ Over150Warning.js
│ └─ TipUnlock.js
├─ state/ // Zustand store (single source of truth)
├─ storage/ // AsyncStorage wrapper with namespaced keys
└─ utils/ // goal, date, tips, mascot state
App.js // bootstraps fonts + hydrates store
index.js // Expo entry
The brief left it to me. I picked Zustand for these reasons:
useStore(selector) reads any slice; you
don’t have to compose 6 Context providers around <App />.src/state/store.js).src/storage/storage.js)
rather than via zustand/middleware/persist. This keeps the on-disk schema
stable across rebuilds and avoids accidentally wiping unrelated namespaced
keys.All keys are namespaced under @hydrafy/.
| Key | Shape |
|---|---|
@hydrafy/onboarded |
'true' |
@hydrafy/profile |
{ name, weightKg, weightUnit, activity, climate, wakeTime, sleepTime, createdAt } |
@hydrafy/settings |
{ goalMl, displayUnit, remindersOn, pausedUntil } |
@hydrafy/log/<YYYY-MM-DD> |
{ date, entries:[{id,ts,ml,kind}], totalMl, goalMl, pct, unlockedTipsToday:[] } |
@hydrafy/history |
{ days: { 'YYYY-MM-DD': { totalMl, goalMl, pct, mascotState } } } (last 60d cache) |
@hydrafy/streak |
{ current, longest, lastHitDate } |
@hydrafy/cosmetics |
{ ownedIds:[], wornId\|null } |
@hydrafy/tipsSeen |
[tipId, …] |
Hydration % State Vibe
─────────────────────────────────────────────────
0 – 25 tired droopy, dull, half-lidded eyes
26 – 60 waking neutral mouth, blush returning
61 – 99 happy smile, blush, faint sparkles
100 – 119 glow halo, big smile, max sparkles
120 – 149 bloated queasy mouth, sweat drops, coral wash
150+ bloated_strong X-eyes, deep coral wash
The mascot is a single SVG that morphs across the entire range with continuous parameters (fill colors, eye openness, mouth path, blush, sparkles, body squish, halo). State is just a bucketed label used for analytics / history.
react-native-svg. If you want true Lottie
later, install lottie-react-native and replace the <Svg> tree in
src/components/Mascot.js. Keep the pct / accessory / size props the
same and the rest of the app just works.68 kg × 33 ml = 2 244 ml).Everything else (palette, type, component styles, screen layouts, mascot states, onboarding copy) mirrors the design.
assets/icon.png and assets/splash.png
are referenced in app.json but not committed. Drop in your own or replace
with Drip art.claude.ai/design).@expo-google-fonts: Sora, Plus Jakarta Sans, JetBrains Mono.