diff --git a/homeflow/app.config.js b/homeflow/app.config.js
index 85fe042..76ebe09 100644
--- a/homeflow/app.config.js
+++ b/homeflow/app.config.js
@@ -66,6 +66,13 @@ module.exports = {
"usageDescription": "StreamSync would like to access your clinical health records to import medications, lab results, and conditions — reducing manual data entry."
}
],
+ [
+ "expo-notifications",
+ {
+ "icon": "./assets/images/icon.png",
+ "color": "#8C1515"
+ }
+ ],
"expo-apple-authentication",
[
"@react-native-google-signin/google-signin",
diff --git a/homeflow/app/(onboarding)/permissions.tsx b/homeflow/app/(onboarding)/permissions.tsx
index b75b640..5b6c5ee 100644
--- a/homeflow/app/(onboarding)/permissions.tsx
+++ b/homeflow/app/(onboarding)/permissions.tsx
@@ -27,6 +27,7 @@ import {
areClinicalRecordsAvailable,
requestClinicalPermissions,
} from '@/lib/services/healthkit';
+import { requestNotificationPermissions } from '@/lib/services/notification-service';
import {
OnboardingProgressBar,
PermissionCard,
@@ -136,6 +137,9 @@ export default function PermissionsScreen() {
const handleContinue = async () => {
setIsLoading(true);
try {
+ // Request notification permissions alongside health permissions
+ await requestNotificationPermissions();
+
await OnboardingService.updateData({
permissions: {
healthKit: healthKitStatus as 'granted' | 'denied' | 'not_determined',
diff --git a/homeflow/app/(tabs)/profile.tsx b/homeflow/app/(tabs)/profile.tsx
index 9462d20..6002c09 100644
--- a/homeflow/app/(tabs)/profile.tsx
+++ b/homeflow/app/(tabs)/profile.tsx
@@ -14,6 +14,7 @@ import { useRouter, Href } from 'expo-router';
import { SafeAreaView } from 'react-native-safe-area-context';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { useAuth } from '@/hooks/use-auth';
+import { triggerTestNotification, requestNotificationPermissions } from '@/lib/services/notification-service';
import {
CONSENT_PROFILE_SUMMARY,
DATA_PERMISSIONS_SUMMARY,
@@ -230,6 +231,56 @@ export default function ProfileScreen() {
+
+
+
+ {
+ try {
+ const granted = await requestNotificationPermissions();
+ if (!granted) {
+ Alert.alert('Permission Denied', 'Enable notifications in Settings → HomeFlow → Notifications.');
+ return;
+ }
+ await triggerTestNotification('healthkit');
+ Alert.alert('Sent', 'HealthKit reminder notification fired. Background the app to see the banner.');
+ } catch (e: any) {
+ Alert.alert('Error', e?.message ?? 'Failed to send notification.');
+ }
+ }}
+ activeOpacity={0.7}
+ >
+
+
+ Test HealthKit Reminder
+
+
+
+
+
+ {
+ try {
+ const granted = await requestNotificationPermissions();
+ if (!granted) {
+ Alert.alert('Permission Denied', 'Enable notifications in Settings → HomeFlow → Notifications.');
+ return;
+ }
+ await triggerTestNotification('throne');
+ Alert.alert('Sent', 'Throne reminder notification fired. Background the app to see the banner.');
+ } catch (e: any) {
+ Alert.alert('Error', e?.message ?? 'Failed to send notification.');
+ }
+ }}
+ activeOpacity={0.7}
+ >
+
+
+ Test Throne Reminder
+
+
)}
@@ -422,6 +473,20 @@ const styles = StyleSheet.create({
fontWeight: '500',
},
+ // Dev tools
+ devButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 10,
+ borderRadius: 10,
+ paddingHorizontal: 14,
+ paddingVertical: 12,
+ },
+ devButtonText: {
+ fontSize: 14,
+ fontWeight: '500',
+ },
+
// Sign out
signOutButton: {
flexDirection: 'row',
diff --git a/homeflow/app/_layout.tsx b/homeflow/app/_layout.tsx
index fd9917b..21b9bd1 100644
--- a/homeflow/app/_layout.tsx
+++ b/homeflow/app/_layout.tsx
@@ -12,6 +12,7 @@ import { bootstrapHealthKitSync } from '@/src/services/healthkitSync';
import { useOnboardingStatus } from '@/hooks/use-onboarding-status';
import { useAuth } from '@/hooks/use-auth';
+import { useDataSyncCheck } from '@/hooks/use-data-sync-check';
import { LoadingScreen } from '@/components/ui/loading-screen';
import { ErrorBoundary } from '@/components/error-boundary';
import { StandardProvider, useStandard } from '@/lib/services/standard-context';
@@ -55,6 +56,9 @@ function RootLayoutNav() {
);
}, [user?.id]);
+ // Run 48-hour data sync check only when user is fully in the app
+ useDataSyncCheck(!!onboardingComplete && isAuthenticated);
+
// While checking onboarding/auth status, show loading
if (onboardingComplete === null || authLoading) {
return ;
diff --git a/homeflow/hooks/use-data-sync-check.ts b/homeflow/hooks/use-data-sync-check.ts
new file mode 100644
index 0000000..0bbb011
--- /dev/null
+++ b/homeflow/hooks/use-data-sync-check.ts
@@ -0,0 +1,31 @@
+/**
+ * useDataSyncCheck
+ *
+ * Runs the 48-hour data sync check whenever the app comes to the foreground.
+ * Only active when the user is authenticated and onboarding is complete.
+ */
+
+import { useEffect, useRef } from 'react';
+import { AppState, type AppStateStatus } from 'react-native';
+import { checkAndScheduleReminders } from '@/lib/services/notification-service';
+
+export function useDataSyncCheck(active: boolean): void {
+ const appState = useRef(AppState.currentState);
+
+ useEffect(() => {
+ if (!active) return;
+
+ // Run on mount
+ checkAndScheduleReminders().catch(() => {});
+
+ // Run every time the app comes back to the foreground
+ const subscription = AppState.addEventListener('change', (nextState) => {
+ if (appState.current.match(/inactive|background/) && nextState === 'active') {
+ checkAndScheduleReminders().catch(() => {});
+ }
+ appState.current = nextState;
+ });
+
+ return () => subscription.remove();
+ }, [active]);
+}
diff --git a/homeflow/ios/HomeFlow.xcodeproj/project.pbxproj b/homeflow/ios/HomeFlow.xcodeproj/project.pbxproj
index 352c807..5d7ad2f 100644
--- a/homeflow/ios/HomeFlow.xcodeproj/project.pbxproj
+++ b/homeflow/ios/HomeFlow.xcodeproj/project.pbxproj
@@ -8,11 +8,11 @@
/* Begin PBXBuildFile section */
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
- 1DAB361E531732CB0B04D49B /* libPods-HomeFlow.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 24A7EF4276D45AC52EF6B7A6 /* libPods-HomeFlow.a */; };
+ 2520E33509CBB6E7BFBB83F6 /* libPods-HomeFlow.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F0B05807DAA7EC15E2DE6ED5 /* libPods-HomeFlow.a */; };
+ 31A024BC08B6A796A2A7C30E /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 8FDD2D2B4AF190C01933B668 /* PrivacyInfo.xcprivacy */; };
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
- 4C3912353B4D298C7B8021D2 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 82D567622218F40090F50B48 /* PrivacyInfo.xcprivacy */; };
- 57725D3E768F6B0B3A9CEF0F /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326D018C2EF7167CDBB461A5 /* ExpoModulesProvider.swift */; };
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
+ CB21B2499DBD4ED7C0A90315 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 932A01F59D95FB87CA82478D /* ExpoModulesProvider.swift */; };
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11748412D0307B40044C1D9 /* AppDelegate.swift */; };
/* End PBXBuildFile section */
@@ -27,7 +27,9 @@
82D567622218F40090F50B48 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = HomeFlow/PrivacyInfo.xcprivacy; sourceTree = ""; };
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = HomeFlow/SplashScreen.storyboard; sourceTree = ""; };
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = ""; };
+ E293299E035AA5F8170074DA /* Pods-HomeFlow.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-HomeFlow.debug.xcconfig"; path = "Target Support Files/Pods-HomeFlow/Pods-HomeFlow.debug.xcconfig"; sourceTree = ""; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
+ F0B05807DAA7EC15E2DE6ED5 /* libPods-HomeFlow.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-HomeFlow.a"; sourceTree = BUILT_PRODUCTS_DIR; };
F11748412D0307B40044C1D9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = HomeFlow/AppDelegate.swift; sourceTree = ""; };
F11748442D0722820044C1D9 /* HomeFlow-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "HomeFlow-Bridging-Header.h"; path = "HomeFlow/HomeFlow-Bridging-Header.h"; sourceTree = ""; };
/* End PBXFileReference section */
@@ -37,13 +39,21 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- 1DAB361E531732CB0B04D49B /* libPods-HomeFlow.a in Frameworks */,
+ 2520E33509CBB6E7BFBB83F6 /* libPods-HomeFlow.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
+ 06AF249EECC0DC6BD1080856 /* ExpoModulesProviders */ = {
+ isa = PBXGroup;
+ children = (
+ 49B95061D3108EDF0F3CB595 /* HomeFlow */,
+ );
+ name = ExpoModulesProviders;
+ sourceTree = "";
+ };
13B07FAE1A68108700A75B9A /* HomeFlow */ = {
isa = PBXGroup;
children = (
@@ -53,7 +63,7 @@
13B07FB51A68108700A75B9A /* Images.xcassets */,
13B07FB61A68108700A75B9A /* Info.plist */,
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
- 82D567622218F40090F50B48 /* PrivacyInfo.xcprivacy */,
+ 8FDD2D2B4AF190C01933B668 /* PrivacyInfo.xcprivacy */,
);
name = HomeFlow;
sourceTree = "";
@@ -62,11 +72,28 @@
isa = PBXGroup;
children = (
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
- 24A7EF4276D45AC52EF6B7A6 /* libPods-HomeFlow.a */,
+ F0B05807DAA7EC15E2DE6ED5 /* libPods-HomeFlow.a */,
);
name = Frameworks;
sourceTree = "";
};
+ 49B95061D3108EDF0F3CB595 /* HomeFlow */ = {
+ isa = PBXGroup;
+ children = (
+ 932A01F59D95FB87CA82478D /* ExpoModulesProvider.swift */,
+ );
+ name = HomeFlow;
+ sourceTree = "";
+ };
+ 5D90005D0F3213D19DF0DB41 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ E293299E035AA5F8170074DA /* Pods-HomeFlow.debug.xcconfig */,
+ 46D8236488368F4AA1119996 /* Pods-HomeFlow.release.xcconfig */,
+ );
+ path = Pods;
+ sourceTree = "";
+ };
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
isa = PBXGroup;
children = (
@@ -81,8 +108,8 @@
832341AE1AAA6A7D00B99B32 /* Libraries */,
83CBBA001A601CBA00E9B192 /* Products */,
2D16E6871FA4F8E400B85C8A /* Frameworks */,
- C470204D92E850BA354AB466 /* Pods */,
- F97F0906BAF7BB4193F9BA60 /* ExpoModulesProviders */,
+ 5D90005D0F3213D19DF0DB41 /* Pods */,
+ 06AF249EECC0DC6BD1080856 /* ExpoModulesProviders */,
);
indentWidth = 2;
sourceTree = "";
@@ -139,13 +166,13 @@
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "HomeFlow" */;
buildPhases = (
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */,
- C609B1F205B83D107274398B /* [Expo] Configure project */,
+ F24774EFA1BE2BCDB4F06351 /* [Expo] Configure project */,
13B07F871A680F5B00A75B9A /* Sources */,
13B07F8C1A680F5B00A75B9A /* Frameworks */,
13B07F8E1A680F5B00A75B9A /* Resources */,
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */,
- 3296883504C3D6E5DA57CFC3 /* [CP] Embed Pods Frameworks */,
+ FDFF33297BF3D162CC41091F /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
@@ -197,7 +224,7 @@
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */,
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
- 4C3912353B4D298C7B8021D2 /* PrivacyInfo.xcprivacy in Resources */,
+ 31A024BC08B6A796A2A7C30E /* PrivacyInfo.xcprivacy in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -243,28 +270,6 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
- 3296883504C3D6E5DA57CFC3 /* [CP] Embed Pods Frameworks */ = {
- isa = PBXShellScriptBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- inputPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-HomeFlow/Pods-HomeFlow-frameworks.sh",
- "${PODS_XCFRAMEWORKS_BUILD_DIR}/React-Core-prebuilt/React.framework/React",
- "${PODS_XCFRAMEWORKS_BUILD_DIR}/ReactNativeDependencies/ReactNativeDependencies.framework/ReactNativeDependencies",
- "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
- );
- name = "[CP] Embed Pods Frameworks";
- outputPaths = (
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactNativeDependencies.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-HomeFlow/Pods-HomeFlow-frameworks.sh\"\n";
- showEnvVarsInLog = 0;
- };
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -273,8 +278,10 @@
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-HomeFlow/Pods-HomeFlow-resources.sh",
"${PODS_CONFIGURATION_BUILD_DIR}/AppAuth/AppAuthCore_Privacy.bundle",
+ "${PODS_CONFIGURATION_BUILD_DIR}/EXApplication/ExpoApplication_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
+ "${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/GTMAppAuth/GTMAppAuth_Privacy.bundle",
@@ -290,8 +297,10 @@
name = "[CP] Copy Pods Resources";
outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AppAuthCore_Privacy.bundle",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoApplication_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GTMAppAuth_Privacy.bundle",
@@ -309,7 +318,7 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-HomeFlow/Pods-HomeFlow-resources.sh\"\n";
showEnvVarsInLog = 0;
};
- C609B1F205B83D107274398B /* [Expo] Configure project */ = {
+ F24774EFA1BE2BCDB4F06351 /* [Expo] Configure project */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
@@ -333,6 +342,28 @@
shellPath = /bin/sh;
shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-HomeFlow/expo-configure-project.sh\"\n";
};
+ FDFF33297BF3D162CC41091F /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-HomeFlow/Pods-HomeFlow-frameworks.sh",
+ "${PODS_XCFRAMEWORKS_BUILD_DIR}/React-Core-prebuilt/React.framework/React",
+ "${PODS_XCFRAMEWORKS_BUILD_DIR}/ReactNativeDependencies/ReactNativeDependencies.framework/ReactNativeDependencies",
+ "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactNativeDependencies.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-HomeFlow/Pods-HomeFlow-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -341,7 +372,7 @@
buildActionMask = 2147483647;
files = (
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */,
- 57725D3E768F6B0B3A9CEF0F /* ExpoModulesProvider.swift in Sources */,
+ CB21B2499DBD4ED7C0A90315 /* ExpoModulesProvider.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -350,7 +381,7 @@
/* Begin XCBuildConfiguration section */
13B07F941A680F5B00A75B9A /* Debug */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = 5D614E4466675C6ABC12F82F /* Pods-HomeFlow.debug.xcconfig */;
+ baseConfigurationReference = E293299E035AA5F8170074DA /* Pods-HomeFlow.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
@@ -358,6 +389,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = "";
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
@@ -389,7 +421,7 @@
};
13B07F951A680F5B00A75B9A /* Release */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = 42CC32645DAAB3607522405A /* Pods-HomeFlow.release.xcconfig */;
+ baseConfigurationReference = 46D8236488368F4AA1119996 /* Pods-HomeFlow.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
@@ -397,6 +429,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = HomeFlow/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
diff --git a/homeflow/ios/HomeFlow/HomeFlow.entitlements b/homeflow/ios/HomeFlow/HomeFlow.entitlements
index dca2b85..e109c16 100644
--- a/homeflow/ios/HomeFlow/HomeFlow.entitlements
+++ b/homeflow/ios/HomeFlow/HomeFlow.entitlements
@@ -2,6 +2,12 @@
+ aps-environment
+ development
+ com.apple.developer.applesignin
+
+ Default
+
com.apple.developer.healthkit
com.apple.developer.healthkit.access
diff --git a/homeflow/ios/HomeFlow/PrivacyInfo.xcprivacy b/homeflow/ios/HomeFlow/PrivacyInfo.xcprivacy
index 22c59a7..05d1a14 100644
--- a/homeflow/ios/HomeFlow/PrivacyInfo.xcprivacy
+++ b/homeflow/ios/HomeFlow/PrivacyInfo.xcprivacy
@@ -6,21 +6,21 @@
NSPrivacyAccessedAPIType
- NSPrivacyAccessedAPICategoryUserDefaults
+ NSPrivacyAccessedAPICategoryFileTimestamp
NSPrivacyAccessedAPITypeReasons
- CA92.1
- C56D.1
+ C617.1
+ 0A2A.1
+ 3B52.1
NSPrivacyAccessedAPIType
- NSPrivacyAccessedAPICategoryFileTimestamp
+ NSPrivacyAccessedAPICategoryUserDefaults
NSPrivacyAccessedAPITypeReasons
- 0A2A.1
- 3B52.1
- C617.1
+ CA92.1
+ C56D.1
diff --git a/homeflow/ios/Podfile.lock b/homeflow/ios/Podfile.lock
index bc36639..e4f138f 100644
--- a/homeflow/ios/Podfile.lock
+++ b/homeflow/ios/Podfile.lock
@@ -11,8 +11,12 @@ PODS:
- PromisesObjC (~> 2.4)
- ClinicalRecordsModule (0.1.0):
- ExpoModulesCore
+ - EXApplication (7.0.8):
+ - ExpoModulesCore
- EXConstants (18.0.13):
- ExpoModulesCore
+ - EXNotifications (0.32.16):
+ - ExpoModulesCore
- Expo (54.0.32):
- ExpoModulesCore
- hermes-engine
@@ -2158,7 +2162,9 @@ PODS:
DEPENDENCIES:
- ClinicalRecordsModule (from `../modules/expo-clinical-records/ios`)
+ - EXApplication (from `../node_modules/expo-application/ios`)
- EXConstants (from `../node_modules/expo-constants/ios`)
+ - EXNotifications (from `../node_modules/expo-notifications/ios`)
- Expo (from `../node_modules/expo`)
- "ExpoAdapterGoogleSignIn (from `../node_modules/@react-native-google-signin/google-signin/expo/ios`)"
- ExpoAppleAuthentication (from `../node_modules/expo-apple-authentication/ios`)
@@ -2274,8 +2280,12 @@ SPEC REPOS:
EXTERNAL SOURCES:
ClinicalRecordsModule:
:path: "../modules/expo-clinical-records/ios"
+ EXApplication:
+ :path: "../node_modules/expo-application/ios"
EXConstants:
:path: "../node_modules/expo-constants/ios"
+ EXNotifications:
+ :path: "../node_modules/expo-notifications/ios"
Expo:
:path: "../node_modules/expo"
ExpoAdapterGoogleSignIn:
@@ -2468,7 +2478,9 @@ SPEC CHECKSUMS:
AppAuth: 1c1a8afa7e12f2ec3a294d9882dfa5ab7d3cb063
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
ClinicalRecordsModule: ca5a9ab487199aa4fe864e4fa00f4cc497f9b6ff
+ EXApplication: 1e98d4b1dccdf30627f92917f4b2c5a53c330e5f
EXConstants: fce59a631a06c4151602843667f7cfe35f81e271
+ EXNotifications: 9eec98712cc814ceff916d876cb53859003b0597
Expo: 4e503a041c59c4e34c8be262a135848ad5cd3710
ExpoAdapterGoogleSignIn: 21ff1daeef3d21cb0b45772d8029102cc728d6b1
ExpoAppleAuthentication: 9413ae9a5e631a424cc04c1b13b0694dbab7d594
diff --git a/homeflow/lib/constants.ts b/homeflow/lib/constants.ts
index f70c893..aaf3ce8 100644
--- a/homeflow/lib/constants.ts
+++ b/homeflow/lib/constants.ts
@@ -30,6 +30,10 @@ export const STORAGE_KEYS = {
// Permissions
PERMISSIONS_STATUS: '@homeflow_permissions_status',
+
+ // Notification tracking (last time we fired each reminder, to avoid spam)
+ LAST_NOTIFICATION_HEALTHKIT: '@homeflow_last_notification_healthkit',
+ LAST_NOTIFICATION_THRONE: '@homeflow_last_notification_throne',
} as const;
// Legacy keys for backwards compatibility
diff --git a/homeflow/lib/services/notification-service.ts b/homeflow/lib/services/notification-service.ts
new file mode 100644
index 0000000..318ca33
--- /dev/null
+++ b/homeflow/lib/services/notification-service.ts
@@ -0,0 +1,189 @@
+/**
+ * Notification Service
+ *
+ * Manages local push notifications that remind users to sync their
+ * Apple Watch and Throne device if no new data is detected in 48 hours.
+ */
+
+import * as Notifications from 'expo-notifications';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import { Platform } from 'react-native';
+import { STORAGE_KEYS } from '../constants';
+import { getDailyActivity } from './healthkit/HealthKitClient';
+import { ThroneService } from './throne-service';
+
+const HOURS_48 = 48 * 60 * 60 * 1000;
+const HOURS_24 = 24 * 60 * 60 * 1000;
+
+const NOTIFICATION_IDS = {
+ healthkit: 'homeflow-healthkit-reminder',
+ throne: 'homeflow-throne-reminder',
+} as const;
+
+type DataSource = keyof typeof NOTIFICATION_IDS;
+
+// Configure how notifications are presented when the app is foregrounded
+Notifications.setNotificationHandler({
+ handleNotification: async () => ({
+ shouldPlaySound: false,
+ shouldSetBadge: false,
+ shouldShowBanner: true,
+ shouldShowList: true,
+ }),
+});
+
+const NOTIFICATION_CONTENT: Record = {
+ healthkit: {
+ title: 'Apple Watch not syncing',
+ body: "It looks like you haven't worn your Apple Watch in the last 48 hours. Please put it on to continue tracking your health!",
+ },
+ throne: {
+ title: 'Throne device reminder',
+ body: "We haven't recorded a urinary flow reading in the past 48 hours. Use your Throne device to continue tracking your progress!",
+ },
+};
+
+/**
+ * Request notification permissions from the user.
+ * Returns true if granted.
+ */
+export async function requestNotificationPermissions(): Promise {
+ if (Platform.OS === 'web') return false;
+
+ const { status: existing } = await Notifications.getPermissionsAsync();
+ if (existing === 'granted') return true;
+
+ const { status } = await Notifications.requestPermissionsAsync();
+ return status === 'granted';
+}
+
+/**
+ * Cancel any pending notification for the given source and schedule
+ * a new one to fire after `delayMs` milliseconds.
+ */
+async function scheduleReminder(source: DataSource, delayMs: number): Promise {
+ await Notifications.cancelScheduledNotificationAsync(NOTIFICATION_IDS[source]).catch(() => {});
+
+ await Notifications.scheduleNotificationAsync({
+ identifier: NOTIFICATION_IDS[source],
+ content: {
+ title: NOTIFICATION_CONTENT[source].title,
+ body: NOTIFICATION_CONTENT[source].body,
+ },
+ trigger: {
+ type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL,
+ seconds: Math.round(delayMs / 1000),
+ },
+ });
+}
+
+/**
+ * Fire an immediate notification (shown right away as a banner).
+ * Throttled to once per 24h per source via AsyncStorage.
+ */
+async function fireImmediateReminder(source: DataSource): Promise {
+ const storageKey = source === 'healthkit'
+ ? STORAGE_KEYS.LAST_NOTIFICATION_HEALTHKIT
+ : STORAGE_KEYS.LAST_NOTIFICATION_THRONE;
+
+ const lastFiredStr = await AsyncStorage.getItem(storageKey);
+ if (lastFiredStr) {
+ const lastFired = parseInt(lastFiredStr, 10);
+ if (Date.now() - lastFired < HOURS_24) return; // already notified today
+ }
+
+ await Notifications.scheduleNotificationAsync({
+ identifier: `${NOTIFICATION_IDS[source]}-immediate`,
+ content: {
+ title: NOTIFICATION_CONTENT[source].title,
+ body: NOTIFICATION_CONTENT[source].body,
+ },
+ trigger: null, // fires immediately
+ });
+
+ await AsyncStorage.setItem(storageKey, Date.now().toString());
+}
+
+/**
+ * Check if HealthKit has data in the last 48 hours.
+ * Returns true if recent data exists.
+ */
+async function hasRecentHealthKitData(): Promise {
+ if (Platform.OS !== 'ios') return true; // non-iOS: don't nag
+
+ try {
+ const now = new Date();
+ const cutoff = new Date(now.getTime() - HOURS_48);
+
+ const activity = await getDailyActivity({ startDate: cutoff, endDate: now });
+
+ // Check if any day has non-zero steps or activity
+ const hasActivity = activity.some(
+ (day) => day.steps > 0 || day.activeEnergyBurned > 0 || day.exerciseMinutes > 0
+ );
+
+ return hasActivity;
+ } catch {
+ return true; // on error, assume data exists (don't spam user)
+ }
+}
+
+/**
+ * Check if Throne has data in the last 48 hours.
+ * Returns true if a recent measurement exists.
+ */
+async function hasRecentThroneData(): Promise {
+ try {
+ const latest = await ThroneService.getLatestMeasurement();
+ if (!latest) return false;
+
+ const age = Date.now() - new Date(latest.timestamp).getTime();
+ return age < HOURS_48;
+ } catch {
+ return true; // on error, assume data exists
+ }
+}
+
+/**
+ * DEV ONLY: Fire an immediate notification for a specific source,
+ * bypassing the 24h throttle. Useful for testing notification appearance.
+ */
+export async function triggerTestNotification(source: DataSource): Promise {
+ await Notifications.scheduleNotificationAsync({
+ identifier: `${NOTIFICATION_IDS[source]}-test`,
+ content: {
+ title: `[TEST] ${NOTIFICATION_CONTENT[source].title}`,
+ body: NOTIFICATION_CONTENT[source].body,
+ },
+ trigger: null, // fires immediately
+ });
+}
+
+/**
+ * Main entry point. Call this whenever the app comes to the foreground.
+ *
+ * For each data source:
+ * - If data found within 48h → reschedule reminder 48h from now
+ * - If no data found → fire an immediate reminder (once per 24h)
+ */
+export async function checkAndScheduleReminders(): Promise {
+ const { status } = await Notifications.getPermissionsAsync();
+ if (status !== 'granted') return;
+
+ const [healthKitOk, throneOk] = await Promise.all([
+ hasRecentHealthKitData(),
+ hasRecentThroneData(),
+ ]);
+
+ if (healthKitOk) {
+ await scheduleReminder('healthkit', HOURS_48);
+ } else {
+ await fireImmediateReminder('healthkit');
+ }
+
+ if (throneOk) {
+ await scheduleReminder('throne', HOURS_48);
+ } else {
+ await fireImmediateReminder('throne');
+ }
+}
diff --git a/homeflow/package-lock.json b/homeflow/package-lock.json
index 927903c..296a937 100644
--- a/homeflow/package-lock.json
+++ b/homeflow/package-lock.json
@@ -35,6 +35,7 @@
"expo-haptics": "~15.0.8",
"expo-image": "~3.0.11",
"expo-linking": "~8.0.11",
+ "expo-notifications": "~0.32.16",
"expo-router": "~6.0.21",
"expo-splash-screen": "~31.0.13",
"expo-status-bar": "~3.0.9",
@@ -2285,6 +2286,7 @@
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=6.9.0"
}
@@ -4531,6 +4533,12 @@
"url": "https://github.com/sponsors/nzakas"
}
},
+ "node_modules/@ide/backoff": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@ide/backoff/-/backoff-1.0.0.tgz",
+ "integrity": "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==",
+ "license": "MIT"
+ },
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
@@ -7021,6 +7029,19 @@
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"license": "MIT"
},
+ "node_modules/assert": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz",
+ "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "is-nan": "^1.3.2",
+ "object-is": "^1.1.5",
+ "object.assign": "^4.1.4",
+ "util": "^0.12.5"
+ }
+ },
"node_modules/async-function": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
@@ -7041,7 +7062,6 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"possible-typed-array-names": "^1.0.0"
@@ -7262,6 +7282,12 @@
"@babel/core": "^7.0.0"
}
},
+ "node_modules/badgin": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/badgin/-/badgin-1.2.3.tgz",
+ "integrity": "sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw==",
+ "license": "MIT"
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -7476,7 +7502,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
- "dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.0",
@@ -7495,7 +7520,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -7509,7 +7533,6 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@@ -8100,7 +8123,6 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
- "dev": true,
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
@@ -8127,7 +8149,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"define-data-property": "^1.0.1",
@@ -8269,7 +8290,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
- "dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
@@ -8421,7 +8441,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -8431,7 +8450,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -8469,7 +8487,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@@ -9199,6 +9216,15 @@
"react-native": "*"
}
},
+ "node_modules/expo-application": {
+ "version": "7.0.8",
+ "resolved": "https://registry.npmjs.org/expo-application/-/expo-application-7.0.8.tgz",
+ "integrity": "sha512-qFGyxk7VJbrNOQWBbE09XUuGuvkOgFS9QfToaK2FdagM2aQ+x3CvGV2DuVgl/l4ZxPgIf3b/MNh9xHpwSwn74Q==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-asset": {
"version": "12.0.12",
"resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-12.0.12.tgz",
@@ -9390,6 +9416,26 @@
"react-native": "*"
}
},
+ "node_modules/expo-notifications": {
+ "version": "0.32.16",
+ "resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.32.16.tgz",
+ "integrity": "sha512-QQD/UA6v7LgvwIJ+tS7tSvqJZkdp0nCSj9MxsDk/jU1GttYdK49/5L2LvE/4U0H7sNBz1NZAyhDZozg8xgBLXw==",
+ "license": "MIT",
+ "dependencies": {
+ "@expo/image-utils": "^0.8.8",
+ "@ide/backoff": "^1.0.0",
+ "abort-controller": "^3.0.0",
+ "assert": "^2.0.0",
+ "badgin": "^1.1.5",
+ "expo-application": "~7.0.8",
+ "expo-constants": "~18.0.13"
+ },
+ "peerDependencies": {
+ "expo": "*",
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/expo-router": {
"version": "6.0.22",
"resolved": "https://registry.npmjs.org/expo-router/-/expo-router-6.0.22.tgz",
@@ -10188,7 +10234,6 @@
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
"integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"is-callable": "^1.2.7"
@@ -10322,7 +10367,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
"integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -10350,7 +10394,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@@ -10393,7 +10436,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
- "dev": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
@@ -10536,7 +10578,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -10599,7 +10640,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0"
@@ -10628,7 +10668,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -10641,7 +10680,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
@@ -10956,6 +10994,22 @@
"loose-envify": "^1.0.0"
}
},
+ "node_modules/is-arguments": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
+ "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -11061,7 +11115,6 @@
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -11184,7 +11237,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
"integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.4",
@@ -11226,6 +11278,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-nan": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz",
+ "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "define-properties": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-negative-zero": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
@@ -11278,7 +11346,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
- "dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
@@ -11374,7 +11441,6 @@
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
"integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"which-typed-array": "^1.1.16"
@@ -12823,7 +12889,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -13515,11 +13580,26 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/object-is": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
+ "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -13529,7 +13609,6 @@
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
"integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.8",
@@ -14026,7 +14105,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
"integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -14455,6 +14533,7 @@
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.6.tgz",
"integrity": "sha512-F+ZJBYiok/6Jzp1re75F/9aLzkgoQCOh4yxrnwATa8392RvM3kx+fiXXFvwcgE59v48lMwd9q0nzF1oJLXpfxQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"react-native-is-edge-to-edge": "^1.2.1",
"semver": "7.7.2"
@@ -14560,6 +14639,7 @@
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz",
"integrity": "sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/plugin-transform-arrow-functions": "^7.0.0-0",
"@babel/plugin-transform-class-properties": "^7.0.0-0",
@@ -15083,7 +15163,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
"integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
@@ -15240,7 +15319,6 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
@@ -16696,6 +16774,19 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
+ "node_modules/util": {
+ "version": "0.12.5",
+ "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
+ "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "is-arguments": "^1.0.4",
+ "is-generator-function": "^1.0.7",
+ "is-typed-array": "^1.1.3",
+ "which-typed-array": "^1.1.2"
+ }
+ },
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -17127,7 +17218,6 @@
"version": "1.1.20",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
"integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"available-typed-arrays": "^1.0.7",
diff --git a/homeflow/package.json b/homeflow/package.json
index 1dfb433..bedb7fa 100644
--- a/homeflow/package.json
+++ b/homeflow/package.json
@@ -38,6 +38,7 @@
"expo-haptics": "~15.0.8",
"expo-image": "~3.0.11",
"expo-linking": "~8.0.11",
+ "expo-notifications": "~0.32.16",
"expo-router": "~6.0.21",
"expo-splash-screen": "~31.0.13",
"expo-status-bar": "~3.0.9",