hydrafy

Hydrafy

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.

Setup

# 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

What’s implemented

Onboarding (10 screens)

  1. Splash — gradient + mascot intro
  2. Mascot intro — buddy says hi
  3. Name your buddy — text input + 5 chip suggestions
  4. Weight — slider with kg / lbs toggle
  5. Activity — Low / Medium / High cards
  6. Climate — Cool / Moderate / Hot tiles
  7. Wake & sleep times — native time picker
  8. Daily-goal reveal — animated total + breakdown
  9. Notification permission — requests expo-notifications perms
  10. All set — confetti, finishes onboarding flag

All onboarding data is persisted to AsyncStorage. The flow is skipped on every subsequent launch (see @hydrafy/onboarded).

Core app

Logic

Project structure

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

State management — why Zustand

The brief left it to me. I picked Zustand for these reasons:

AsyncStorage schema

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, …]

Mascot state machine

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.

Design fidelity — where I diverged from the brief

Everything else (palette, type, component styles, screen layouts, mascot states, onboarding copy) mirrors the design.

Known TODOs

v2 roadmap

Credits