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";