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