import React, { useCallback, useEffect, useRef, useState } from 'react'; import { ActivityIndicator, StatusBar, StyleSheet, View } from 'react-native'; import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context'; import { colors } from './src/theme'; import { OnboardingScreen } from './src/features/onboarding/OnboardingScreen'; import { onboardingRepository, type OnboardingAnswers, } from './src/features/onboarding/onboardingRepository'; import { CardiacMirrorScreen } from './src/features/cardiacMirror/CardiacMirrorScreen'; import { MyCoherenceScreen } from './src/features/coherence/MyCoherenceScreen'; import { coherenceSessionRepository } from './src/features/coherence/coherenceSessionRepository'; import { HapticWaveExerciseScreen, type HapticExerciseSummary, } from './src/features/cardiacMirror/HapticWaveExerciseScreen'; import { getRegulationProtocol, recommendRegulationProtocol, type HapticRegulationProtocolKind, } from './src/features/cardiacMirror/regulationProtocols'; import { compareRegulationMeasurements } from './src/features/cardiacMirror/regulationComparison'; import { regulationSessionRepository } from './src/features/cardiacMirror/regulationSessionRepository'; import type { RegulationComparison, RegulationSession, } from './src/features/cardiacMirror/regulationSessionTypes'; import { usePPGCamera } from './src/features/ppg/camera/usePPGCamera'; import { evaluateHealthKitContext } from './src/features/ppg/health/healthContextEvaluator'; import { healthKitService } from './src/features/ppg/health/healthKitService'; import type { HealthKitContext } from './src/features/ppg/health/healthTypes'; import { PPG_ALGORITHM_VERSION } from './src/features/ppg/measurement/measurementConfiguration'; import { usePPGMeasurement } from './src/features/ppg/measurement/usePPGMeasurement'; import { PPGFailureScreen } from './src/features/ppg/screens/PPGFailureScreen'; import { PPGHistoryScreen } from './src/features/ppg/screens/PPGHistoryScreen'; import { PPGMeasurementScreen } from './src/features/ppg/screens/PPGMeasurementScreen'; import { PPGPreparationScreen } from './src/features/ppg/screens/PPGPreparationScreen'; import { PPGResultScreen } from './src/features/ppg/screens/PPGResultScreen'; import { calculatePotentialStress } from './src/features/ppg/stress/potentialStressCalculator'; import type { BaselineCalculationResult } from './src/features/ppg/stress/baselineTypes'; import { baselineRepository } from './src/features/ppg/storage/baselineRepository'; import { acquisitionPreferencesRepository } from './src/features/ppg/storage/acquisitionPreferencesRepository'; import { measurementRepository } from './src/features/ppg/storage/measurementRepository'; import { settingsRepository } from './src/features/ppg/storage/settingsRepository'; import type { DataRetentionPolicy, MeasurementSelfReport, StoredMeasurement, } from './src/features/ppg/storage/storageTypes'; import {calculateWatchBaseline, type WatchBaselineV1} from './src/features/watch/watchBaseline'; import {watchRepository} from './src/features/watch/watchRepository'; import {watchSyncCoordinator} from './src/features/watch/watchService'; import type { WatchConnectionState, WatchMeasurementSummaryV1, WatchRegulationSessionV1, WatchSyncEnvelopeV1, WatchTombstoneV1, } from './src/features/watch/watchTypes'; type AppView = 'measurement' | 'history' | 'cardiacMirror' | 'hapticExercise' | 'myCoherence'; type FinalizationState = 'idle' | 'saving' | 'failed'; type SelfReportState = 'idle' | 'saving' | 'saved' | 'failed'; type OnboardingStatus = 'loading' | 'required' | 'completed'; const EMPTY_WATCH_CONNECTION: WatchConnectionState = { supported: false, paired: false, watchAppInstalled: false, reachable: false, activationState: 'notActivated', latestEventId: null, latestEventAt: null, }; function createMeasurementId(): string { return `ppg-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; } function App(): React.JSX.Element { return ( ); } function HaptikaApp(): React.JSX.Element { const cameraPermission = usePPGCamera(); const measurement = usePPGMeasurement(); const [onboardingStatus, setOnboardingStatus] = useState('loading'); const [onboardingAnswers, setOnboardingAnswers] = useState< OnboardingAnswers | undefined >(); const [appView, setAppView] = useState('measurement'); const [includeHealthKit, setIncludeHealthKit] = useState(false); const [sensitiveContact, setSensitiveContact] = useState(true); const [cameraPosition, setCameraPosition] = useState<'back' | 'front'>( 'front', ); const [atRestSelection, setAtRestSelection] = useState(null); const [history, setHistory] = useState([]); const [watchHistory, setWatchHistory] = useState([]); const [watchRegulationSessions, setWatchRegulationSessions] = useState([]); const [watchBaseline, setWatchBaseline] = useState(null); const [watchConnection, setWatchConnection] = useState(EMPTY_WATCH_CONNECTION); const [saveMindAndBodyWorkouts, setSaveMindAndBodyWorkouts] = useState(true); const [retentionPolicy, setRetentionPolicy] = useState('forever'); const [baselineProgress, setBaselineProgress] = useState(null); const [currentMeasurement, setCurrentMeasurement] = useState(null); const [exerciseProtocol, setExerciseProtocol] = useState('coherence55'); const [pendingProtocolIntent, setPendingProtocolIntent] = useState(null); const [regulationBeforeMeasurement, setRegulationBeforeMeasurement] = useState(null); const [pendingRegulationSession, setPendingRegulationSession] = useState(null); const [pendingCoherenceSessionId, setPendingCoherenceSessionId] = useState< string | null >(null); const [regulationComparison, setRegulationComparison] = useState(null); const [regulationSessions, setRegulationSessions] = useState< RegulationSession[] >([]); const [selfReportState, setSelfReportState] = useState('idle'); const [finalizationState, setFinalizationState] = useState('idle'); const finalizingResultRef = useRef(false); const measurementAtRestRef = useRef(false); const postRegulationMeasurementRef = useRef(false); const refreshHistory = useCallback(async (): Promise => { setHistory(await measurementRepository.getAll()); }, []); const refreshWatchData = useCallback(async (): Promise => { const [measurements, sessions] = await Promise.all([ watchRepository.getMeasurements(), watchRepository.getRegulationSessions(), ]); setWatchHistory(measurements); setWatchRegulationSessions(sessions); setWatchBaseline(calculateWatchBaseline(measurements)); setWatchConnection(await watchSyncCoordinator.connectionState()); }, []); const refreshBaseline = useCallback(async (): Promise => { const result = await baselineRepository.calculate({ measurementHourLocal: new Date().getHours(), cameraPosition, }); setBaselineProgress(result); return result; }, [cameraPosition]); const refreshRegulationSessions = useCallback(async (): Promise => { setRegulationSessions(await regulationSessionRepository.getAll()); }, []); useEffect(() => { onboardingRepository .get() .then(state => { if (state !== null) { setOnboardingAnswers(state); } setOnboardingStatus( state?.completed === true ? 'completed' : 'required', ); }) .catch(() => setOnboardingStatus('required')); settingsRepository.get().then(settings => { setIncludeHealthKit(settings.includeHealthKitContext); setSaveMindAndBodyWorkouts(settings.saveMindAndBodyWorkouts); watchSyncCoordinator.updatePreferences({ schemaVersion: 1, saveMindAndBodyWorkouts: settings.saveMindAndBodyWorkouts, protocolCatalogVersion: '1', updatedAt: new Date().toISOString(), }); }); acquisitionPreferencesRepository.get().then(preferences => { setCameraPosition(preferences.cameraPosition); setSensitiveContact(preferences.sensitiveContact); }); refreshHistory(); refreshBaseline(); refreshRegulationSessions(); refreshWatchData(); watchSyncCoordinator.start(refreshWatchData); measurementRepository.getRetentionPolicy().then(setRetentionPolicy); const connectionTimer = setInterval(() => { watchSyncCoordinator.connectionState().then(setWatchConnection).catch(() => undefined); }, 2_000); return () => { clearInterval(connectionTimer); watchSyncCoordinator.stop(); }; }, [refreshBaseline, refreshHistory, refreshRegulationSessions, refreshWatchData]); useEffect(() => { if (measurement.snapshot.value === 'cancelled') { measurement.reset(); } }, [measurement, measurement.snapshot.value]); useEffect(() => { const result = measurement.snapshot.context.result; if ( measurement.snapshot.value !== 'completed' || result === null || finalizingResultRef.current || finalizationState !== 'idle' || currentMeasurement !== null ) { return; } finalizingResultRef.current = true; setFinalizationState('saving'); (async (): Promise => { let healthContext: HealthKitContext | null = null; let contextConsistency = 0.5; if ( measurement.snapshot.context.healthPermission === 'requestCompleted' ) { try { healthContext = await healthKitService.fetchContext(14); if (healthContext !== null) { const evaluation = evaluateHealthKitContext(healthContext); if (evaluation.hasUsableContext) { contextConsistency = evaluation.contextConfidence; } } } catch { healthContext = null; } } const baselineResult = await baselineRepository.calculate({ measurementHourLocal: new Date().getHours(), cameraPosition, }); const potentialStress = calculatePotentialStress({ atRest: measurementAtRestRef.current, bpm: result.bpm.bpm, estimatedRmssdMs: result.prv.estimatedRmssdMs, prvReliable: result.prv.reliable, validIntervalCount: result.prv.validIntervalCount, signalQuality: result.quality.score, usableDurationSeconds: result.quality.usableDurationSeconds, peakConfidence: result.bpm.confidence, methodAgreementScore: result.quality.methodAgreementScore, contextConsistency, baseline: baselineResult.baseline, }); const record: StoredMeasurement = { schemaVersion: 1, algorithmVersion: PPG_ALGORITHM_VERSION, id: createMeasurementId(), acquisition: { cameraPosition, torchUsed: cameraPosition === 'back', }, measuredAt: new Date().toISOString(), atRest: measurementAtRestRef.current, baselineEligible: measurementAtRestRef.current && !postRegulationMeasurementRef.current, cameraResult: result, potentialStress, healthKitContext: healthContext, selfReport: null, }; await measurementRepository.add(record); if ( pendingRegulationSession !== null && regulationBeforeMeasurement !== null ) { try { await regulationSessionRepository.complete( pendingRegulationSession.id, record.id, record.measuredAt, ); if (pendingCoherenceSessionId !== null) { await coherenceSessionRepository.attachAfterMeasurement( pendingCoherenceSessionId, record.id, ); } await refreshRegulationSessions(); } catch { // The physiological result remains usable even if the local journey // link cannot be persisted. Never discard a completed measurement. } setRegulationComparison( compareRegulationMeasurements(regulationBeforeMeasurement, record), ); setPendingRegulationSession(null); setPendingCoherenceSessionId(null); } await refreshBaseline(); setCurrentMeasurement(record); if (pendingProtocolIntent !== null && pendingRegulationSession === null) { setExerciseProtocol(pendingProtocolIntent); setRegulationBeforeMeasurement(record); setPendingProtocolIntent(null); setAppView('hapticExercise'); } setSelfReportState('idle'); refreshHistory().catch(() => undefined); })() .catch(() => { setFinalizationState('failed'); }) .finally(() => { finalizingResultRef.current = false; }); }, [ currentMeasurement, cameraPosition, finalizationState, measurement.snapshot.context.healthPermission, measurement.snapshot.context.result, measurement.snapshot.value, pendingRegulationSession, pendingCoherenceSessionId, pendingProtocolIntent, regulationBeforeMeasurement, refreshHistory, refreshBaseline, refreshRegulationSessions, ]); const handleHealthKitPreference = useCallback((value: boolean): void => { setIncludeHealthKit(value); settingsRepository.setIncludeHealthKitContext(value); }, []); const handleSaveMindAndBodyPreference = useCallback((value: boolean): void => { setSaveMindAndBodyWorkouts(value); settingsRepository.setSaveMindAndBodyWorkouts(value).catch(() => undefined); watchSyncCoordinator.updatePreferences({ schemaVersion: 1, saveMindAndBodyWorkouts: value, protocolCatalogVersion: '1', updatedAt: new Date().toISOString(), }); }, []); const handleOnboardingHealthKit = useCallback(async (): Promise => { if (!healthKitService.isAvailable()) { return false; } try { const accepted = await healthKitService.requestReadAuthorization(); if (accepted) { setIncludeHealthKit(true); await settingsRepository.setIncludeHealthKitContext(true); } return accepted; } catch { return false; } }, []); const handleCameraPositionPreference = useCallback( (value: 'back' | 'front'): void => { setCameraPosition(value); acquisitionPreferencesRepository .set({ cameraPosition: value, sensitiveContact }) .catch(() => undefined); }, [sensitiveContact], ); const handleSensitiveContactPreference = useCallback( (value: boolean): void => { setSensitiveContact(value); acquisitionPreferencesRepository .set({ cameraPosition, sensitiveContact: value }) .catch(() => undefined); }, [cameraPosition], ); const startMeasurement = useCallback((atRestOverride?: boolean): void => { const selectedAtRest = atRestOverride ?? atRestSelection; if (selectedAtRest === null) return; setAppView('measurement'); setCurrentMeasurement(null); setSelfReportState('idle'); setFinalizationState('idle'); finalizingResultRef.current = false; measurementAtRestRef.current = selectedAtRest; postRegulationMeasurementRef.current = pendingRegulationSession !== null; if (pendingRegulationSession === null) { setRegulationComparison(null); setRegulationBeforeMeasurement(null); } measurement.start({ includeHealthKitContext: includeHealthKit, sensitiveContact, requestCameraPermission: cameraPermission.requestCameraPermission, }); }, [ cameraPermission.requestCameraPermission, atRestSelection, includeHealthKit, measurement, pendingRegulationSession, sensitiveContact, ]); const restartMeasurement = useCallback((): void => { measurement.reset(); setCurrentMeasurement(null); setAtRestSelection(null); setFinalizationState('idle'); finalizingResultRef.current = false; postRegulationMeasurementRef.current = false; setPendingRegulationSession(null); setPendingCoherenceSessionId(null); setRegulationBeforeMeasurement(null); setRegulationComparison(null); }, [measurement]); const retryMeasurement = useCallback((): void => { setCurrentMeasurement(null); finalizingResultRef.current = false; measurement.retry(); if (measurement.snapshot.context.cameraPermission !== 'granted') { Promise.resolve().then(() => startMeasurement()); } }, [measurement, startMeasurement]); const backToPreparation = useCallback((): void => { measurement.reset(); setCurrentMeasurement(null); setAtRestSelection(null); setFinalizationState('idle'); setSelfReportState('idle'); setAppView('measurement'); finalizingResultRef.current = false; postRegulationMeasurementRef.current = false; setPendingRegulationSession(null); setPendingCoherenceSessionId(null); setRegulationBeforeMeasurement(null); setRegulationComparison(null); }, [measurement]); const startRegulationJourney = useCallback( (protocolKind: HapticRegulationProtocolKind): void => { if (currentMeasurement === null) return; setExerciseProtocol(protocolKind); setRegulationBeforeMeasurement(currentMeasurement); setRegulationComparison(null); setAppView('hapticExercise'); }, [currentMeasurement], ); const completeRegulationExercise = useCallback( (summary: HapticExerciseSummary, coherenceSessionId?: string): void => { if (regulationBeforeMeasurement === null || !summary.completed) return; (async (): Promise => { const created = await regulationSessionRepository.create({ protocolKind: summary.protocolKind, beforeMeasurementId: regulationBeforeMeasurement.id, startedAt: summary.startedAt, }); if (coherenceSessionId !== undefined) { await coherenceSessionRepository.attachRegulationSession( coherenceSessionId, created.id, ); setPendingCoherenceSessionId(coherenceSessionId); } else { setPendingCoherenceSessionId(null); } const awaitingAfter = await regulationSessionRepository.markAwaitingAfter( created.id, summary.completedAt, ); await refreshRegulationSessions(); setPendingRegulationSession(awaitingAfter); measurement.reset(); setCurrentMeasurement(null); setAtRestSelection(true); setFinalizationState('idle'); setSelfReportState('idle'); finalizingResultRef.current = false; postRegulationMeasurementRef.current = true; setAppView('measurement'); })().catch(() => { setPendingRegulationSession(null); setPendingCoherenceSessionId(null); setAppView('measurement'); }); }, [measurement, refreshRegulationSessions, regulationBeforeMeasurement], ); const saveSelfReport = useCallback( (report: MeasurementSelfReport): void => { if (currentMeasurement === null) return; setSelfReportState('saving'); measurementRepository .attachSelfReport(currentMeasurement.id, report) .then(async () => { const baselineEligible = currentMeasurement.atRest && regulationComparison === null && ![ report.recentExercise, report.recentCaffeine, report.recentMeal, report.movementDuringMeasurement, report.phoneUnusuallyWarm, ].some(value => value === true); setCurrentMeasurement({ ...currentMeasurement, selfReport: report, baselineEligible, }); setSelfReportState('saved'); await Promise.all([refreshHistory(), refreshBaseline()]); }) .catch(() => setSelfReportState('failed')); }, [currentMeasurement, refreshBaseline, refreshHistory, regulationComparison], ); const deleteMeasurement = useCallback( async (measurementId: string): Promise => { await Promise.all([ measurementRepository.deleteById(measurementId), regulationSessionRepository.deleteRelatedToMeasurement(measurementId), coherenceSessionRepository.deleteRelatedToMeasurement(measurementId), ]); await Promise.all([ refreshHistory(), refreshBaseline(), refreshRegulationSessions(), ]); }, [refreshBaseline, refreshHistory, refreshRegulationSessions], ); const transferWatchTombstone = useCallback((tombstone: WatchTombstoneV1): void => { const envelope: WatchSyncEnvelopeV1 = { schemaVersion: 1, eventId: `iphone-delete-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, deviceId: 'iphone', sequence: 0, sentAt: new Date().toISOString(), kind: 'tombstone', payload: tombstone, }; watchSyncCoordinator.transferEvent(envelope); }, []); const deleteWatchMeasurement = useCallback(async (measurementId: string): Promise => { const tombstone = await watchRepository.deleteMeasurement(measurementId); transferWatchTombstone(tombstone); await refreshWatchData(); }, [refreshWatchData, transferWatchTombstone]); const clearMeasurementData = useCallback(async (): Promise => { const [, , , watchTombstones] = await Promise.all([ measurementRepository.clearAll(), regulationSessionRepository.clearAll(), coherenceSessionRepository.clearAll(), watchRepository.clearAll(), ]); watchTombstones.forEach(transferWatchTombstone); setCurrentMeasurement(null); await Promise.all([ refreshHistory(), refreshBaseline(), refreshRegulationSessions(), refreshWatchData(), ]); }, [refreshBaseline, refreshHistory, refreshRegulationSessions, refreshWatchData, transferWatchTombstone]); const changeRetentionPolicy = useCallback( async (policy: DataRetentionPolicy): Promise => { await measurementRepository.setRetentionPolicy(policy); setRetentionPolicy(policy); await Promise.all([refreshHistory(), refreshBaseline()]); }, [refreshBaseline, refreshHistory], ); if (onboardingStatus === 'loading') { return ( ); } if (onboardingStatus === 'required') { return ( { setOnboardingAnswers(answers); setOnboardingStatus('completed'); }} /> ); } if (appView === 'history') { return ( { setRegulationBeforeMeasurement(null); setPendingRegulationSession(null); setAppView('measurement'); }} /> ); } if (appView === 'cardiacMirror') { return setAppView('measurement')} />; } if (appView === 'hapticExercise') { return ( setAppView('measurement')} onCompleted={completeRegulationExercise} onRequestStart={protocol => { if (currentMeasurement === null) { setPendingProtocolIntent(protocol.kind); setExerciseProtocol(protocol.kind); setAtRestSelection(true); setAppView('measurement'); return true; } setExerciseProtocol(protocol.kind); setRegulationBeforeMeasurement(currentMeasurement); if (protocol.mode === 'livePPG') { cameraPermission.requestCameraPermission().then(granted => { if (granted) setAppView('myCoherence'); }); return true; } return false; }} onCancelled={() => { setRegulationBeforeMeasurement(null); setPendingRegulationSession(null); setPendingCoherenceSessionId(null); }} /> ); } if (appView === 'myCoherence') { return ( { completeRegulationExercise( { protocolKind: 'myCoherence', startedAt: summary.startedAt, completedAt: summary.completedAt, elapsedSeconds: summary.elapsedSeconds, completed: summary.completed, }, session.id, ); }} onCancelled={() => { setRegulationBeforeMeasurement(null); setAppView('hapticExercise'); }} /> ); } const state = measurement.snapshot.value; if ( state === 'preparingCamera' || state === 'waitingForFinger' || state === 'stabilizing' || state === 'measuring' || state === 'processing' ) { return ( ); } if (state === 'completed') { if (currentMeasurement === null) { if (finalizationState === 'failed') { return ( setFinalizationState('idle')} onBack={backToPreparation} /> ); } return ( ); } return ( ); } if ( state === 'invalid' || state === 'thermalStop' || state === 'interrupted' || state === 'error' ) { return ( ); } return ( setAppView('history')} onOpenCardiacMirror={() => setAppView('cardiacMirror')} onOpenHapticExercise={() => setAppView('hapticExercise')} isStarting={ state === 'requestingCameraPermission' || state === 'requestingHealthPermission' } /> ); } const styles = StyleSheet.create({ safeArea: { flex: 1, backgroundColor: colors.background, }, loadingResult: { flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: colors.background, }, }); export default App;