@tracked/health
Version:
Health module
176 lines (155 loc) • 5.82 kB
text/typescript
import { useCallback, useEffect, useState } from "react";
import { Platform } from "react-native";
// Import the health module with its types
import { Health, SleepSession } from "../..";
export enum AuthStatus {
Unknown = "UNKNOWN",
Authorized = "AUTHORIZED",
NotAuthorized = "NOT_AUTHORIZED",
}
export function useSleep(date: string) {
const [sleepSessions, setSleepSessions] = useState<SleepSession[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isInitialized, setIsInitialized] = useState(false);
const [authStatus, setAuthStatus] = useState<AuthStatus>(AuthStatus.Unknown);
// Platform-specific health service name
const healthServiceName = Platform.select({
ios: "HealthKit",
android: "Health Connect",
default: "Health Service",
});
// Prepare date range for the query
const getDateRange = useCallback((dateString: string) => {
const localDate = new Date(dateString + "T00:00:00.000");
const startTime = new Date(localDate);
startTime.setHours(0, 0, 0, 0);
const endTime = new Date(localDate);
endTime.setHours(23, 59, 59, 999);
return { startTime: startTime.getTime(), endTime: endTime.getTime() };
}, []);
// Fetch sleep sessions using our custom Health module
const fetchSleepSessions = useCallback(async () => {
try {
if (!Health.isHealthDataAvailable) {
setError(`${healthServiceName} is not available on this device`);
return;
}
setLoading(true);
const { startTime, endTime } = getDateRange(date);
const sessions = await Health.getSleepSessions(startTime, endTime);
setSleepSessions(sessions);
setError(null);
} catch (err) {
console.error(`${healthServiceName} error:`, err);
setError("Error getting sleep data");
} finally {
setLoading(false);
}
}, [date, getDateRange, healthServiceName]);
// Fetch sleep when date changes or when initialized
useEffect(() => {
if (isInitialized && authStatus === AuthStatus.Authorized) {
fetchSleepSessions();
}
}, [date, isInitialized, authStatus, fetchSleepSessions]);
// Set up background delivery and event listeners
useEffect(() => {
const subscription = Health.addListener("onSleepDataUpdate", (event: SleepSession) => {
// Check if this sleep session is for the current date being viewed.
// Bucket by the session's LOCAL calendar date to match the local-midnight
// bounds getDateRange uses — not the UTC date from toISOString(), which
// would drop evening sessions in negative-offset zones as "tomorrow".
const start = new Date(event.startTime);
const sessionDate = `${start.getFullYear()}-${String(start.getMonth() + 1).padStart(2, "0")}-${String(start.getDate()).padStart(2, "0")}`;
if (date === sessionDate) {
// Update or add the sleep session
setSleepSessions((prevSessions) => {
const existingIndex = prevSessions.findIndex(
(s) => s.startTime === event.startTime && s.endTime === event.endTime
);
if (existingIndex >= 0) {
// Update existing session
const newSessions = [...prevSessions];
newSessions[existingIndex] = event;
return newSessions;
} else {
// Add new session
return [...prevSessions, event].sort((a, b) => a.startTime - b.startTime);
}
});
}
});
// Enable background delivery if initialized and not already enabled
const setupBackgroundDelivery = async () => {
if (isInitialized && authStatus === AuthStatus.Authorized) {
try {
await Health.enableSleepUpdates("daily");
} catch (err) {
// noop - background delivery might already be enabled
}
}
};
setupBackgroundDelivery();
// Clean up event listener when component unmounts
return () => {
subscription.remove();
// Don't disable background delivery on unmount - it should remain active
// so the app can receive updates even when not in foreground
};
}, [isInitialized, authStatus, date]);
const requestInitialization = async () => {
if (!Health.isHealthDataAvailable) {
setError(`${healthServiceName} is not available on this device`);
return false;
}
setLoading(true);
try {
// Request authorization using our custom module
const authorized = await Health.requestAuthorization();
if (authorized) {
setIsInitialized(true);
setAuthStatus(AuthStatus.Authorized);
await fetchSleepSessions();
return true;
} else {
setError("Health data access was denied");
setAuthStatus(AuthStatus.NotAuthorized);
return false;
}
} catch (err) {
setError(`Cannot access ${healthServiceName}`);
setIsInitialized(false);
setAuthStatus(AuthStatus.NotAuthorized);
console.error(err);
return false;
} finally {
setLoading(false);
}
};
// Calculate total sleep duration from all sessions
const totalSleepDuration = sleepSessions.reduce(
(total, session) => total + session.totalDuration,
0
);
// Calculate sleep stage durations
const sleepStageBreakdown = sleepSessions.reduce((breakdown, session) => {
session.stages.forEach((stage) => {
if (!breakdown[stage.type]) {
breakdown[stage.type] = 0;
}
breakdown[stage.type] += stage.duration;
});
return breakdown;
}, {} as Record<string, number>);
return {
sleepSessions,
totalSleepDuration,
sleepStageBreakdown,
loading,
error,
isInitialized,
requestInitialization,
refetch: fetchSleepSessions,
};
}