diff --git a/apps/expo/src/app/(app)/(tabs)/account.tsx b/apps/expo/src/app/(app)/(tabs)/account.tsx index cb8d164..3708e24 100644 --- a/apps/expo/src/app/(app)/(tabs)/account.tsx +++ b/apps/expo/src/app/(app)/(tabs)/account.tsx @@ -8,6 +8,7 @@ import { import DexcomCGMData from "~/components/dexcom/dexcom-data"; import DexcomDevicesList from "~/components/dexcom/dexcom-devices"; import { DexcomLogin } from "~/components/dexcom/dexcom-login"; +import { DexcomBackgroundSync } from "~/components/dexcom/dexcom-sync"; import { ChangeRangeSetting } from "~/components/glucose/change-range-setting"; import { CalculateRecap } from "~/components/glucose/create-recap"; import { ThemeToggle } from "~/components/theme-toggle"; @@ -51,6 +52,8 @@ export default function AccountScreen() { + + ); diff --git a/apps/expo/src/components/dexcom/dexcom-sync.tsx b/apps/expo/src/components/dexcom/dexcom-sync.tsx new file mode 100644 index 0000000..5afa7df --- /dev/null +++ b/apps/expo/src/components/dexcom/dexcom-sync.tsx @@ -0,0 +1,79 @@ +import { View } from "react-native"; +import * as BackgroundFetch from "expo-background-fetch"; +import * as Notifications from "expo-notifications"; +import * as TaskManager from "expo-task-manager"; + +import { Button } from "~/components/ui/button"; +import { Text } from "~/components/ui/text"; +import { useDexcomSync } from "~/hooks/use-dexcom-sync"; +import { BACKGROUND_FETCH_DEXCOM_SYNC } from "~/lib/constants"; +import { useGlucoseStore } from "~/stores/glucose-store"; + +const BACKGROUND_FETCH_TASK = "background-fetch"; + +// 1. Define the task by providing a name and the function that should be executed +// Note: This needs to be called in the global scope (e.g outside of your React components) +TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => { + const { lastSyncedTime } = useGlucoseStore(); + + try { + const result = await performSync(lastSyncedTime); + + if (result.newData) { + await Notifications.scheduleNotificationAsync({ + content: { + title: "New CGM Data Available", + body: "Tap to view your latest glucose readings.", + }, + trigger: null, + }); + return BackgroundFetch.BackgroundFetchResult.NewData; + } else { + return BackgroundFetch.BackgroundFetchResult.NoData; + } + } catch (error) { + console.error("Background sync failed:", error); + return BackgroundFetch.BackgroundFetchResult.Failed; + } +}); + +export function DexcomBackgroundSync() { + const { status, syncNow, isPending, isRegistered, toggleFetchTask } = + useDexcomSync(); + + return ( + + + + Background fetch status:{" "} + + {status !== null + ? BackgroundFetch.BackgroundFetchStatus[status] + : "Unknown"} + + + + Background fetch task name:{" "} + + {isRegistered + ? BACKGROUND_FETCH_DEXCOM_SYNC + : "Not registered yet!"} + + + + + + + + + + ); +} diff --git a/apps/expo/src/components/home/blood-sugar-widget.tsx b/apps/expo/src/components/home/blood-sugar-widget.tsx index 2b2ee31..adf13da 100644 --- a/apps/expo/src/components/home/blood-sugar-widget.tsx +++ b/apps/expo/src/components/home/blood-sugar-widget.tsx @@ -26,9 +26,6 @@ export default function BloodSugarWidget() { const bloodSugarColors = getBloodSugarColors(bloodSugar, isDark, rangeView); - console.log("Raw system time:", latestEgv?.egv?.systemTime); - console.log("Parsed system time:", localTime.toISO()); - return ( {/* Blood Sugar */} diff --git a/apps/expo/src/hooks/use-dexcom-sync.ts b/apps/expo/src/hooks/use-dexcom-sync.ts new file mode 100644 index 0000000..05f4539 --- /dev/null +++ b/apps/expo/src/hooks/use-dexcom-sync.ts @@ -0,0 +1,110 @@ +import { useCallback, useEffect, useState } from "react"; +import * as BackgroundFetch from "expo-background-fetch"; +import * as TaskManager from "expo-task-manager"; +import { DateTime } from "luxon"; + +import { BACKGROUND_FETCH_DEXCOM_SYNC } from "~/lib/constants"; +import { useGlucoseStore } from "~/stores/glucose-store"; +import { api } from "~/utils/api"; + +// 2. Register the task at some point in your app by providing the same name, +// and some configuration options for how the background fetch should behave +// Note: This does NOT need to be in the global scope and CAN be used in your React components! +export async function registerBackgroundFetchAsync() { + try { + await BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_DEXCOM_SYNC, { + minimumInterval: 15 * 60, // 15 minutes + stopOnTerminate: false, // android only + startOnBoot: true, // android only + }); + await BackgroundFetch.setMinimumIntervalAsync(15 * 60); // 15 minutes, iOS minimum + console.log("Background fetch registered"); + } catch (err) { + console.log("Background fetch failed to register", err); + } +} + +// 3. (Optional) Unregister tasks by specifying the task name +// This will cancel any future background fetch calls that match the given name +// Note: This does NOT need to be in the global scope and CAN be used in your React components! +export async function unregisterBackgroundFetchAsync() { + return BackgroundFetch.unregisterTaskAsync(BACKGROUND_FETCH_DEXCOM_SYNC); +} + +export function useDexcomSync() { + const { lastSyncedTime, setLastSyncedTime } = useGlucoseStore(); + const [status, setStatus] = + useState(null); + const [isRegistered, setIsRegistered] = useState(false); + + const fetchDataRangeQuery = api.dexcom.fetchDataRange.useQuery({ + lastSyncTime: lastSyncedTime ?? undefined, + }); + const fetchAndStoreEGVsMutation = api.dexcom.fetchAndStoreEGVs.useMutation(); + + useEffect(() => { + void checkStatusAsync(); + }, []); + + const checkStatusAsync = async () => { + const fetchStatus = await BackgroundFetch.getStatusAsync(); + const isTaskRegistered = await TaskManager.isTaskRegisteredAsync( + BACKGROUND_FETCH_DEXCOM_SYNC, + ); + setStatus(fetchStatus); + setIsRegistered(isTaskRegistered); + }; + + const toggleFetchTask = async () => { + if (isRegistered) { + await unregisterBackgroundFetchAsync(); + } else { + await registerBackgroundFetchAsync(); + } + await checkStatusAsync(); + }; + + const syncNow = useCallback(async () => { + try { + if (fetchDataRangeQuery.data?.egvs) { + const startDate = DateTime.fromISO( + fetchDataRangeQuery.data.egvs.start.systemTime, + { zone: "utc" }, + ); + const endDate = DateTime.fromISO( + fetchDataRangeQuery.data.egvs.end.systemTime, + { zone: "utc" }, + ); + + const result = await fetchAndStoreEGVsMutation.mutateAsync({ + startDate: startDate.toISO() ?? "", + endDate: endDate.toISO() ?? "", + }); + + if (result.recordsInserted > 0 && result.latestEGVTimestamp) { + setLastSyncedTime( + DateTime.fromISO(result.latestEGVTimestamp, { zone: "utc" }), + ); + console.log("New data synced"); + } else { + console.log("No new data available"); + } + } + } catch (error) { + console.error("Sync failed", error); + } + }, [ + fetchDataRangeQuery.data?.egvs, + fetchAndStoreEGVsMutation, + setLastSyncedTime, + ]); + + return { + syncNow, + status, + isRegistered, + toggleFetchTask, + isPending: + fetchDataRangeQuery.isPending || fetchAndStoreEGVsMutation.isPending, + }; +} diff --git a/apps/expo/src/lib/constants.ts b/apps/expo/src/lib/constants.ts index 2ff4442..604bef0 100644 --- a/apps/expo/src/lib/constants.ts +++ b/apps/expo/src/lib/constants.ts @@ -110,3 +110,5 @@ export const INTRO_CONTENT = [ fontColor: colors.pink[500], }, ]; + +export const BACKGROUND_FETCH_DEXCOM_SYNC = "background-fetch"; diff --git a/packages/api/src/router/dexcom.ts b/packages/api/src/router/dexcom.ts index 3cba97f..c32e0fd 100644 --- a/packages/api/src/router/dexcom.ts +++ b/packages/api/src/router/dexcom.ts @@ -1,6 +1,5 @@ import type { TRPCRouterRecord } from "@trpc/server"; import { TRPCError } from "@trpc/server"; -import { parseISO } from "date-fns"; import { DateTime } from "luxon"; import { z } from "zod";