Skip to content

Latest commit

 

History

History
848 lines (688 loc) · 33.9 KB

File metadata and controls

848 lines (688 loc) · 33.9 KB

Fitness Tracker Integration Strategy

Executive Summary

Your database is already well-architected for fitness trackers with a provider-agnostic design.

Decisions Made

Decision Choice
Priority providers Fitbit → Whoop → Oura
Sync architecture Hybrid (Edge Functions for OAuth providers, native SDKs for Apple/Google later)
Frontend UX Profile screen expansion with "Connected Devices" section
Sync triggers Manual, Scheduled (cron), Webhooks
Intraday data retention Retain all data indefinitely
Initial backfill Maximum available historical data
Cron schedule Daily at 4 AM UTC (fixed time)
Token storage Dedicated table per provider

Part 1: Backend Architecture

1.1 Current Schema (Already Solid)

fitness_trackers (provider-agnostic registry)
    ├── activities (workouts)
    ├── daily_summary (steps, calories, resting HR)
    ├── intraday_data (minute-level HR, step deltas)
    └── sleep_sessions (sleep periods)

Key design strengths:

  • vendor_metadata JSONB columns preserve provider-specific data without schema changes
  • last_summary_sync_date / last_intraday_sync_date enable incremental sync
  • ingestion_log table ready for sync status tracking

1.2 Data Normalization Strategy

Normalized Fields Provider-Specific (vendor_metadata)
steps, calories, distance_m Fitbit: activityCalories, sedentaryMinutes
heart_rate, resting_heart_rate Whoop: strain, recovery, hrv
total_duration_minutes (sleep) Oura: readinessScore, temperatureDelta
activity_type, duration_sec All: raw API response fields

Store raw API responses in vendor_metadata and extract key fields into normalized columns for cross-provider analytics.

1.3 Token Storage Architecture

Decision: Dedicated table per provider

Provider Token Table
Fitbit fitbit_tokens (already exists)
Whoop whoop_tokens (to create)
Oura oura_tokens (to create)

Reserve user_credentials for API keys or non-OAuth credentials.

1.4 Sync Architecture

Decision: Hybrid Approach

Provider Integration Method
Fitbit Edge Function (OAuth + REST API)
Whoop Edge Function (OAuth + REST API)
Oura Edge Function (OAuth + REST API)
Apple Health Mobile HealthKit SDK (future)
Google Fit Mobile Health Connect SDK (future)
Mobile App → Supabase Edge Function → Provider API → Supabase DB

Why Edge Functions for Fitbit/Whoop/Oura:

  • Tokens never leave your backend
  • Server-side token refresh (no mobile token expiry issues)
  • Works when app is closed (webhooks/scheduled syncs)
  • Single codebase for web + mobile

Part 2: OAuth Flow Architecture

2.1 Reuse Your WebView Pattern

Your GoogleAuthWebView.jsx pattern is reusable:

// TrackerAuthWebView.jsx
const authUrls = {
  fitbit: `${baseUrl}/api/auth/fitbit/start`,
  whoop: `${baseUrl}/api/auth/whoop/start`,
  oura: `${baseUrl}/api/auth/oura/start`,
};

2.2 OAuth Flow Sequence

┌─────────────┐     ┌──────────────┐     ┌─────────────┐     ┌──────────────┐
│  Mobile App │────▶│ WebView Auth │────▶│ Provider    │────▶│ Supabase     │
│             │     │ Page         │     │ OAuth       │     │ Edge Fn      │
└─────────────┘     └──────────────┘     └─────────────┘     └──────────────┘
       │                                                            │
       │◀───────────── postMessage({ tracker_id, provider }) ──────┘
       │
       ▼
   Save tracker_id locally, start sync

2.3 Supabase Edge Function for OAuth Callback

// supabase/functions/fitbit-callback/index.ts
export default async function handler(req: Request) {
  const { code, state } = parseQuery(req.url);
  const userId = verifyState(state); // CSRF protection

  // Exchange code for tokens
  const tokens = await exchangeFitbitCode(code);

  // Store tokens
  await supabase.from('fitbit_tokens').upsert({
    user_id: userId,
    access_token: tokens.access_token,
    refresh_token: tokens.refresh_token,
    expires_at: new Date(Date.now() + tokens.expires_in * 1000),
    fitbit_user_id: tokens.user_id,
    scope: tokens.scope,
  });

  // Create tracker record
  const { data: tracker } = await supabase.from('fitness_trackers').insert({
    user_id: userId,
    provider: 'fitbit',
    device_name: 'Fitbit', // Can be updated later with device info
  }).select().single();

  // Return HTML that sends postMessage to WebView
  return new Response(successHtml(tracker.tracker_id), {
    headers: { 'Content-Type': 'text/html' },
  });
}

Part 3: Data Sync Strategy

3.1 Sync Triggers

Decision: Manual + Cron + Webhooks (no auto-sync on app foreground)

Trigger Implementation
Manual User taps "Sync Now" → calls Edge Function
Scheduled (cron) Supabase pg_cron runs daily sync for all users
Webhooks Provider pushes updates → Edge Function receives

3.2 Initial Backfill vs. Ongoing Sync

Sync Type Trigger Data Range
Initial backfill After OAuth complete Maximum available (all historical data)
Daily sync Cron job (4 AM UTC) Yesterday + today
Real-time Webhooks As received

3.3 Fitbit Sync Implementation

// supabase/functions/sync-fitbit/index.ts
export default async function handler(req: Request) {
  const { user_id, tracker_id, sync_type } = await req.json();

  // Get fresh tokens (refresh if needed)
  const tokens = await getFreshFitbitTokens(user_id);

  // Determine date range (max available on initial, incremental thereafter)
  const lastSync = await getLastSyncDate(tracker_id, sync_type);
  const startDate = lastSync || null; // null = fetch all available history
  const endDate = new Date();

  // Fetch and store data
  if (sync_type === 'summary') {
    await syncDailySummary(tracker_id, tokens, startDate, endDate);
  } else if (sync_type === 'intraday') {
    await syncIntradayData(tracker_id, tokens, startDate, endDate);
  } else if (sync_type === 'sleep') {
    await syncSleepSessions(tracker_id, tokens, startDate, endDate);
  } else if (sync_type === 'activities') {
    await syncActivities(tracker_id, tokens, startDate, endDate);
  }

  // Log sync status
  await supabase.from('ingestion_log').insert({
    user_id,
    provider: 'fitbit',
    status: 'success',
    message: `Synced ${sync_type} from ${startDate} to ${endDate}`,
  });
}

3.4 Data Transformer Pattern

// Normalize Fitbit data to your schema
function transformFitbitDailySummary(fitbitData: FitbitSummary): DailySummaryInsert {
  return {
    tracker_id: trackerId,
    activity_date: fitbitData.dateTime,
    steps: fitbitData.summary.steps,
    distance_m: fitbitData.summary.distances.find(d => d.activity === 'total')?.distance * 1000,
    calories_total: fitbitData.summary.caloriesOut,
    resting_heart_rate: fitbitData.summary.restingHeartRate,
    vendor_metadata: {
      // Preserve Fitbit-specific data
      activityCalories: fitbitData.summary.activityCalories,
      sedentaryMinutes: fitbitData.summary.sedentaryMinutes,
      lightlyActiveMinutes: fitbitData.summary.lightlyActiveMinutes,
      fairlyActiveMinutes: fitbitData.summary.fairlyActiveMinutes,
      veryActiveMinutes: fitbitData.summary.veryActiveMinutes,
      floors: fitbitData.summary.floors,
      elevation: fitbitData.summary.elevation,
    },
  };
}

3.5 Intraday Data Retention

Decision: Retain all data indefinitely

Fitbit intraday HR data can be ~1440 rows/day/user. With full retention:

  • 1 year = ~525,600 rows per user
  • Storage cost is acceptable for the analytical value
  • No retention policy needed

Part 4: Frontend UX Strategy

4.1 Profile Screen Enhancement

Add a "Connected Devices" section to your profile screen:

┌─────────────────────────────────────┐
│ Profile                             │
├─────────────────────────────────────┤
│ [Avatar] John Doe                   │
│ john@example.com                    │
├─────────────────────────────────────┤
│ Connected Devices                   │
│ ┌─────────────────────────────────┐ │
│ │ [+] Connect a Fitness Tracker   │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ [✓] Fitbit Charge 5             │ │
│ │     Last synced: 2 hours ago    │ │
│ │     [Sync Now] [Disconnect]     │ │
│ └─────────────────────────────────┘ │
├─────────────────────────────────────┤
│ My Templates                        │
│ Account Settings                    │
│ ...                                 │
└─────────────────────────────────────┘

4.2 Device Connection Flow

┌──────────────────────┐
│ Connect a Device     │
├──────────────────────┤
│ [Fitbit logo]        │
│ Fitbit               │
├──────────────────────┤
│ [Whoop logo]         │
│ WHOOP                │
├──────────────────────┤
│ [Oura logo]          │
│ Oura Ring            │
└──────────────────────┘

4.3 Component Structure

mobile/src/
├── components/
│   ├── FitnessTracker/
│   │   ├── ConnectedDeviceCard.jsx      # Shows connected tracker status
│   │   ├── DevicePickerModal.jsx        # Provider selection grid
│   │   └── TrackerAuthWebView.jsx       # OAuth WebView (reuse pattern)
│   └── ...
├── utils/
│   ├── fitnessTrackers/
│   │   ├── useConnectedTrackers.js      # React Query hook for trackers
│   │   ├── useSyncTracker.js            # Mutation hook for manual sync
│   │   └── providers.js                 # Provider config (logos, names, auth URLs)
│   └── ...
└── app/
    └── (tabs)/
        └── profile.jsx                  # Add Connected Devices section

Part 5: Provider-Specific Details

5.1 Fitbit (Phase 1)

Aspect Details
OAuth Standard OAuth 2.0, 8-hour access token
API REST, well-documented
Rate limits 150 requests/hour per user
Webhooks Subscription API for real-time updates
Data available Activities, sleep, HR (intraday requires app approval)
Token table fitbit_tokens (exists)

5.2 Whoop (Phase 2)

Aspect Details
OAuth OAuth 2.0
API REST API (newer, evolving)
Rate limits TBD (generally permissive)
Webhooks Available
Data available Strain, recovery, sleep, HR, HRV
Token table whoop_tokens (to create)

Whoop-specific vendor_metadata:

{
  "strain": 14.2,
  "recovery": 67,
  "hrv": 45,
  "respiratory_rate": 15.2,
  "skin_temp": 33.1
}

5.3 Oura (Phase 3)

Aspect Details
OAuth OAuth 2.0
API REST v2, straightforward
Rate limits Generous (5000/day)
Webhooks Available
Data available Sleep, readiness, activity, HR
Token table oura_tokens (to create)

Oura-specific vendor_metadata:

{
  "readiness_score": 85,
  "sleep_score": 78,
  "activity_score": 92,
  "temperature_delta": 0.2,
  "lowest_heart_rate": 52
}

Part 6: Implementation Phases

Phase 1: Foundation + Fitbit

Database:

  • Verify fitness tables exist in Supabase (run migrations if needed)
  • Add RLS policies for fitness tables

Frontend:

  • Build ConnectedDeviceCard component
  • Build DevicePickerModal component
  • Build TrackerAuthWebView component
  • Add "Connected Devices" section to profile screen
  • Create useConnectedTrackers React Query hook
  • Create useSyncTracker mutation hook

Backend (Edge Functions):

  • fitbit-auth-start - Initiates OAuth flow
  • fitbit-callback - Handles OAuth callback, stores tokens
  • sync-fitbit - Fetches and stores Fitbit data
  • fitbit-webhook - Receives real-time updates

Cron:

  • Set up pg_cron job for daily Fitbit sync

Phase 2: Whoop Integration

Database:

  • Create whoop_tokens table (mirror fitbit_tokens structure)

Backend (Edge Functions):

  • whoop-auth-start - Initiates OAuth flow
  • whoop-callback - Handles OAuth callback
  • sync-whoop - Fetches and stores Whoop data
  • whoop-webhook - Receives real-time updates

Frontend:

  • Add Whoop to DevicePickerModal
  • Update providers.js with Whoop config

Phase 3: Oura Integration

Database:

  • Create oura_tokens table

Backend (Edge Functions):

  • oura-auth-start - Initiates OAuth flow
  • oura-callback - Handles OAuth callback
  • sync-oura - Fetches and stores Oura data
  • oura-webhook - Receives real-time updates

Frontend:

  • Add Oura to DevicePickerModal
  • Update providers.js with Oura config

Phase 4: Analytics & Insights (Future)

  • Cross-provider data views (unified sleep analysis)
  • Correlate fitness data with food/supplement logging
  • Surface insights ("Your HRV improved 12% since starting creatine")
  • Compare with similar users (age, activity level, goals)

Phase 5: Additional Providers (Future)

  • Apple Health (HealthKit SDK - iOS native)
  • Google Fit (Health Connect SDK - Android native)
  • Garmin (OAuth 1.0a - more complex)

Part 7: Database Migrations Needed

New Token Tables

-- whoop_tokens
CREATE TABLE whoop_tokens (
  user_id UUID PRIMARY KEY REFERENCES user_profiles(user_id),
  whoop_user_id TEXT,
  access_token TEXT NOT NULL,
  refresh_token TEXT NOT NULL,
  expires_at TIMESTAMPTZ NOT NULL,
  token_type TEXT,
  scope TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- oura_tokens
CREATE TABLE oura_tokens (
  user_id UUID PRIMARY KEY REFERENCES user_profiles(user_id),
  oura_user_id TEXT,
  access_token TEXT NOT NULL,
  refresh_token TEXT NOT NULL,
  expires_at TIMESTAMPTZ NOT NULL,
  token_type TEXT,
  scope TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

RLS Policies

-- fitness_trackers RLS
ALTER TABLE fitness_trackers ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users can view own trackers"
  ON fitness_trackers FOR SELECT
  USING (auth.uid() = user_id);

CREATE POLICY "Users can insert own trackers"
  ON fitness_trackers FOR INSERT
  WITH CHECK (auth.uid() = user_id);

CREATE POLICY "Users can delete own trackers"
  ON fitness_trackers FOR DELETE
  USING (auth.uid() = user_id);

-- Similar policies for daily_summary, intraday_data, sleep_sessions, activities

Part 8: Architecture Diagram

┌─────────────────────────────────────────────────────────────────────┐
│                        MOBILE APP                                    │
│  ┌─────────────┐  ┌──────────────┐  ┌─────────────────────────────┐ │
│  │ Profile     │  │ Device       │  │ TrackerAuth                 │ │
│  │ Screen      │──│ Picker       │──│ WebView                     │ │
│  └─────────────┘  └──────────────┘  └─────────────────────────────┘ │
│         │                                        │                   │
└─────────┼────────────────────────────────────────┼───────────────────┘
          │ React Query                            │ OAuth WebView
          ▼                                        ▼
┌─────────────────────────────────────────────────────────────────────┐
│                      SUPABASE EDGE FUNCTIONS                         │
│  ┌─────────────┐  ┌──────────────┐  ┌─────────────────────────────┐ │
│  │ *-auth-     │  │ sync-*       │  │ *-webhook                   │ │
│  │ start       │  │ (fitbit,     │  │ (real-time                  │ │
│  │ *-callback  │  │ whoop, oura) │  │ updates)                    │ │
│  └─────────────┘  └──────────────┘  └─────────────────────────────┘ │
│         │                │                       ▲                   │
│         │                │                       │ Webhooks          │
│         ▼                ▼                       │                   │
│  ┌──────────────────────────────────────────────┴────────────────┐  │
│  │                    PROVIDER APIs                               │  │
│  │         Fitbit API  |  Whoop API  |  Oura API                 │  │
│  └───────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────┘
          │                │
          ▼                ▼
┌─────────────────────────────────────────────────────────────────────┐
│                      SUPABASE DATABASE                               │
│  ┌─────────────┐  ┌──────────────┐  ┌─────────────────────────────┐ │
│  │ fitness_    │  │ daily_       │  │ intraday_data               │ │
│  │ trackers    │──│ summary      │  │ sleep_sessions              │ │
│  │             │  │ activities   │  │ fitbit/whoop/oura_tokens    │ │
│  └─────────────┘  └──────────────┘  └─────────────────────────────┘ │
│                                                                      │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │ pg_cron: Daily sync job for all connected trackers            │  │
│  └───────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────┘

Part 9: Test Cases (TDD Approach)

9.1 Frontend Component Tests

ConnectedDeviceCard.test.jsx

Test Case Description
renders device name and provider Shows "Fitbit Charge 5" and Fitbit logo
renders last synced time Shows "Last synced: 2 hours ago" with correct relative time
renders never synced state Shows "Never synced" when last_synced is null
sync button triggers mutation Tapping "Sync Now" calls useSyncTracker mutation
shows syncing state Button shows spinner and "Syncing..." during sync
disconnect shows confirmation Tapping "Disconnect" opens confirmation dialog
disconnect confirmed removes tracker Confirming disconnect calls delete mutation
handles sync error Shows error toast when sync fails

DevicePickerModal.test.jsx

Test Case Description
renders when visible Modal appears when visible=true
hidden when not visible Modal hidden when visible=false
shows all supported providers Lists Fitbit, Whoop, Oura with logos
provider tap triggers onSelect Tapping Fitbit calls onSelect('fitbit')
close button dismisses Tapping X calls onClose
disables already connected providers Fitbit shown as "Connected" if already linked
shows provider descriptions Each provider shows brief description

TrackerAuthWebView.test.jsx

Test Case Description
renders WebView with correct URL WebView source is fitbit auth URL for provider='fitbit'
handles success postMessage Receiving {tracker_id, provider} calls onSuccess
handles error postMessage Receiving {error} calls onError
close button dismisses Tapping X calls onClose
shows loading indicator Shows spinner while WebView loads

Profile Connected Devices Section

Test Case Description
shows connect button when no trackers Empty state with "Connect a Fitness Tracker"
shows tracker list when connected Renders ConnectedDeviceCard for each tracker
connect button opens picker Tapping connect opens DevicePickerModal
shows loading state Shows skeleton while fetching trackers
handles fetch error Shows error message when fetch fails

9.2 Frontend Hook Tests

useConnectedTrackers.test.js

Test Case Description
returns loading initially isLoading=true before fetch completes
returns trackers on success data contains array of user's trackers
returns empty array for new user data=[] when user has no trackers
returns error on failure error populated when fetch fails
filters by current user Only returns trackers for authenticated user
includes last_synced timestamps Each tracker has last_synced field
refetches on invalidation Calling invalidate triggers refetch

useSyncTracker.test.js

Test Case Description
calls Edge Function with tracker_id POST to sync-fitbit with correct payload
returns loading during sync isLoading=true while syncing
updates last_synced on success Tracker's last_synced updated after sync
invalidates trackers query useConnectedTrackers refetches on success
returns error on failure error populated when sync fails
handles token refresh error Shows re-auth prompt on 401

useDisconnectTracker.test.js

Test Case Description
deletes tracker from database Removes row from fitness_trackers
deletes associated tokens Removes row from fitbit_tokens
invalidates trackers query List refreshes after disconnect
returns error on failure error populated when delete fails

9.3 Backend Edge Function Tests

fitbit-auth-start.test.ts

Test Case Description
generates secure state parameter State includes user_id + random nonce
stores state in database State saved to oauth_states table with expiry
returns redirect to Fitbit 302 redirect to api.fitbit.com/oauth2/authorize
includes required scopes URL has scope=activity+sleep+heartrate+profile
includes correct redirect_uri Callback URL matches Edge Function URL
requires authentication Returns 401 if no auth token

fitbit-callback.test.ts

Test Case Description
verifies state parameter Rejects if state doesn't match stored state
rejects expired state Returns error if state older than 10 minutes
exchanges code for tokens POST to api.fitbit.com/oauth2/token succeeds
stores tokens in fitbit_tokens Tokens upserted with user_id as key
creates fitness_tracker record New row in fitness_trackers with provider='fitbit'
returns success HTML Response includes postMessage script
handles invalid code Returns error page for bad authorization code
handles Fitbit API error Returns error page if token exchange fails
stores fitbit_user_id Saves Fitbit's user ID for API calls
triggers initial backfill Queues sync job for all historical data

sync-fitbit.test.ts

Test Case Description
refreshes expired token Calls refresh endpoint if expires_at < now
updates stored token after refresh New access_token saved to fitbit_tokens
fetches daily summary GET /1/user/-/activities/date/{date}.json
transforms daily summary Maps Fitbit fields to daily_summary schema
stores daily summary Upserts into daily_summary table
fetches sleep sessions GET /1.2/user/-/sleep/date/{date}.json
transforms sleep data Maps sleep stages to sleep_sessions schema
stores sleep sessions Upserts into sleep_sessions table
fetches activities GET /1/user/-/activities/list.json
transforms activities Maps activity fields to activities schema
stores activities Upserts into activities table
fetches intraday heart rate GET /1/user/-/activities/heart/date/{date}/1d/1min.json
transforms intraday data Maps HR readings to intraday_data schema
stores intraday data Batch inserts into intraday_data table
uses incremental sync Only fetches data after last_synced
updates last_synced timestamps Updates fitness_trackers.last_synced
logs to ingestion_log Creates success/failure log entry
handles rate limit (429) Retries with backoff, logs warning
handles auth error (401) Marks tracker as needing re-auth
handles partial failure Continues with other data types if one fails

fitbit-webhook.test.ts

Test Case Description
validates webhook signature Rejects requests with invalid signature
handles verification request Returns 204 with verify parameter
parses notification payload Extracts collectionType and ownerId
triggers sync for activities Queues sync when collectionType=activities
triggers sync for sleep Queues sync when collectionType=sleep
triggers sync for body Queues sync when collectionType=body
logs webhook receipt Creates entry in ingestion_log
handles unknown user Logs warning for unrecognized ownerId
returns 204 on success Always returns 204 to Fitbit

9.4 Data Transformer Tests

transformFitbitDailySummary.test.ts

Test Case Description
maps steps correctly summary.steps → steps
converts distance to meters summary.distances[total] * 1000 → distance_m
maps calories summary.caloriesOut → calories_total
maps resting HR summary.restingHeartRate → resting_heart_rate
preserves vendor_metadata activityCalories, sedentaryMinutes, etc. in JSON
handles missing optional fields Returns null for missing fields
sets correct activity_date Date string in YYYY-MM-DD format

transformFitbitSleep.test.ts

Test Case Description
maps start_time sleep.startTime → start_time (ISO 8601)
maps end_time sleep.endTime → end_time (ISO 8601)
calculates duration minutesAsleep → total_duration_minutes
preserves sleep stages deep, light, rem, wake in vendor_metadata
handles multiple sessions Returns array for multiple sleep periods
handles classic vs stages Works with both Fitbit sleep data formats

transformFitbitActivity.test.ts

Test Case Description
maps activity_type activityName → activity_type
maps start_time startTime → start_time (ISO 8601)
converts duration activeDuration (ms) / 1000 → duration_sec
converts distance distance (km) * 1000 → distance_m
maps calories calories → calories_burned
preserves raw data Full activity object in vendor_metadata

transformFitbitIntraday.test.ts

Test Case Description
maps timestamp time + date → timestamp (ISO 8601)
maps heart_rate value → heart_rate
handles step deltas steps intraday → steps_delta
batch transforms array Handles 1440 data points efficiently
deduplicates by timestamp Prevents duplicate entries on re-sync

9.5 Database & RLS Tests

fitness_trackers RLS

Test Case Description
user can select own trackers SELECT succeeds for own user_id
user cannot select others' trackers SELECT returns empty for other user_id
user can insert own tracker INSERT succeeds with own user_id
user cannot insert for other user INSERT fails with other user_id
user can delete own tracker DELETE succeeds for own tracker
user cannot delete others' tracker DELETE fails for other user's tracker
service role bypasses RLS Service role can access all data

daily_summary RLS

Test Case Description
user can select via tracker join SELECT succeeds for own tracker's data
user cannot select others' data SELECT returns empty for other user's tracker
service role can insert Edge Function can insert for any tracker

Token table RLS

Test Case Description
tokens not accessible to users SELECT fails for users (service role only)
service role can read tokens Edge Functions can read for sync
service role can update tokens Token refresh updates work

9.6 Integration Tests

Full OAuth Flow

Test Case Description
complete Fitbit connection User clicks Connect → OAuth → tracker created
initial backfill runs Historical data synced after OAuth complete
UI updates after connection Profile shows new tracker card

Manual Sync Flow

Test Case Description
sync button triggers full sync All data types fetched and stored
UI shows syncing state Button disabled, spinner shown
last_synced updates Tracker card shows "Just now"

Disconnect Flow

Test Case Description
disconnect removes tracker fitness_trackers row deleted
disconnect removes tokens fitbit_tokens row deleted
disconnect removes data (optional) Associated data optionally purged
UI updates after disconnect Tracker card removed from list

9.7 Test File Structure

mobile/
├── __tests__/
│   ├── components/
│   │   └── FitnessTracker/
│   │       ├── ConnectedDeviceCard.test.jsx
│   │       ├── DevicePickerModal.test.jsx
│   │       └── TrackerAuthWebView.test.jsx
│   └── utils/
│       └── fitnessTrackers/
│           ├── useConnectedTrackers.test.js
│           ├── useSyncTracker.test.js
│           └── useDisconnectTracker.test.js

supabase/
├── functions/
│   ├── fitbit-auth-start/
│   │   ├── index.ts
│   │   └── index.test.ts
│   ├── fitbit-callback/
│   │   ├── index.ts
│   │   └── index.test.ts
│   ├── sync-fitbit/
│   │   ├── index.ts
│   │   ├── index.test.ts
│   │   └── transformers/
│   │       ├── dailySummary.ts
│   │       ├── dailySummary.test.ts
│   │       ├── sleep.ts
│   │       ├── sleep.test.ts
│   │       ├── activity.ts
│   │       ├── activity.test.ts
│   │       ├── intraday.ts
│   │       └── intraday.test.ts
│   └── fitbit-webhook/
│       ├── index.ts
│       └── index.test.ts

Next Steps

  1. Database: Create whoop_tokens and oura_tokens tables, add RLS policies
  2. UI: Build Connected Devices components, add to profile screen
  3. OAuth: Create Fitbit auth Edge Functions
  4. Sync: Implement Fitbit data sync Edge Function
  5. Webhooks: Set up Fitbit Subscription API
  6. Cron: Configure pg_cron for daily sync
  7. Expand: Repeat for Whoop, then Oura