Files
haptika/App.tsx
T
2026-07-31 16:20:10 +02:00

854 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (
<SafeAreaProvider>
<StatusBar
barStyle="light-content"
translucent
backgroundColor="transparent"
/>
<SafeAreaView
style={styles.safeArea}
edges={['right', 'bottom', 'left']}>
<HaptikaApp />
</SafeAreaView>
</SafeAreaProvider>
);
}
function HaptikaApp(): React.JSX.Element {
const cameraPermission = usePPGCamera();
const measurement = usePPGMeasurement();
const [onboardingStatus, setOnboardingStatus] =
useState<OnboardingStatus>('loading');
const [onboardingAnswers, setOnboardingAnswers] = useState<
OnboardingAnswers | undefined
>();
const [appView, setAppView] = useState<AppView>('measurement');
const [includeHealthKit, setIncludeHealthKit] = useState(false);
const [sensitiveContact, setSensitiveContact] = useState(true);
const [cameraPosition, setCameraPosition] = useState<'back' | 'front'>(
'front',
);
const [atRestSelection, setAtRestSelection] = useState<boolean | null>(null);
const [history, setHistory] = useState<StoredMeasurement[]>([]);
const [watchHistory, setWatchHistory] = useState<WatchMeasurementSummaryV1[]>([]);
const [watchRegulationSessions, setWatchRegulationSessions] = useState<WatchRegulationSessionV1[]>([]);
const [watchBaseline, setWatchBaseline] = useState<WatchBaselineV1 | null>(null);
const [watchConnection, setWatchConnection] = useState<WatchConnectionState>(EMPTY_WATCH_CONNECTION);
const [saveMindAndBodyWorkouts, setSaveMindAndBodyWorkouts] = useState(true);
const [retentionPolicy, setRetentionPolicy] =
useState<DataRetentionPolicy>('forever');
const [baselineProgress, setBaselineProgress] =
useState<BaselineCalculationResult | null>(null);
const [currentMeasurement, setCurrentMeasurement] =
useState<StoredMeasurement | null>(null);
const [exerciseProtocol, setExerciseProtocol] =
useState<HapticRegulationProtocolKind>('coherence55');
const [pendingProtocolIntent, setPendingProtocolIntent] =
useState<HapticRegulationProtocolKind | null>(null);
const [regulationBeforeMeasurement, setRegulationBeforeMeasurement] =
useState<StoredMeasurement | null>(null);
const [pendingRegulationSession, setPendingRegulationSession] =
useState<RegulationSession | null>(null);
const [pendingCoherenceSessionId, setPendingCoherenceSessionId] = useState<
string | null
>(null);
const [regulationComparison, setRegulationComparison] =
useState<RegulationComparison | null>(null);
const [regulationSessions, setRegulationSessions] = useState<
RegulationSession[]
>([]);
const [selfReportState, setSelfReportState] =
useState<SelfReportState>('idle');
const [finalizationState, setFinalizationState] =
useState<FinalizationState>('idle');
const finalizingResultRef = useRef(false);
const measurementAtRestRef = useRef(false);
const postRegulationMeasurementRef = useRef(false);
const refreshHistory = useCallback(async (): Promise<void> => {
setHistory(await measurementRepository.getAll());
}, []);
const refreshWatchData = useCallback(async (): Promise<void> => {
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<BaselineCalculationResult> => {
const result = await baselineRepository.calculate({
measurementHourLocal: new Date().getHours(),
cameraPosition,
});
setBaselineProgress(result);
return result;
}, [cameraPosition]);
const refreshRegulationSessions = useCallback(async (): Promise<void> => {
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<void> => {
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<boolean> => {
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<void> => {
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<void> => {
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<WatchTombstoneV1> = {
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<void> => {
const tombstone = await watchRepository.deleteMeasurement(measurementId);
transferWatchTombstone(tombstone);
await refreshWatchData();
}, [refreshWatchData, transferWatchTombstone]);
const clearMeasurementData = useCallback(async (): Promise<void> => {
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<void> => {
await measurementRepository.setRetentionPolicy(policy);
setRetentionPolicy(policy);
await Promise.all([refreshHistory(), refreshBaseline()]);
},
[refreshBaseline, refreshHistory],
);
if (onboardingStatus === 'loading') {
return (
<View style={styles.loadingResult}>
<ActivityIndicator size="large" color={colors.accent} />
</View>
);
}
if (onboardingStatus === 'required') {
return (
<OnboardingScreen
initialAnswers={onboardingAnswers}
onRequestHealthKit={handleOnboardingHealthKit}
onComplete={answers => {
setOnboardingAnswers(answers);
setOnboardingStatus('completed');
}}
/>
);
}
if (appView === 'history') {
return (
<PPGHistoryScreen
measurements={history}
watchMeasurements={watchHistory}
baselineProgress={baselineProgress}
watchBaseline={watchBaseline}
regulationSessions={regulationSessions}
watchRegulationSessions={watchRegulationSessions}
watchConnectionState={watchConnection}
saveMindAndBodyWorkouts={saveMindAndBodyWorkouts}
retentionPolicy={retentionPolicy}
onDeleteMeasurement={deleteMeasurement}
onDeleteWatchMeasurement={deleteWatchMeasurement}
onClearAll={clearMeasurementData}
onSaveMindAndBodyWorkoutsChange={handleSaveMindAndBodyPreference}
onRetentionPolicyChange={changeRetentionPolicy}
onBack={() => {
setRegulationBeforeMeasurement(null);
setPendingRegulationSession(null);
setAppView('measurement');
}}
/>
);
}
if (appView === 'cardiacMirror') {
return <CardiacMirrorScreen onBack={() => setAppView('measurement')} />;
}
if (appView === 'hapticExercise') {
return (
<HapticWaveExerciseScreen
initialProtocolKind={exerciseProtocol}
onBack={() => 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 (
<MyCoherenceScreen
cameraPosition={cameraPosition}
beforeMeasurementId={regulationBeforeMeasurement?.id ?? null}
onCompleted={(summary, session) => {
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 (
<PPGMeasurementScreen
state={state}
latestFrame={measurement.latestFrame}
phaseProgress={measurement.phaseProgress}
isCameraActive={measurement.isCameraActive}
torchEnabled={measurement.torchEnabled}
cameraPosition={cameraPosition}
onCameraStarted={measurement.onCameraStarted}
onFrameUpdate={measurement.onFrameUpdate}
onInterrupted={measurement.onCameraInterrupted}
onCameraError={measurement.onCameraError}
onCancel={measurement.cancel}
/>
);
}
if (state === 'completed') {
if (currentMeasurement === null) {
if (finalizationState === 'failed') {
return (
<PPGFailureScreen
state="error"
message="Le résultat n’a pas pu être enregistré localement. Vous pouvez réessayer sans relancer la caméra."
onRetry={() => setFinalizationState('idle')}
onBack={backToPreparation}
/>
);
}
return (
<View style={styles.loadingResult}>
<ActivityIndicator size="large" color={colors.accent} />
</View>
);
}
return (
<PPGResultScreen
cameraResult={currentMeasurement.cameraResult}
measurementHistory={history}
potentialStress={currentMeasurement.potentialStress}
healthKitContext={currentMeasurement.healthKitContext}
baselineProgress={baselineProgress}
regulationComparison={regulationComparison}
recommendedProtocolKind={recommendRegulationProtocol(
currentMeasurement.potentialStress.index,
)}
selfReportSaved={selfReportState === 'saved'}
selfReportSaving={selfReportState === 'saving'}
selfReportError={selfReportState === 'failed'}
onSaveSelfReport={saveSelfReport}
onStartRegulation={startRegulationJourney}
onRestart={restartMeasurement}
onDone={backToPreparation}
/>
);
}
if (
state === 'invalid' ||
state === 'thermalStop' ||
state === 'interrupted' ||
state === 'error'
) {
return (
<PPGFailureScreen
state={state}
message={
measurement.snapshot.context.failure?.message ??
'Le signal n’a pas permis de produire un résultat fiable.'
}
result={measurement.snapshot.context.result}
showOpenSettings={
measurement.snapshot.context.failure?.code ===
'cameraPermissionDenied'
}
onRetry={retryMeasurement}
onBack={backToPreparation}
/>
);
}
return (
<PPGPreparationScreen
includeHealthKit={includeHealthKit}
baselineProgress={baselineProgress}
postExerciseProtocolTitle={
pendingRegulationSession === null
? null
: getRegulationProtocol(pendingRegulationSession.protocolKind).title
}
onIncludeHealthKitChange={handleHealthKitPreference}
sensitiveContact={sensitiveContact}
onSensitiveContactChange={handleSensitiveContactPreference}
cameraPosition={cameraPosition}
onCameraPositionChange={handleCameraPositionPreference}
atRest={atRestSelection}
onAtRestChange={setAtRestSelection}
onStart={startMeasurement}
onOpenHistory={() => 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;