diff --git a/homeflow/README.md b/homeflow/README.md
new file mode 100644
index 0000000..3133ec6
--- /dev/null
+++ b/homeflow/README.md
@@ -0,0 +1,190 @@
+# HomeFlow
+
+A research app for Stanford's CS342 — Building for Digital Health. HomeFlow helps men with BPH (benign prostatic hyperplasia) passively track their voiding patterns, sleep, and activity before and after bladder outlet surgery, so researchers can actually measure whether surgery works in the real world — not just in a clinic.
+
+Built by Stream Team (Team 3).
+
+---
+
+## What it does
+
+Right now, when a patient has prostate surgery, the main outcome measure is a questionnaire they fill out in a waiting room. HomeFlow replaces that with continuous, passive data collection at home using hardware and sensors the patient already has.
+
+Concretely, it:
+
+- Records every void using a **Throne uroflow device** attached to the patient's toilet, capturing flow rate, volume, and flow curve shape
+- Pulls **activity, sleep, and heart rate** data from Apple Watch and HealthKit automatically, once per day
+- Collects a **baseline IPSS symptom score** at enrollment and follow-up scores at 1 and 12 weeks post-surgery
+- Walks patients through **enrollment and informed consent** entirely in-app, with a signed PDF stored securely
+- Syncs everything to a **Firebase backend** for the research team to analyze
+
+The patient doesn't need to do anything after setup. The app runs in the background.
+
+---
+
+## The patient journey
+
+```
+Download app
+ ↓
+Eligibility screening (takes ~2 min)
+ ↓
+Informed consent + digital signature
+ ↓
+Create account
+ ↓
+Grant HealthKit + Throne permissions
+ ↓
+Medical history review (pre-filled from Apple Health)
+ ↓
+Baseline IPSS survey
+ ↓
+Done — passive collection starts
+ ↓
+Follow-up IPSS at 1 week and 12 weeks post-surgery
+```
+
+---
+
+## Tech stack
+
+| Layer | What we use |
+|---|---|
+| App framework | React Native + Expo 54, TypeScript |
+| Navigation | Expo Router (file-based) |
+| Uroflow data | Throne API |
+| Health data | Apple HealthKit + Apple Watch |
+| Clinical records | Apple Health FHIR (CDA/HL7 XML, parsed on-device) |
+| Backend | Firebase (Firestore + Storage + Cloud Functions) |
+| Auth | Firebase Auth (Apple Sign In) |
+| Forms | Formik + Yup |
+| Surveys | Custom IPSS questionnaire component |
+
+---
+
+## Project structure
+
+```
+app/
+ (onboarding)/ # Enrollment flow: welcome → eligibility → consent → account → permissions → medical history → survey
+ (tabs)/ # Main app: home, voiding log, profile
+ _layout.tsx # Root layout, auth guards, provider hierarchy
+
+src/services/
+ clinicalNotesSync.ts # HealthKit FHIR clinical notes → parse CDA XML → Firebase Storage + Firestore
+ consentPdfSync.ts # Generate signed consent PDF → Firebase Storage
+ healthkitSync.ts # Activity, sleep, HRV → Firestore
+ throneFirestore.ts # Throne session/metric reads + medical history writes
+ cdaParser.ts # Decode and parse HL7 CDA XML from Apple Health
+
+lib/
+ services/ # HealthKit client, consent service, onboarding state
+ consent/ # IRB consent document content
+ questionnaires/ # IPSS and eligibility questionnaire definitions
+
+components/
+ ui/ # SignaturePad, icons, shared primitives
+ onboarding/ # Progress bar, consent agreement, continue button
+
+functions/ # Firebase Cloud Functions (Throne data ingestion)
+```
+
+---
+
+## Getting started
+
+You'll need:
+- macOS with Xcode installed
+- A physical iPhone (HealthKit does not work on simulators)
+- Node.js 18+
+- Firebase CLI: `npm install -g firebase-tools`
+- A `.env` file with the keys below (ask a teammate)
+
+```bash
+# Install dependencies
+npm install
+
+# Start the dev server
+npx expo start
+
+# Run on physical device (replace UDID with yours)
+npx expo run:ios --device YOUR_DEVICE_UDID
+
+# Run tests
+npm test
+
+# Deploy Firestore rules
+firebase deploy --only firestore
+
+# Deploy Storage rules
+firebase deploy --only storage
+
+# Deploy Cloud Functions
+firebase deploy --only functions
+```
+
+### Environment variables
+
+```
+EXPO_PUBLIC_FIREBASE_API_KEY=
+EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN=
+EXPO_PUBLIC_FIREBASE_PROJECT_ID=
+EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET=
+EXPO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=
+EXPO_PUBLIC_FIREBASE_APP_ID=
+EXPO_PUBLIC_BACKEND_TYPE=firebase
+THRONE_BASE_URL=
+THRONE_API_KEY=
+THRONE_TIMEZONE=
+STUDY_ID=
+```
+
+> `THRONE_API_KEY` is server-side only — it lives in Cloud Functions and never reaches the client bundle.
+
+---
+
+## Data model
+
+Everything in Firestore is scoped under `users/{uid}/`:
+
+| Collection | What's in it |
+|---|---|
+| `throne_sessions/` | Void events from the Throne device (written by Cloud Function) |
+| `throne_metrics/` | Flow rate curves per session |
+| `hk_stepCount/` | Daily step counts from HealthKit |
+| `hk_sleepAnalysis/` | Sleep stages and duration |
+| `hk_heartRate/` | Heart rate samples |
+| `hk_heartRateVariabilitySDNN/` | HRV samples |
+| `medical_history/current` | Confirmed demographics, medications, conditions, procedures |
+| `medical_history_prefill/latest` | FHIR-parsed pre-fill from Apple Health (read-only to user) |
+| `clinical_notes/` | CDA XML notes from Apple Health, parsed to readable text |
+| `surgery_date/current` | Patient's scheduled surgery date |
+| `consent_response/current` | Consent record + storage path of signed PDF |
+
+Firebase Storage holds:
+- `users/{uid}/consent_pdfs/` — signed consent PDFs
+- `users/{uid}/clinical_notes/` — raw decoded CDA XML for the MedGemma research pipeline
+
+---
+
+## A few things worth knowing
+
+**Clinical notes come as HL7 CDA XML, not PDFs.** Apple Health delivers clinical documents as base64-encoded CDA XML. We parse them on-device into human-readable sections and store both the parsed text (in Firestore, for the app) and the raw XML (in Storage, for the research pipeline).
+
+**Consent is recorded in two places.** The local AsyncStorage entry is the fast gate-keeper (used to block/allow onboarding steps). The Firebase record + signed PDF is the durable IRB audit trail.
+
+**The Throne API key never touches the client.** All Throne API calls go through a Cloud Function. The client only reads from Firestore.
+
+**Background sync doesn't require the app to be open.** HealthKit background delivery is registered at onboarding completion. The app wakes up to sync data once per day even if the patient never opens it.
+
+**This is a research prototype.** Some stubs exist. Demo-safe fallbacks are in place when Throne hardware isn't available. Data is not used for clinical care.
+
+---
+
+## Team
+
+Stream Team (Team 3) — Stanford CS342, Winter 2026
+
+Principal Investigator: Ryan Sun, MD
+IRB Protocol: IRB# -----
+Contact: homeflow-study@stanford.edu
diff --git a/homeflow/app/(onboarding)/account.tsx b/homeflow/app/(onboarding)/account.tsx
index edadc5b..77e058b 100644
--- a/homeflow/app/(onboarding)/account.tsx
+++ b/homeflow/app/(onboarding)/account.tsx
@@ -5,7 +5,7 @@
* In dev mode, a skip button allows bypassing auth for faster iteration.
*/
-import React, { useState, useEffect } from 'react';
+import React, { useState } from 'react';
import {
View,
Text,
@@ -29,6 +29,7 @@ import { OnboardingService } from '@/lib/services/onboarding-service';
import { useAuth } from '@/hooks/use-auth';
import { saveSurgeryDate } from '@/src/services/throneFirestore';
import { getAuth } from '@/src/services/firestore';
+import { uploadConsentPdf } from '@/src/services/consentPdfSync';
import {
OnboardingProgressBar,
ContinueButton,
@@ -38,7 +39,7 @@ export default function AccountScreen() {
const router = useRouter();
const colorScheme = useColorScheme();
const colors = Colors[colorScheme ?? 'light'];
- const { signInWithEmail, signUpWithEmail, signInWithApple, signInWithGoogle, isAuthenticated } = useAuth();
+ const { signInWithEmail, signUpWithEmail, signInWithApple, signInWithGoogle, signOut, isAuthenticated, user } = useAuth();
const [mode, setMode] = useState<'login' | 'signup'>('signup');
const [email, setEmail] = useState('');
@@ -47,24 +48,36 @@ export default function AccountScreen() {
const [lastName, setLastName] = useState('');
const [loading, setLoading] = useState(false);
- // If user is already authenticated, advance automatically
- useEffect(() => {
- if (isAuthenticated) {
- handleAdvance();
- }
- }, [isAuthenticated]);
+ // If already signed in, show a "continue / switch account" prompt rather than
+ // silently skipping — this handles the onboarding-reset-while-logged-in case.
const handleAdvance = async () => {
- // Flush any surgery date collected before login to Firestore now that we have a UID.
const uid = getAuth().currentUser?.uid;
if (uid) {
const data = await OnboardingService.getData();
+
+ // Flush surgery date collected pre-login
const surgeryDate = data.eligibility?.surgeryDate;
if (surgeryDate) {
saveSurgeryDate(uid, surgeryDate).catch((err) => {
console.warn('[Account] Failed to flush surgery date to Firestore:', err);
});
}
+
+ // Upload consent PDF now that we have a UID
+ const pending = data.pendingConsentPdf;
+ if (pending) {
+ uploadConsentPdf({
+ signatureType: pending.signatureType,
+ participantName: pending.participantName,
+ signatureValue: pending.signatureValue,
+ consentDate: pending.consentDate,
+ }).then((result) => {
+ if (!result.ok) {
+ console.warn('[Account] Consent PDF upload failed (non-fatal):', result.error);
+ }
+ });
+ }
}
await OnboardingService.goToStep(OnboardingStep.PERMISSIONS);
@@ -160,6 +173,29 @@ export default function AccountScreen() {
+ {/* Already signed in — let user continue or switch accounts */}
+ {isAuthenticated && (
+
+
+ Signed in as {user?.email ?? 'your account'}
+
+
+
+ Continue
+
+ { await signOut(); }}
+ >
+ Switch Account
+
+
+
+ )}
+
{mode === 'signup' && (
@@ -371,6 +407,46 @@ const styles = StyleSheet.create({
fontSize: 17,
fontWeight: '500',
},
+ alreadySignedIn: {
+ borderWidth: 1,
+ borderRadius: 12,
+ padding: Spacing.md,
+ marginBottom: Spacing.lg,
+ gap: Spacing.sm,
+ },
+ alreadySignedInText: {
+ fontSize: 15,
+ fontWeight: '500',
+ textAlign: 'center',
+ },
+ alreadySignedInButtons: {
+ flexDirection: 'row',
+ gap: Spacing.sm,
+ },
+ continueAsButton: {
+ flex: 1,
+ height: 44,
+ borderRadius: 10,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ continueAsButtonText: {
+ color: '#fff',
+ fontSize: 15,
+ fontWeight: '600',
+ },
+ switchButton: {
+ flex: 1,
+ height: 44,
+ borderRadius: 10,
+ borderWidth: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ switchButtonText: {
+ fontSize: 15,
+ fontWeight: '500',
+ },
footer: {
flexDirection: 'row',
justifyContent: 'center',
diff --git a/homeflow/app/(onboarding)/consent.tsx b/homeflow/app/(onboarding)/consent.tsx
index ed49c21..3568315 100644
--- a/homeflow/app/(onboarding)/consent.tsx
+++ b/homeflow/app/(onboarding)/consent.tsx
@@ -40,7 +40,6 @@ import {
import { IconSymbol } from '@/components/ui/icon-symbol';
import { SignaturePad, type SignaturePadRef } from '@/components/ui/SignaturePad';
import { useAuth } from '@/hooks/use-auth';
-import { uploadConsentPdf } from '@/src/services/consentPdfSync';
function buildConsentText(): string {
const header = [
@@ -123,16 +122,16 @@ export default function ConsentScreen() {
// Record consent locally (source of truth for gate-keeping)
await ConsentService.recordConsent(signatureValue);
- // Upload signed PDF to Firebase Storage + write Firestore metadata.
- // Non-fatal: failure should not block the participant from proceeding.
- const pdfResult = await uploadConsentPdf({
- signatureType: signatureMode === 'type' ? 'typed' : 'drawn',
- participantName,
- signatureValue,
+ // Store signature data so account.tsx can upload the PDF after sign-in.
+ // We can't upload now because the user isn't authenticated yet.
+ await OnboardingService.updateData({
+ pendingConsentPdf: {
+ signatureType: signatureMode === 'type' ? 'typed' : 'drawn',
+ participantName,
+ signatureValue,
+ consentDate: new Date().toISOString(),
+ },
});
- if (!pdfResult.ok) {
- console.warn('[Consent] PDF upload failed (non-fatal):', pdfResult.error);
- }
// Advance onboarding
await OnboardingService.goToStep(OnboardingStep.ACCOUNT);
diff --git a/homeflow/app/(onboarding)/medical-history.tsx b/homeflow/app/(onboarding)/medical-history.tsx
index 7831cdd..d37081f 100644
--- a/homeflow/app/(onboarding)/medical-history.tsx
+++ b/homeflow/app/(onboarding)/medical-history.tsx
@@ -429,13 +429,24 @@ export default function MedicalHistoryScreen() {
setReviewStep(0);
setCorrectionsNeeded(new Set());
- setDemoName('');
setDemoAge('');
setDemoBiologicalSex('');
setDemoEthnicity('');
setDemoRace('');
- setDemoStage('name');
setDemoEditingField(null);
+
+ // Pre-fill name from consent signature — must happen after all resets
+ // so it isn't overwritten. The consent record is in-memory so this is fast.
+ const consentRecord = await ConsentService.getConsentRecord();
+ const consentName = consentRecord?.participantSignature;
+ if (consentName && consentName !== '[Drawn signature provided]') {
+ setDemoName(consentName);
+ setDemoStage('ethnicity');
+ } else {
+ setDemoName('');
+ setDemoStage('name');
+ }
+
setPhase('reviewing');
} catch {
setPhase('reviewing');
@@ -446,20 +457,6 @@ export default function MedicalHistoryScreen() {
loadPrefillData();
}, [loadPrefillData]);
- // Pre-fill name from typed consent signature so the user doesn't enter it twice
- useEffect(() => {
- let cancelled = false;
- ConsentService.getConsentRecord().then((record) => {
- if (cancelled) return;
- const sig = record?.participantSignature;
- if (sig && sig !== '[Drawn signature provided]') {
- setDemoName(sig);
- setDemoStage('ethnicity');
- }
- }).catch(() => {});
- return () => { cancelled = true; };
- }, []);
-
// ── Review step navigation ────────────────────────────────────────
const handleConfirmStep = useCallback((withCorrection = false) => {
diff --git a/homeflow/lib/constants.ts b/homeflow/lib/constants.ts
index d99d7c7..54e4447 100644
--- a/homeflow/lib/constants.ts
+++ b/homeflow/lib/constants.ts
@@ -95,6 +95,6 @@ export const STUDY_INFO = {
institution: 'Stanford University',
principalInvestigator: 'Ryan Sun, MD',
irbProtocol: 'IRB# -----',
- contactEmail: 'homeflow-study@stanford.edu',
+ contactEmail: 'info@streamsyncresearch.com',
contactPhone: '713-677-1764',
} as const;
diff --git a/homeflow/lib/services/onboarding-service.ts b/homeflow/lib/services/onboarding-service.ts
index d116d1b..d5318e2 100644
--- a/homeflow/lib/services/onboarding-service.ts
+++ b/homeflow/lib/services/onboarding-service.ts
@@ -47,6 +47,14 @@ export interface OnboardingData {
throne: 'granted' | 'denied' | 'not_determined' | 'skipped';
};
+ // Consent PDF — stored pre-auth, uploaded to Firebase Storage after sign-in
+ pendingConsentPdf?: {
+ signatureType: 'typed' | 'drawn';
+ participantName: string | null;
+ signatureValue: string;
+ consentDate: string; // ISO string
+ };
+
// IPSS baseline score
ipssBaseline?: {
score: number;
diff --git a/homeflow/src/services/consentPdfSync.ts b/homeflow/src/services/consentPdfSync.ts
index b459fe2..a7223cf 100644
--- a/homeflow/src/services/consentPdfSync.ts
+++ b/homeflow/src/services/consentPdfSync.ts
@@ -31,6 +31,8 @@ export interface ConsentSignatureData {
participantName: string | null;
/** The raw value passed to ConsentService.recordConsent — typed name or marker string. */
signatureValue: string;
+ /** ISO string of when the participant actually signed (may differ from upload time). */
+ consentDate?: string;
}
export interface ConsentPdfResult {
@@ -122,7 +124,8 @@ export async function uploadConsentPdf(
}
const now = new Date();
- const consentDate = now.toLocaleDateString('en-US', {
+ const signedAt = signature.consentDate ? new Date(signature.consentDate) : now;
+ const consentDate = signedAt.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',