@tracked/health
Version:
Health module
130 lines (112 loc) • 4.09 kB
text/typescript
import { useCallback, useEffect, useRef, useState } from "react";
import { Platform } from "react-native";
// Import the health module with its type
import { Health, StepUpdateEvent } from "../..";
import { AuthStatus } from "../../types";
import { getDayBoundsMs, getTodayInUserTimezone } from "./date-utils";
export { AuthStatus };
export function useSteps(date: string) {
const [steps, setSteps] = useState<number>(0);
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",
});
// Fetch steps using our custom Health module
const fetchSteps = useCallback(async () => {
try {
if (!Health.isHealthDataAvailable) {
setError(`${healthServiceName} is not available on this device`);
return;
}
setLoading(true);
const { startTime, endTime } = getDayBoundsMs(date);
const totalSteps = await Health.getStepCount(startTime, endTime);
setSteps(totalSteps);
setError(null);
} catch (err) {
console.error(`${healthServiceName} error:`, err);
setError("Error getting steps");
} finally {
setLoading(false);
}
}, [date, healthServiceName]);
// Fetch steps when date changes or when initialized
useEffect(() => {
if (isInitialized && authStatus === AuthStatus.Authorized) {
fetchSteps();
}
}, [date, isInitialized, authStatus, fetchSteps]);
const backgroundDeliveryEnabled = useRef(false);
// Set up background delivery and event listeners
useEffect(() => {
const subscription = Health.addListener("onStepDataUpdate", (event: StepUpdateEvent) => {
// Only update steps if we're looking at today's date. Compare against the
// user's local date (not the UTC date) so evening updates in negative-UTC
// timezones aren't dropped by an off-by-one day mismatch.
const today = getTodayInUserTimezone();
if (date === today && event.steps !== undefined) {
setSteps(event.steps);
}
});
// Enable background delivery once
const setupBackgroundDelivery = async () => {
if (isInitialized && authStatus === AuthStatus.Authorized && !backgroundDeliveryEnabled.current) {
try {
await Health.enableBackgroundDelivery("immediate");
backgroundDeliveryEnabled.current = true;
} 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 fetchSteps();
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);
}
};
return {
steps: Math.round(steps),
loading,
error,
isInitialized,
requestInitialization,
};
}