├── .gitignore ├── App.js ├── App.tsx ├── app.config.ts ├── app.json ├── assets ├── adaptive-icon.png ├── favicon.png ├── icon.png └── splash-icon.png ├── config.json ├── index.ts ├── ios ├── .gitignore ├── .xcode.env ├── Podfile ├── Podfile.lock ├── Podfile.properties.json ├── todoapp.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ └── todoapp.xcscheme ├── todoapp.xcworkspace │ └── contents.xcworkspacedata └── todoapp │ ├── AppDelegate.h │ ├── AppDelegate.mm │ ├── Images.xcassets │ ├── AppIcon.appiconset │ │ ├── App-Icon-1024x1024@1x.png │ │ └── Contents.json │ ├── Contents.json │ ├── SplashScreenBackground.colorset │ │ └── Contents.json │ └── SplashScreenLogo.imageset │ │ ├── Contents.json │ │ ├── image.png │ │ ├── image@2x.png │ │ └── image@3x.png │ ├── Info.plist │ ├── PrivacyInfo.xcprivacy │ ├── SplashScreen.storyboard │ ├── Supporting │ └── Expo.plist │ ├── main.m │ ├── noop-file.swift │ ├── todoapp-Bridging-Header.h │ └── todoapp.entitlements ├── package-lock.json ├── package.json ├── src ├── components │ ├── ErrorBoundary.tsx │ ├── ErrorDisplay.tsx │ ├── ErrorMessage.tsx │ ├── LoadingSpinner.tsx │ ├── MainContent.tsx │ ├── SecureView.tsx │ ├── liquid │ │ └── LiquidShader.tsx │ ├── shaders │ │ └── DarkModeShader.tsx │ └── todo │ │ ├── SmartTodoInput.tsx │ │ ├── TodoItem.tsx │ │ ├── TodoList.tsx │ │ └── constants.ts ├── contexts │ ├── AuthContext.tsx │ ├── SettingsContext.tsx │ ├── ThemeContext.tsx │ └── TodoContext.tsx ├── hooks │ ├── useErrorHandler.ts │ ├── useNotifications.ts │ └── useVoiceRecognition.ts ├── navigation │ └── AppNavigator.tsx ├── screens │ ├── AddTodoScreen.tsx │ ├── AnalyticsScreen.tsx │ ├── AuthScreen.tsx │ ├── DebugScreen.tsx │ ├── HomeScreen.tsx │ ├── SettingsScreen.tsx │ └── _layout.tsx ├── services │ ├── ApiService.ts │ ├── DatabaseService.ts │ ├── LLMService.ts │ ├── LoggingService.ts │ ├── MockLLMService.ts │ ├── NLPService.ts │ ├── NotificationService.ts │ ├── SettingsService.ts │ └── database.ts ├── types │ ├── index.ts │ ├── nlp.ts │ └── settings.ts └── utils │ ├── animations.ts │ ├── storage.ts │ └── types.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | expo-env.d.ts 11 | 12 | # Native 13 | *.orig.* 14 | *.jks 15 | *.p8 16 | *.p12 17 | *.key 18 | *.mobileprovision 19 | 20 | # Metro 21 | .metro-health-check* 22 | 23 | # debug 24 | npm-debug.* 25 | yarn-debug.* 26 | yarn-error.* 27 | 28 | # macOS 29 | .DS_Store 30 | *.pem 31 | 32 | # local env files 33 | .env*.local 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | // ... existing code ... 38 | # local env files 39 | .env 40 | .env*.local 41 | .env.development 42 | .env.production 43 | // ... existing code ... -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | import { LogBox } from 'react-native'; 2 | import Reanimated from 'react-native-reanimated'; 3 | 4 | // Disable the strict mode warning 5 | if (Reanimated.setGestureStateIfRunningInRemoteDebugger) { 6 | Reanimated.setGestureStateIfRunningInRemoteDebugger(true); 7 | } 8 | 9 | // Or alternatively, you can just ignore the warning 10 | LogBox.ignoreLogs([ 11 | "[Reanimated] Reading from `value` during component render" 12 | ]); -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import { SQLiteProvider } from 'expo-sqlite'; 2 | import { Suspense } from 'react'; 3 | import { View } from 'react-native'; 4 | import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context'; 5 | import { TodoProvider } from './src/contexts/TodoContext'; 6 | import { LoadingSpinner } from './src/components/LoadingSpinner'; 7 | import { SettingsProvider } from './src/contexts/SettingsContext'; 8 | import { AppNavigator } from './src/navigation/AppNavigator'; 9 | import { ErrorBoundary } from './src/components/ErrorBoundary'; 10 | import { GestureHandlerRootView } from 'react-native-gesture-handler'; 11 | import { LogBox } from 'react-native'; 12 | import { configureReanimatedLogger } from 'react-native-reanimated'; 13 | 14 | LogBox.ignoreLogs([ 15 | "[Reanimated] Reading from `value` during component render" 16 | ]); 17 | 18 | configureReanimatedLogger({ 19 | strict: false // This disables strict mode warnings 20 | }); 21 | 22 | export default function App() { 23 | 24 | return ( 25 | 26 | 27 | 28 | 32 | }> 33 | { 36 | await db.execAsync(` 37 | PRAGMA journal_mode = WAL; 38 | CREATE TABLE IF NOT EXISTS todos ( 39 | id TEXT PRIMARY KEY NOT NULL, 40 | title TEXT NOT NULL, 41 | description TEXT, 42 | dueDate TEXT NOT NULL, 43 | completed INTEGER NOT NULL DEFAULT 0, 44 | tags TEXT, 45 | priority TEXT NOT NULL 46 | ); 47 | `); 48 | }} 49 | useSuspense 50 | > 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | ); 63 | } -------------------------------------------------------------------------------- /app.config.ts: -------------------------------------------------------------------------------- 1 | import { ExpoConfig, ConfigContext } from 'expo/config'; 2 | 3 | export default function ({ config }: ConfigContext): ExpoConfig { 4 | return { 5 | ...config, 6 | name: 'todo-app', 7 | slug: 'todo-app', 8 | extra: { 9 | openAiKey: process.env.OPENAI_API_KEY, 10 | environment: process.env.NODE_ENV || 'development', 11 | }, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "Dotomo", 4 | "slug": "dotomo", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "newArchEnabled": true, 10 | "splash": { 11 | "image": "./assets/splash-icon.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#ffffff" 14 | }, 15 | "ios": { 16 | "bundleIdentifier": "com.dotomo.app", 17 | "infoPlist": { 18 | "NSFaceIDUsageDescription": "We need to use Face ID to securely authenticate you" 19 | } 20 | }, 21 | "plugins": [ 22 | [ 23 | "expo-local-authentication", 24 | { 25 | "faceIDPermission": "Allow $(PRODUCT_NAME) to use Face ID to authenticate." 26 | } 27 | ] 28 | ], 29 | "_internal": { 30 | "ios": { 31 | "useFrameworks": "static" 32 | } 33 | }, 34 | "android": { 35 | "permissions": [ 36 | "android.permission.USE_BIOMETRIC", 37 | "android.permission.USE_FINGERPRINT" 38 | ], 39 | "package": "com.dotomo.app" 40 | }, 41 | "extra": { 42 | "environment": "development", 43 | "eas": { 44 | "projectId": "876bbb17-33fe-46ff-b86b-8814629318a3" 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arashmidus/dotomo/3e258d81cec55d638e3c9d1ee508b9eeea45937c/assets/adaptive-icon.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arashmidus/dotomo/3e258d81cec55d638e3c9d1ee508b9eeea45937c/assets/favicon.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arashmidus/dotomo/3e258d81cec55d638e3c9d1ee508b9eeea45937c/assets/icon.png -------------------------------------------------------------------------------- /assets/splash-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arashmidus/dotomo/3e258d81cec55d638e3c9d1ee508b9eeea45937c/assets/splash-icon.png -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "todo-app", 4 | "slug": "todo-app", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "newArchEnabled": true, 10 | "splash": { 11 | "image": "./assets/splash-icon.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#ffffff" 14 | }, 15 | "ios": { 16 | "supportsTablet": true 17 | }, 18 | "android": { 19 | "adaptiveIcon": { 20 | "foregroundImage": "./assets/adaptive-icon.png", 21 | "backgroundColor": "#ffffff" 22 | } 23 | }, 24 | "web": { 25 | "favicon": "./assets/favicon.png" 26 | }, 27 | "plugins": [ 28 | "expo-sqlite" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from 'expo'; 2 | 3 | import App from './App'; 4 | 5 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App); 6 | // It also ensures that whether you load the app in Expo Go or in a native build, 7 | // the environment is set up appropriately 8 | registerRootComponent(App); 9 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | .xcode.env.local 25 | 26 | # Bundle artifacts 27 | *.jsbundle 28 | 29 | # CocoaPods 30 | /Pods/ 31 | -------------------------------------------------------------------------------- /ios/.xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | 6 | # NODE_BINARY variable contains the PATH to the node executable. 7 | # 8 | # Customize the NODE_BINARY variable here. 9 | # For example, to use nvm with brew, add the following line 10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 11 | export NODE_BINARY=$(command -v node) 12 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking") 2 | require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods") 3 | 4 | require 'json' 5 | podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {} 6 | 7 | ENV['RCT_NEW_ARCH_ENABLED'] = podfile_properties['newArchEnabled'] == 'true' ? '1' : '0' 8 | ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR'] 9 | 10 | platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1' 11 | install! 'cocoapods', 12 | :deterministic_uuids => false 13 | 14 | prepare_react_native_project! 15 | 16 | target 'todoapp' do 17 | use_expo_modules! 18 | 19 | if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1' 20 | config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"]; 21 | else 22 | config_command = [ 23 | 'node', 24 | '--no-warnings', 25 | '--eval', 26 | 'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))', 27 | 'react-native-config', 28 | '--json', 29 | '--platform', 30 | 'ios' 31 | ] 32 | end 33 | 34 | config = use_native_modules!(config_command) 35 | 36 | use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks'] 37 | use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS'] 38 | 39 | use_react_native!( 40 | :path => config[:reactNativePath], 41 | :hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes', 42 | # An absolute path to your application root. 43 | :app_path => "#{Pod::Config.instance.installation_root}/..", 44 | :privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false', 45 | ) 46 | 47 | post_install do |installer| 48 | react_native_post_install( 49 | installer, 50 | config[:reactNativePath], 51 | :mac_catalyst_enabled => false, 52 | :ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true', 53 | ) 54 | 55 | # This is necessary for Xcode 14, because it signs resource bundles by default 56 | # when building for devices. 57 | installer.target_installation_results.pod_target_installation_results 58 | .each do |pod_name, target_installation_result| 59 | target_installation_result.resource_bundle_targets.each do |resource_bundle_target| 60 | resource_bundle_target.build_configurations.each do |config| 61 | config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO' 62 | end 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /ios/Podfile.properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo.jsEngine": "hermes", 3 | "EX_DEV_CLIENT_NETWORK_INSPECTOR": "true", 4 | "newArchEnabled": "false" 5 | } 6 | -------------------------------------------------------------------------------- /ios/todoapp.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; }; 11 | 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 12 | 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 13 | 3949569EBE653CAC3A96DA3E /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = DED89A866FE68C14CF32FCD7 /* PrivacyInfo.xcprivacy */; }; 14 | 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; }; 15 | 96905EF65AED1B983A6B3ABC /* libPods-todoapp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-todoapp.a */; }; 16 | B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; }; 17 | BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; }; 18 | F352EC546FB94079A309964A /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CF93DCEE5C4D448621986A /* noop-file.swift */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXFileReference section */ 22 | 13B07F961A680F5B00A75B9A /* todoapp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = todoapp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 23 | 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = todoapp/AppDelegate.h; sourceTree = ""; }; 24 | 13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = todoapp/AppDelegate.mm; sourceTree = ""; }; 25 | 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = todoapp/Images.xcassets; sourceTree = ""; }; 26 | 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = todoapp/Info.plist; sourceTree = ""; }; 27 | 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = todoapp/main.m; sourceTree = ""; }; 28 | 48CF93DCEE5C4D448621986A /* noop-file.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = "noop-file.swift"; path = "todoapp/noop-file.swift"; sourceTree = ""; }; 29 | 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-todoapp.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-todoapp.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 30 | 5D4DFB6B8E0B4457A95B71A4 /* todoapp-Bridging-Header.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = "todoapp-Bridging-Header.h"; path = "todoapp/todoapp-Bridging-Header.h"; sourceTree = ""; }; 31 | 6C2E3173556A471DD304B334 /* Pods-todoapp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-todoapp.debug.xcconfig"; path = "Target Support Files/Pods-todoapp/Pods-todoapp.debug.xcconfig"; sourceTree = ""; }; 32 | 7A4D352CD337FB3A3BF06240 /* Pods-todoapp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-todoapp.release.xcconfig"; path = "Target Support Files/Pods-todoapp/Pods-todoapp.release.xcconfig"; sourceTree = ""; }; 33 | AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = todoapp/SplashScreen.storyboard; sourceTree = ""; }; 34 | BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = ""; }; 35 | DED89A866FE68C14CF32FCD7 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = todoapp/PrivacyInfo.xcprivacy; sourceTree = ""; }; 36 | ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; 37 | FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-todoapp/ExpoModulesProvider.swift"; sourceTree = ""; }; 38 | /* End PBXFileReference section */ 39 | 40 | /* Begin PBXFrameworksBuildPhase section */ 41 | 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { 42 | isa = PBXFrameworksBuildPhase; 43 | buildActionMask = 2147483647; 44 | files = ( 45 | 96905EF65AED1B983A6B3ABC /* libPods-todoapp.a in Frameworks */, 46 | ); 47 | runOnlyForDeploymentPostprocessing = 0; 48 | }; 49 | /* End PBXFrameworksBuildPhase section */ 50 | 51 | /* Begin PBXGroup section */ 52 | 13B07FAE1A68108700A75B9A /* todoapp */ = { 53 | isa = PBXGroup; 54 | children = ( 55 | BB2F792B24A3F905000567C9 /* Supporting */, 56 | 13B07FAF1A68108700A75B9A /* AppDelegate.h */, 57 | 13B07FB01A68108700A75B9A /* AppDelegate.mm */, 58 | 13B07FB51A68108700A75B9A /* Images.xcassets */, 59 | 13B07FB61A68108700A75B9A /* Info.plist */, 60 | 13B07FB71A68108700A75B9A /* main.m */, 61 | AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */, 62 | 48CF93DCEE5C4D448621986A /* noop-file.swift */, 63 | 5D4DFB6B8E0B4457A95B71A4 /* todoapp-Bridging-Header.h */, 64 | DED89A866FE68C14CF32FCD7 /* PrivacyInfo.xcprivacy */, 65 | ); 66 | name = todoapp; 67 | sourceTree = ""; 68 | }; 69 | 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { 70 | isa = PBXGroup; 71 | children = ( 72 | ED297162215061F000B7C4FE /* JavaScriptCore.framework */, 73 | 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-todoapp.a */, 74 | ); 75 | name = Frameworks; 76 | sourceTree = ""; 77 | }; 78 | 832341AE1AAA6A7D00B99B32 /* Libraries */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | ); 82 | name = Libraries; 83 | sourceTree = ""; 84 | }; 85 | 83CBB9F61A601CBA00E9B192 = { 86 | isa = PBXGroup; 87 | children = ( 88 | 13B07FAE1A68108700A75B9A /* todoapp */, 89 | 832341AE1AAA6A7D00B99B32 /* Libraries */, 90 | 83CBBA001A601CBA00E9B192 /* Products */, 91 | 2D16E6871FA4F8E400B85C8A /* Frameworks */, 92 | D65327D7A22EEC0BE12398D9 /* Pods */, 93 | D7E4C46ADA2E9064B798F356 /* ExpoModulesProviders */, 94 | ); 95 | indentWidth = 2; 96 | sourceTree = ""; 97 | tabWidth = 2; 98 | usesTabs = 0; 99 | }; 100 | 83CBBA001A601CBA00E9B192 /* Products */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | 13B07F961A680F5B00A75B9A /* todoapp.app */, 104 | ); 105 | name = Products; 106 | sourceTree = ""; 107 | }; 108 | 92DBD88DE9BF7D494EA9DA96 /* todoapp */ = { 109 | isa = PBXGroup; 110 | children = ( 111 | FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */, 112 | ); 113 | name = todoapp; 114 | sourceTree = ""; 115 | }; 116 | BB2F792B24A3F905000567C9 /* Supporting */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | BB2F792C24A3F905000567C9 /* Expo.plist */, 120 | ); 121 | name = Supporting; 122 | path = todoapp/Supporting; 123 | sourceTree = ""; 124 | }; 125 | D65327D7A22EEC0BE12398D9 /* Pods */ = { 126 | isa = PBXGroup; 127 | children = ( 128 | 6C2E3173556A471DD304B334 /* Pods-todoapp.debug.xcconfig */, 129 | 7A4D352CD337FB3A3BF06240 /* Pods-todoapp.release.xcconfig */, 130 | ); 131 | path = Pods; 132 | sourceTree = ""; 133 | }; 134 | D7E4C46ADA2E9064B798F356 /* ExpoModulesProviders */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | 92DBD88DE9BF7D494EA9DA96 /* todoapp */, 138 | ); 139 | name = ExpoModulesProviders; 140 | sourceTree = ""; 141 | }; 142 | /* End PBXGroup section */ 143 | 144 | /* Begin PBXNativeTarget section */ 145 | 13B07F861A680F5B00A75B9A /* todoapp */ = { 146 | isa = PBXNativeTarget; 147 | buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "todoapp" */; 148 | buildPhases = ( 149 | 08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */, 150 | 535735544E73AD6AF9DCF13B /* [Expo] Configure project */, 151 | 13B07F871A680F5B00A75B9A /* Sources */, 152 | 13B07F8C1A680F5B00A75B9A /* Frameworks */, 153 | 13B07F8E1A680F5B00A75B9A /* Resources */, 154 | 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, 155 | 800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */, 156 | 5BAA030C90FC35609B65D1EF /* [CP] Embed Pods Frameworks */, 157 | ); 158 | buildRules = ( 159 | ); 160 | dependencies = ( 161 | ); 162 | name = todoapp; 163 | productName = todoapp; 164 | productReference = 13B07F961A680F5B00A75B9A /* todoapp.app */; 165 | productType = "com.apple.product-type.application"; 166 | }; 167 | /* End PBXNativeTarget section */ 168 | 169 | /* Begin PBXProject section */ 170 | 83CBB9F71A601CBA00E9B192 /* Project object */ = { 171 | isa = PBXProject; 172 | attributes = { 173 | LastUpgradeCheck = 1130; 174 | TargetAttributes = { 175 | 13B07F861A680F5B00A75B9A = { 176 | LastSwiftMigration = 1250; 177 | }; 178 | }; 179 | }; 180 | buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "todoapp" */; 181 | compatibilityVersion = "Xcode 3.2"; 182 | developmentRegion = en; 183 | hasScannedForEncodings = 0; 184 | knownRegions = ( 185 | en, 186 | Base, 187 | ); 188 | mainGroup = 83CBB9F61A601CBA00E9B192; 189 | productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; 190 | projectDirPath = ""; 191 | projectRoot = ""; 192 | targets = ( 193 | 13B07F861A680F5B00A75B9A /* todoapp */, 194 | ); 195 | }; 196 | /* End PBXProject section */ 197 | 198 | /* Begin PBXResourcesBuildPhase section */ 199 | 13B07F8E1A680F5B00A75B9A /* Resources */ = { 200 | isa = PBXResourcesBuildPhase; 201 | buildActionMask = 2147483647; 202 | files = ( 203 | BB2F792D24A3F905000567C9 /* Expo.plist in Resources */, 204 | 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, 205 | 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */, 206 | 3949569EBE653CAC3A96DA3E /* PrivacyInfo.xcprivacy in Resources */, 207 | ); 208 | runOnlyForDeploymentPostprocessing = 0; 209 | }; 210 | /* End PBXResourcesBuildPhase section */ 211 | 212 | /* Begin PBXShellScriptBuildPhase section */ 213 | 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { 214 | isa = PBXShellScriptBuildPhase; 215 | alwaysOutOfDate = 1; 216 | buildActionMask = 2147483647; 217 | files = ( 218 | ); 219 | inputPaths = ( 220 | ); 221 | name = "Bundle React Native code and images"; 222 | outputPaths = ( 223 | ); 224 | runOnlyForDeploymentPostprocessing = 0; 225 | shellPath = /bin/sh; 226 | shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n# Source .xcode.env.updates if it exists to allow\n# SKIP_BUNDLING to be unset if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.updates\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.updates\"\nfi\n# Source local changes to allow overrides\n# if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n"; 227 | }; 228 | 08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */ = { 229 | isa = PBXShellScriptBuildPhase; 230 | buildActionMask = 2147483647; 231 | files = ( 232 | ); 233 | inputFileListPaths = ( 234 | ); 235 | inputPaths = ( 236 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 237 | "${PODS_ROOT}/Manifest.lock", 238 | ); 239 | name = "[CP] Check Pods Manifest.lock"; 240 | outputFileListPaths = ( 241 | ); 242 | outputPaths = ( 243 | "$(DERIVED_FILE_DIR)/Pods-todoapp-checkManifestLockResult.txt", 244 | ); 245 | runOnlyForDeploymentPostprocessing = 0; 246 | shellPath = /bin/sh; 247 | 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"; 248 | showEnvVarsInLog = 0; 249 | }; 250 | 535735544E73AD6AF9DCF13B /* [Expo] Configure project */ = { 251 | isa = PBXShellScriptBuildPhase; 252 | alwaysOutOfDate = 1; 253 | buildActionMask = 2147483647; 254 | files = ( 255 | ); 256 | inputFileListPaths = ( 257 | ); 258 | inputPaths = ( 259 | ); 260 | name = "[Expo] Configure project"; 261 | outputFileListPaths = ( 262 | ); 263 | outputPaths = ( 264 | ); 265 | runOnlyForDeploymentPostprocessing = 0; 266 | shellPath = /bin/sh; 267 | shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-todoapp/expo-configure-project.sh\"\n"; 268 | }; 269 | 5BAA030C90FC35609B65D1EF /* [CP] Embed Pods Frameworks */ = { 270 | isa = PBXShellScriptBuildPhase; 271 | buildActionMask = 2147483647; 272 | files = ( 273 | ); 274 | inputPaths = ( 275 | "${PODS_ROOT}/Target Support Files/Pods-todoapp/Pods-todoapp-frameworks.sh", 276 | "${PODS_XCFRAMEWORKS_BUILD_DIR}/ExpoSQLite/crsqlite.framework/crsqlite", 277 | "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", 278 | ); 279 | name = "[CP] Embed Pods Frameworks"; 280 | outputPaths = ( 281 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/crsqlite.framework", 282 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", 283 | ); 284 | runOnlyForDeploymentPostprocessing = 0; 285 | shellPath = /bin/sh; 286 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-todoapp/Pods-todoapp-frameworks.sh\"\n"; 287 | showEnvVarsInLog = 0; 288 | }; 289 | 800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */ = { 290 | isa = PBXShellScriptBuildPhase; 291 | buildActionMask = 2147483647; 292 | files = ( 293 | ); 294 | inputPaths = ( 295 | "${PODS_ROOT}/Target Support Files/Pods-todoapp/Pods-todoapp-resources.sh", 296 | "${PODS_CONFIGURATION_BUILD_DIR}/EXApplication/ExpoApplication_privacy.bundle", 297 | "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle", 298 | "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle", 299 | "${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle", 300 | "${PODS_CONFIGURATION_BUILD_DIR}/ExpoDevice/ExpoDevice_privacy.bundle", 301 | "${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle", 302 | "${PODS_ROOT}/GPUImage/framework/Resources/lookup.png", 303 | "${PODS_ROOT}/GPUImage/framework/Resources/lookup_amatorka.png", 304 | "${PODS_ROOT}/GPUImage/framework/Resources/lookup_miss_etikate.png", 305 | "${PODS_ROOT}/GPUImage/framework/Resources/lookup_soft_elegance_1.png", 306 | "${PODS_ROOT}/GPUImage/framework/Resources/lookup_soft_elegance_2.png", 307 | "${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle", 308 | "${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle", 309 | "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle", 310 | "${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle", 311 | "${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle", 312 | "${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle", 313 | ); 314 | name = "[CP] Copy Pods Resources"; 315 | outputPaths = ( 316 | "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoApplication_privacy.bundle", 317 | "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle", 318 | "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle", 319 | "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle", 320 | "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoDevice_privacy.bundle", 321 | "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle", 322 | "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/lookup.png", 323 | "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/lookup_amatorka.png", 324 | "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/lookup_miss_etikate.png", 325 | "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/lookup_soft_elegance_1.png", 326 | "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/lookup_soft_elegance_2.png", 327 | "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle", 328 | "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle", 329 | "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle", 330 | "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle", 331 | "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle", 332 | "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle", 333 | ); 334 | runOnlyForDeploymentPostprocessing = 0; 335 | shellPath = /bin/sh; 336 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-todoapp/Pods-todoapp-resources.sh\"\n"; 337 | showEnvVarsInLog = 0; 338 | }; 339 | /* End PBXShellScriptBuildPhase section */ 340 | 341 | /* Begin PBXSourcesBuildPhase section */ 342 | 13B07F871A680F5B00A75B9A /* Sources */ = { 343 | isa = PBXSourcesBuildPhase; 344 | buildActionMask = 2147483647; 345 | files = ( 346 | 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */, 347 | 13B07FC11A68108700A75B9A /* main.m in Sources */, 348 | B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */, 349 | F352EC546FB94079A309964A /* noop-file.swift in Sources */, 350 | ); 351 | runOnlyForDeploymentPostprocessing = 0; 352 | }; 353 | /* End PBXSourcesBuildPhase section */ 354 | 355 | /* Begin XCBuildConfiguration section */ 356 | 13B07F941A680F5B00A75B9A /* Debug */ = { 357 | isa = XCBuildConfiguration; 358 | baseConfigurationReference = 6C2E3173556A471DD304B334 /* Pods-todoapp.debug.xcconfig */; 359 | buildSettings = { 360 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 361 | CLANG_ENABLE_MODULES = YES; 362 | CODE_SIGN_ENTITLEMENTS = todoapp/todoapp.entitlements; 363 | CURRENT_PROJECT_VERSION = 1; 364 | ENABLE_BITCODE = NO; 365 | GCC_PREPROCESSOR_DEFINITIONS = ( 366 | "$(inherited)", 367 | "FB_SONARKIT_ENABLED=1", 368 | ); 369 | INFOPLIST_FILE = todoapp/Info.plist; 370 | IPHONEOS_DEPLOYMENT_TARGET = 15.1; 371 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 372 | MARKETING_VERSION = 1.0; 373 | OTHER_LDFLAGS = ( 374 | "$(inherited)", 375 | "-ObjC", 376 | "-lc++", 377 | ); 378 | OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; 379 | PRODUCT_BUNDLE_IDENTIFIER = com.yourusername.todoapp; 380 | PRODUCT_NAME = todoapp; 381 | SWIFT_OBJC_BRIDGING_HEADER = "todoapp/todoapp-Bridging-Header.h"; 382 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 383 | SWIFT_VERSION = 5.0; 384 | TARGETED_DEVICE_FAMILY = 1; 385 | VERSIONING_SYSTEM = "apple-generic"; 386 | }; 387 | name = Debug; 388 | }; 389 | 13B07F951A680F5B00A75B9A /* Release */ = { 390 | isa = XCBuildConfiguration; 391 | baseConfigurationReference = 7A4D352CD337FB3A3BF06240 /* Pods-todoapp.release.xcconfig */; 392 | buildSettings = { 393 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 394 | CLANG_ENABLE_MODULES = YES; 395 | CODE_SIGN_ENTITLEMENTS = todoapp/todoapp.entitlements; 396 | CURRENT_PROJECT_VERSION = 1; 397 | INFOPLIST_FILE = todoapp/Info.plist; 398 | IPHONEOS_DEPLOYMENT_TARGET = 15.1; 399 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 400 | MARKETING_VERSION = 1.0; 401 | OTHER_LDFLAGS = ( 402 | "$(inherited)", 403 | "-ObjC", 404 | "-lc++", 405 | ); 406 | OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; 407 | PRODUCT_BUNDLE_IDENTIFIER = com.yourusername.todoapp; 408 | PRODUCT_NAME = todoapp; 409 | SWIFT_OBJC_BRIDGING_HEADER = "todoapp/todoapp-Bridging-Header.h"; 410 | SWIFT_VERSION = 5.0; 411 | TARGETED_DEVICE_FAMILY = 1; 412 | VERSIONING_SYSTEM = "apple-generic"; 413 | }; 414 | name = Release; 415 | }; 416 | 83CBBA201A601CBA00E9B192 /* Debug */ = { 417 | isa = XCBuildConfiguration; 418 | buildSettings = { 419 | ALWAYS_SEARCH_USER_PATHS = NO; 420 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 421 | CLANG_CXX_LANGUAGE_STANDARD = "c++20"; 422 | CLANG_CXX_LIBRARY = "libc++"; 423 | CLANG_ENABLE_MODULES = YES; 424 | CLANG_ENABLE_OBJC_ARC = YES; 425 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 426 | CLANG_WARN_BOOL_CONVERSION = YES; 427 | CLANG_WARN_COMMA = YES; 428 | CLANG_WARN_CONSTANT_CONVERSION = YES; 429 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 430 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 431 | CLANG_WARN_EMPTY_BODY = YES; 432 | CLANG_WARN_ENUM_CONVERSION = YES; 433 | CLANG_WARN_INFINITE_RECURSION = YES; 434 | CLANG_WARN_INT_CONVERSION = YES; 435 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 436 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 437 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 438 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 439 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 440 | CLANG_WARN_STRICT_PROTOTYPES = YES; 441 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 442 | CLANG_WARN_UNREACHABLE_CODE = YES; 443 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 444 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 445 | COPY_PHASE_STRIP = NO; 446 | ENABLE_STRICT_OBJC_MSGSEND = YES; 447 | ENABLE_TESTABILITY = YES; 448 | GCC_C_LANGUAGE_STANDARD = gnu99; 449 | GCC_DYNAMIC_NO_PIC = NO; 450 | GCC_NO_COMMON_BLOCKS = YES; 451 | GCC_OPTIMIZATION_LEVEL = 0; 452 | GCC_PREPROCESSOR_DEFINITIONS = ( 453 | "DEBUG=1", 454 | "$(inherited)", 455 | ); 456 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 457 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 458 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 459 | GCC_WARN_UNDECLARED_SELECTOR = YES; 460 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 461 | GCC_WARN_UNUSED_FUNCTION = YES; 462 | GCC_WARN_UNUSED_VARIABLE = YES; 463 | IPHONEOS_DEPLOYMENT_TARGET = 15.1; 464 | LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)"; 465 | LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; 466 | MTL_ENABLE_DEBUG_INFO = YES; 467 | ONLY_ACTIVE_ARCH = YES; 468 | OTHER_LDFLAGS = ( 469 | "$(inherited)", 470 | " ", 471 | ); 472 | REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; 473 | SDKROOT = iphoneos; 474 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; 475 | USE_HERMES = true; 476 | }; 477 | name = Debug; 478 | }; 479 | 83CBBA211A601CBA00E9B192 /* Release */ = { 480 | isa = XCBuildConfiguration; 481 | buildSettings = { 482 | ALWAYS_SEARCH_USER_PATHS = NO; 483 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 484 | CLANG_CXX_LANGUAGE_STANDARD = "c++20"; 485 | CLANG_CXX_LIBRARY = "libc++"; 486 | CLANG_ENABLE_MODULES = YES; 487 | CLANG_ENABLE_OBJC_ARC = YES; 488 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 489 | CLANG_WARN_BOOL_CONVERSION = YES; 490 | CLANG_WARN_COMMA = YES; 491 | CLANG_WARN_CONSTANT_CONVERSION = YES; 492 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 493 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 494 | CLANG_WARN_EMPTY_BODY = YES; 495 | CLANG_WARN_ENUM_CONVERSION = YES; 496 | CLANG_WARN_INFINITE_RECURSION = YES; 497 | CLANG_WARN_INT_CONVERSION = YES; 498 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 499 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 500 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 501 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 502 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 503 | CLANG_WARN_STRICT_PROTOTYPES = YES; 504 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 505 | CLANG_WARN_UNREACHABLE_CODE = YES; 506 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 507 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 508 | COPY_PHASE_STRIP = YES; 509 | ENABLE_NS_ASSERTIONS = NO; 510 | ENABLE_STRICT_OBJC_MSGSEND = YES; 511 | GCC_C_LANGUAGE_STANDARD = gnu99; 512 | GCC_NO_COMMON_BLOCKS = YES; 513 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 514 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 515 | GCC_WARN_UNDECLARED_SELECTOR = YES; 516 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 517 | GCC_WARN_UNUSED_FUNCTION = YES; 518 | GCC_WARN_UNUSED_VARIABLE = YES; 519 | IPHONEOS_DEPLOYMENT_TARGET = 15.1; 520 | LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)"; 521 | LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; 522 | MTL_ENABLE_DEBUG_INFO = NO; 523 | OTHER_LDFLAGS = ( 524 | "$(inherited)", 525 | " ", 526 | ); 527 | REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; 528 | SDKROOT = iphoneos; 529 | USE_HERMES = true; 530 | VALIDATE_PRODUCT = YES; 531 | }; 532 | name = Release; 533 | }; 534 | /* End XCBuildConfiguration section */ 535 | 536 | /* Begin XCConfigurationList section */ 537 | 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "todoapp" */ = { 538 | isa = XCConfigurationList; 539 | buildConfigurations = ( 540 | 13B07F941A680F5B00A75B9A /* Debug */, 541 | 13B07F951A680F5B00A75B9A /* Release */, 542 | ); 543 | defaultConfigurationIsVisible = 0; 544 | defaultConfigurationName = Release; 545 | }; 546 | 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "todoapp" */ = { 547 | isa = XCConfigurationList; 548 | buildConfigurations = ( 549 | 83CBBA201A601CBA00E9B192 /* Debug */, 550 | 83CBBA211A601CBA00E9B192 /* Release */, 551 | ); 552 | defaultConfigurationIsVisible = 0; 553 | defaultConfigurationName = Release; 554 | }; 555 | /* End XCConfigurationList section */ 556 | }; 557 | rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; 558 | } 559 | -------------------------------------------------------------------------------- /ios/todoapp.xcodeproj/xcshareddata/xcschemes/todoapp.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /ios/todoapp.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/todoapp/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | 5 | @interface AppDelegate : EXAppDelegateWrapper 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /ios/todoapp/AppDelegate.mm: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | #import 5 | 6 | @implementation AppDelegate 7 | 8 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 9 | { 10 | self.moduleName = @"main"; 11 | 12 | // You can add your custom initial props in the dictionary below. 13 | // They will be passed down to the ViewController used by React Native. 14 | self.initialProps = @{}; 15 | 16 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 17 | } 18 | 19 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 20 | { 21 | return [self bundleURL]; 22 | } 23 | 24 | - (NSURL *)bundleURL 25 | { 26 | #if DEBUG 27 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@".expo/.virtual-metro-entry"]; 28 | #else 29 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 30 | #endif 31 | } 32 | 33 | // Linking API 34 | - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { 35 | return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options]; 36 | } 37 | 38 | // Universal Links 39 | - (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler { 40 | BOOL result = [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; 41 | return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || result; 42 | } 43 | 44 | // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries 45 | - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken 46 | { 47 | return [super application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; 48 | } 49 | 50 | // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries 51 | - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error 52 | { 53 | return [super application:application didFailToRegisterForRemoteNotificationsWithError:error]; 54 | } 55 | 56 | // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries 57 | - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler 58 | { 59 | return [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; 60 | } 61 | 62 | @end 63 | -------------------------------------------------------------------------------- /ios/todoapp/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arashmidus/dotomo/3e258d81cec55d638e3c9d1ee508b9eeea45937c/ios/todoapp/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/todoapp/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "filename": "App-Icon-1024x1024@1x.png", 5 | "idiom": "universal", 6 | "platform": "ios", 7 | "size": "1024x1024" 8 | } 9 | ], 10 | "info": { 11 | "version": 1, 12 | "author": "expo" 13 | } 14 | } -------------------------------------------------------------------------------- /ios/todoapp/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "expo" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ios/todoapp/Images.xcassets/SplashScreenBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors": [ 3 | { 4 | "color": { 5 | "components": { 6 | "alpha": "1.000", 7 | "blue": "1.00000000000000", 8 | "green": "1.00000000000000", 9 | "red": "1.00000000000000" 10 | }, 11 | "color-space": "srgb" 12 | }, 13 | "idiom": "universal" 14 | } 15 | ], 16 | "info": { 17 | "version": 1, 18 | "author": "expo" 19 | } 20 | } -------------------------------------------------------------------------------- /ios/todoapp/Images.xcassets/SplashScreenLogo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "idiom": "universal", 5 | "appearances": [ 6 | { 7 | "appearance": "luminosity", 8 | "value": "light" 9 | } 10 | ], 11 | "filename": "image.png", 12 | "scale": "1x" 13 | }, 14 | { 15 | "idiom": "universal", 16 | "appearances": [ 17 | { 18 | "appearance": "luminosity", 19 | "value": "light" 20 | } 21 | ], 22 | "filename": "image@2x.png", 23 | "scale": "2x" 24 | }, 25 | { 26 | "idiom": "universal", 27 | "appearances": [ 28 | { 29 | "appearance": "luminosity", 30 | "value": "light" 31 | } 32 | ], 33 | "filename": "image@3x.png", 34 | "scale": "3x" 35 | } 36 | ], 37 | "info": { 38 | "version": 1, 39 | "author": "expo" 40 | } 41 | } -------------------------------------------------------------------------------- /ios/todoapp/Images.xcassets/SplashScreenLogo.imageset/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arashmidus/dotomo/3e258d81cec55d638e3c9d1ee508b9eeea45937c/ios/todoapp/Images.xcassets/SplashScreenLogo.imageset/image.png -------------------------------------------------------------------------------- /ios/todoapp/Images.xcassets/SplashScreenLogo.imageset/image@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arashmidus/dotomo/3e258d81cec55d638e3c9d1ee508b9eeea45937c/ios/todoapp/Images.xcassets/SplashScreenLogo.imageset/image@2x.png -------------------------------------------------------------------------------- /ios/todoapp/Images.xcassets/SplashScreenLogo.imageset/image@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arashmidus/dotomo/3e258d81cec55d638e3c9d1ee508b9eeea45937c/ios/todoapp/Images.xcassets/SplashScreenLogo.imageset/image@3x.png -------------------------------------------------------------------------------- /ios/todoapp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | todo-app 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 21 | CFBundleShortVersionString 22 | 1.0.0 23 | CFBundleSignature 24 | ???? 25 | CFBundleURLTypes 26 | 27 | 28 | CFBundleURLSchemes 29 | 30 | com.yourusername.todoapp 31 | 32 | 33 | 34 | CFBundleVersion 35 | 1 36 | LSMinimumSystemVersion 37 | 12.0 38 | LSRequiresIPhoneOS 39 | 40 | NSAppTransportSecurity 41 | 42 | NSAllowsArbitraryLoads 43 | 44 | NSAllowsLocalNetworking 45 | 46 | 47 | NSFaceIDUsageDescription 48 | Allow $(PRODUCT_NAME) to use Face ID to authenticate. 49 | UILaunchStoryboardName 50 | SplashScreen 51 | UIRequiredDeviceCapabilities 52 | 53 | arm64 54 | 55 | UIRequiresFullScreen 56 | 57 | UIStatusBarStyle 58 | UIStatusBarStyleDefault 59 | UISupportedInterfaceOrientations 60 | 61 | UIInterfaceOrientationPortrait 62 | UIInterfaceOrientationPortraitUpsideDown 63 | 64 | UISupportedInterfaceOrientations~ipad 65 | 66 | UIInterfaceOrientationPortrait 67 | UIInterfaceOrientationPortraitUpsideDown 68 | UIInterfaceOrientationLandscapeLeft 69 | UIInterfaceOrientationLandscapeRight 70 | 71 | UIUserInterfaceStyle 72 | Light 73 | UIViewControllerBasedStatusBarAppearance 74 | 75 | 76 | -------------------------------------------------------------------------------- /ios/todoapp/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyAccessedAPITypes 6 | 7 | 8 | NSPrivacyAccessedAPIType 9 | NSPrivacyAccessedAPICategoryFileTimestamp 10 | NSPrivacyAccessedAPITypeReasons 11 | 12 | C617.1 13 | 0A2A.1 14 | 3B52.1 15 | 16 | 17 | 18 | NSPrivacyAccessedAPIType 19 | NSPrivacyAccessedAPICategoryUserDefaults 20 | NSPrivacyAccessedAPITypeReasons 21 | 22 | CA92.1 23 | 24 | 25 | 26 | NSPrivacyAccessedAPIType 27 | NSPrivacyAccessedAPICategorySystemBootTime 28 | NSPrivacyAccessedAPITypeReasons 29 | 30 | 35F9.1 31 | 32 | 33 | 34 | NSPrivacyAccessedAPIType 35 | NSPrivacyAccessedAPICategoryDiskSpace 36 | NSPrivacyAccessedAPITypeReasons 37 | 38 | E174.1 39 | 85F4.1 40 | 41 | 42 | 43 | NSPrivacyCollectedDataTypes 44 | 45 | NSPrivacyTracking 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /ios/todoapp/SplashScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /ios/todoapp/Supporting/Expo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | EXUpdatesCheckOnLaunch 6 | ALWAYS 7 | EXUpdatesEnabled 8 | 9 | EXUpdatesLaunchWaitMs 10 | 0 11 | 12 | -------------------------------------------------------------------------------- /ios/todoapp/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char * argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /ios/todoapp/noop-file.swift: -------------------------------------------------------------------------------- 1 | // 2 | // @generated 3 | // A blank Swift file must be created for native modules with Swift files to work correctly. 4 | // 5 | -------------------------------------------------------------------------------- /ios/todoapp/todoapp-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | -------------------------------------------------------------------------------- /ios/todoapp/todoapp.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-app", 3 | "version": "1.0.0", 4 | "main": "index.ts", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo run:android", 8 | "ios": "expo run:ios", 9 | "web": "expo start --web" 10 | }, 11 | "dependencies": { 12 | "@react-native-async-storage/async-storage": "1.23.1", 13 | "@react-native-community/datetimepicker": "8.2.0", 14 | "@react-native-picker/picker": "^2.9.0", 15 | "@react-native-voice/voice": "^3.2.4", 16 | "@react-navigation/bottom-tabs": "^7.2.0", 17 | "@react-navigation/native": "^7.0.14", 18 | "@react-navigation/native-stack": "^7.2.0", 19 | "@react-navigation/stack": "^7.1.1", 20 | "date-fns": "^4.1.0", 21 | "expo": "~52.0.20", 22 | "expo-blur": "~14.0.1", 23 | "expo-constants": "~17.0.3", 24 | "expo-device": "~7.0.1", 25 | "expo-env": "^1.1.1", 26 | "expo-file-system": "~18.0.6", 27 | "expo-gl": "~15.0.2", 28 | "expo-haptics": "~14.0.0", 29 | "expo-intent-launcher": "~12.0.1", 30 | "expo-linear-gradient": "~14.0.1", 31 | "expo-local-authentication": "~15.0.1", 32 | "expo-notifications": "~0.29.11", 33 | "expo-router": "~4.0.15", 34 | "expo-speech": "~13.0.0", 35 | "expo-sqlite": "^15.0.4", 36 | "expo-status-bar": "~2.0.0", 37 | "gl-react": "^5.2.0", 38 | "gl-react-native": "^5.2.1", 39 | "react": "18.3.1", 40 | "react-native": "0.76.5", 41 | "react-native-chart-kit": "^6.12.0", 42 | "react-native-gesture-handler": "~2.20.2", 43 | "react-native-gifted-charts": "^1.4.49", 44 | "react-native-purchases": "^8.5.0", 45 | "react-native-reanimated": "~3.16.1", 46 | "react-native-safe-area-context": "4.12.0", 47 | "react-native-screens": "^4.4.0", 48 | "react-native-svg": "15.8.0", 49 | "react-native-webgl": "^0.8.0", 50 | "zod": "^3.24.1" 51 | }, 52 | "devDependencies": { 53 | "@babel/core": "^7.25.2", 54 | "@types/react": "~18.3.12", 55 | "@types/react-native": "^0.72.8", 56 | "typescript": "^5.3.3" 57 | }, 58 | "private": true 59 | } 60 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, Text, StyleSheet, Pressable } from 'react-native'; 3 | import { MaterialIcons } from '@expo/vector-icons'; 4 | import { LoggingService } from '../services/LoggingService'; 5 | 6 | interface ErrorBoundaryState { 7 | hasError: boolean; 8 | error: Error | null; 9 | } 10 | 11 | interface ErrorBoundaryProps { 12 | children: React.ReactNode; 13 | } 14 | 15 | export class ErrorBoundary extends React.Component { 16 | constructor(props: ErrorBoundaryProps) { 17 | super(props); 18 | this.state = { hasError: false, error: null }; 19 | } 20 | 21 | static getDerivedStateFromError(error: Error): ErrorBoundaryState { 22 | return { hasError: true, error }; 23 | } 24 | 25 | componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { 26 | LoggingService.error('App Error', { error, errorInfo }); 27 | } 28 | 29 | handleReset = () => { 30 | this.setState({ hasError: false, error: null }); 31 | }; 32 | 33 | render() { 34 | if (this.state.hasError) { 35 | return ( 36 | 37 | 38 | Oops! Something went wrong 39 | 40 | {this.state.error?.message || 'An unexpected error occurred'} 41 | 42 | 43 | Try Again 44 | 45 | 46 | ); 47 | } 48 | 49 | return this.props.children; 50 | } 51 | } 52 | 53 | const styles = StyleSheet.create({ 54 | container: { 55 | flex: 1, 56 | justifyContent: 'center', 57 | alignItems: 'center', 58 | padding: 16, 59 | backgroundColor: '#fff', 60 | }, 61 | title: { 62 | fontSize: 24, 63 | fontWeight: 'bold', 64 | marginTop: 16, 65 | marginBottom: 8, 66 | }, 67 | subtitle: { 68 | fontSize: 16, 69 | textAlign: 'center', 70 | color: '#666', 71 | marginBottom: 24, 72 | }, 73 | button: { 74 | backgroundColor: '#007AFF', 75 | paddingHorizontal: 24, 76 | paddingVertical: 12, 77 | borderRadius: 8, 78 | }, 79 | buttonText: { 80 | color: '#fff', 81 | fontSize: 16, 82 | fontWeight: '600', 83 | }, 84 | }); -------------------------------------------------------------------------------- /src/components/ErrorDisplay.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, Text, StyleSheet } from 'react-native'; 3 | 4 | interface ErrorDisplayProps { 5 | error: Error; 6 | } 7 | 8 | export function ErrorDisplay({ error }: ErrorDisplayProps) { 9 | return ( 10 | 11 | Error: {error.message} 12 | 13 | ); 14 | } 15 | 16 | const styles = StyleSheet.create({ 17 | container: { 18 | flex: 1, 19 | justifyContent: 'center', 20 | alignItems: 'center', 21 | padding: 20, 22 | }, 23 | errorText: { 24 | color: 'red', 25 | fontSize: 16, 26 | }, 27 | }); -------------------------------------------------------------------------------- /src/components/ErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | import { Text, StyleSheet, Pressable } from 'react-native'; 2 | 3 | interface ErrorMessageProps { 4 | message: string; 5 | onDismiss: () => void; 6 | } 7 | 8 | export function ErrorMessage({ message, onDismiss }: ErrorMessageProps) { 9 | return ( 10 | 11 | {message} 12 | 13 | ); 14 | } 15 | 16 | const styles = StyleSheet.create({ 17 | container: { 18 | backgroundColor: '#ff3b30', 19 | padding: 16, 20 | margin: 16, 21 | borderRadius: 8, 22 | }, 23 | text: { 24 | color: '#fff', 25 | fontSize: 14, 26 | textAlign: 'center', 27 | }, 28 | }); -------------------------------------------------------------------------------- /src/components/LoadingSpinner.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ActivityIndicator, StyleSheet, View } from 'react-native'; 3 | 4 | export function LoadingSpinner() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | 12 | const styles = StyleSheet.create({ 13 | container: { 14 | flex: 1, 15 | justifyContent: 'center', 16 | alignItems: 'center', 17 | }, 18 | }); -------------------------------------------------------------------------------- /src/components/MainContent.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text } from 'react-native'; 2 | 3 | export function MainContent() { 4 | return ( 5 | 6 | Your Todo App Content Here 7 | 8 | ); 9 | } -------------------------------------------------------------------------------- /src/components/SecureView.tsx: -------------------------------------------------------------------------------- 1 | import { View } from 'react-native'; 2 | import { useAuth } from '../contexts/AuthContext'; 3 | import { AuthScreen } from '../screens/AuthScreen'; 4 | 5 | interface SecureViewProps { 6 | children: React.ReactNode; 7 | } 8 | 9 | export function SecureView({ children }: SecureViewProps) { 10 | const { isAuthenticated } = useAuth(); 11 | 12 | if (!isAuthenticated) { 13 | return ; 14 | } 15 | 16 | return <>{children}; 17 | } -------------------------------------------------------------------------------- /src/components/liquid/LiquidShader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet } from 'react-native'; 3 | import { GLView } from 'expo-gl'; 4 | import { SharedValue, runOnUI } from 'react-native-reanimated'; 5 | 6 | interface LiquidShaderProps { 7 | progress: SharedValue; 8 | sourcePosition: { 9 | x: number; 10 | y: number; 11 | }; 12 | } 13 | 14 | function render(gl: WebGL2RenderingContext, progress: number, sourcePosition: { x: number; y: number }) { 15 | 'worklet'; 16 | 17 | // Create vertex shader 18 | const vert = gl.createShader(gl.VERTEX_SHADER); 19 | if (!vert) return; 20 | 21 | gl.shaderSource(vert, ` 22 | attribute vec4 position; 23 | varying vec2 uv; 24 | void main() { 25 | uv = position.xy * 0.5 + 0.5; 26 | gl_Position = position; 27 | } 28 | `); 29 | gl.compileShader(vert); 30 | 31 | // Create fragment shader 32 | const frag = gl.createShader(gl.FRAGMENT_SHADER); 33 | if (!frag) return; 34 | 35 | gl.shaderSource(frag, ` 36 | precision highp float; 37 | varying vec2 uv; 38 | uniform float progress; 39 | uniform vec2 source; 40 | 41 | void main() { 42 | vec2 p = uv - source; 43 | float d = length(p); 44 | 45 | float r = mix(0.0, 0.3, progress); 46 | float blob = smoothstep(r + 0.01, r, d); 47 | 48 | vec2 dir = normalize(vec2(0.5, 0.5) - source); 49 | float angle = atan(p.y, p.x) - atan(dir.y, dir.x); 50 | float tentacles = smoothstep(0.1, 0.0, abs(sin(angle * 8.0))) * 51 | smoothstep(r + 0.3, r - 0.1, d) * progress; 52 | 53 | float alpha = max(blob, tentacles) * progress; 54 | gl_FragColor = vec4(0.1, 0.1, 0.1, alpha); 55 | } 56 | `); 57 | gl.compileShader(frag); 58 | 59 | // Create program 60 | const program = gl.createProgram(); 61 | if (!program) return; 62 | 63 | gl.attachShader(program, vert); 64 | gl.attachShader(program, frag); 65 | gl.linkProgram(program); 66 | gl.useProgram(program); 67 | 68 | // Set up geometry 69 | const vertices = new Float32Array([ 70 | -1, -1, 71 | 1, -1, 72 | -1, 1, 73 | 1, 1, 74 | ]); 75 | 76 | const buffer = gl.createBuffer(); 77 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer); 78 | gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); 79 | 80 | const position = gl.getAttribLocation(program, 'position'); 81 | gl.enableVertexAttribArray(position); 82 | gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0); 83 | 84 | const progressLoc = gl.getUniformLocation(program, 'progress'); 85 | const sourceLoc = gl.getUniformLocation(program, 'source'); 86 | 87 | gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); 88 | gl.clearColor(0, 0, 0, 0); 89 | gl.clear(gl.COLOR_BUFFER_BIT); 90 | 91 | gl.uniform1f(progressLoc, progress); 92 | gl.uniform2f(sourceLoc, 93 | sourcePosition.x / gl.drawingBufferWidth, 94 | 1 - (sourcePosition.y / gl.drawingBufferHeight) 95 | ); 96 | 97 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 98 | gl.endFrameEXP(); 99 | } 100 | 101 | export function LiquidShaderComponent({ progress, sourcePosition }: LiquidShaderProps) { 102 | const onContextCreate = (gl: WebGL2RenderingContext) => { 103 | runOnUI((contextId: number) => { 104 | 'worklet'; 105 | const glContext = GLView.getWorkletContext(contextId); 106 | render(glContext, progress.value, sourcePosition); 107 | })(gl.contextId); 108 | }; 109 | 110 | return ( 111 | 116 | ); 117 | } 118 | 119 | const styles = StyleSheet.create({ 120 | container: { 121 | ...StyleSheet.absoluteFillObject, 122 | }, 123 | }); -------------------------------------------------------------------------------- /src/components/shaders/DarkModeShader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet } from 'react-native'; 3 | import { GLView } from 'expo-gl'; 4 | 5 | const FRAGMENT_SHADER = ` 6 | precision highp float; 7 | varying vec2 vTexCoord; 8 | uniform float uTime; 9 | 10 | // Convert hex colors to vec3 11 | // #290505 -> rgb(41, 5, 5) -> vec3(0.161, 0.020, 0.020) 12 | // #040927 -> rgb(4, 9, 39) -> vec3(0.016, 0.035, 0.153) 13 | 14 | float noise(vec2 st) { 15 | return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123); 16 | } 17 | 18 | void main() { 19 | vec2 uv = vTexCoord; 20 | 21 | // New base colors from hex codes 22 | vec3 colorA = vec3(0.161, 0.020, 0.020); // #290505 23 | vec3 colorB = vec3(0.016, 0.035, 0.153); // #040927 24 | 25 | // Enhanced gradient with new colors 26 | float gradient = smoothstep(0.0, 1.0, uv.y); 27 | vec3 gradientColor = mix(colorA, colorB, gradient + sin(uTime * 0.2) * 0.1); 28 | 29 | // Add animated waves 30 | float waves = sin(uv.x * 6.0 + uTime) * sin(uv.y * 4.0 - uTime * 0.5) * 0.015; 31 | 32 | // Add subtle noise pattern 33 | float noisePattern = noise(uv * 2.0 + uTime * 0.1) * 0.02; 34 | 35 | // Animated glow points 36 | vec2 center1 = vec2(0.3 + sin(uTime * 0.5) * 0.1, 0.7 + cos(uTime * 0.3) * 0.1); 37 | vec2 center2 = vec2(0.7 + cos(uTime * 0.4) * 0.1, 0.3 + sin(uTime * 0.6) * 0.1); 38 | float glow1 = 0.02 / length(uv - center1) * 0.015; 39 | float glow2 = 0.02 / length(uv - center2) * 0.015; 40 | 41 | // Combine effects 42 | vec3 finalColor = gradientColor; 43 | finalColor += mix(colorA, colorB, 0.5) * waves; 44 | finalColor += mix(colorA, colorB, 0.3) * noisePattern; 45 | finalColor += vec3(0.3, 0.1, 0.1) * glow1; // Reddish glow 46 | finalColor += vec3(0.1, 0.1, 0.3) * glow2; // Bluish glow 47 | 48 | // Enhanced vignette 49 | float vignette = length(uv - 0.5) * 1.2; 50 | finalColor *= 1.0 - vignette * 0.7; 51 | 52 | // Subtle color correction 53 | finalColor = pow(finalColor, vec3(0.95)); 54 | 55 | gl_FragColor = vec4(finalColor, 1.0); 56 | } 57 | `; 58 | 59 | const VERTEX_SHADER = ` 60 | attribute vec4 position; 61 | varying vec2 vTexCoord; 62 | 63 | void main() { 64 | vTexCoord = position.xy * 0.5 + 0.5; 65 | gl_Position = position; 66 | } 67 | `; 68 | 69 | export function DarkModeShader() { 70 | const onContextCreate = (gl: WebGLRenderingContext) => { 71 | // Create shaders 72 | const vertShader = gl.createShader(gl.VERTEX_SHADER)!; 73 | gl.shaderSource(vertShader, VERTEX_SHADER); 74 | gl.compileShader(vertShader); 75 | 76 | const fragShader = gl.createShader(gl.FRAGMENT_SHADER)!; 77 | gl.shaderSource(fragShader, FRAGMENT_SHADER); 78 | gl.compileShader(fragShader); 79 | 80 | // Create program 81 | const program = gl.createProgram()!; 82 | gl.attachShader(program, vertShader); 83 | gl.attachShader(program, fragShader); 84 | gl.linkProgram(program); 85 | gl.useProgram(program); 86 | 87 | // Set up geometry 88 | const vertices = new Float32Array([ 89 | -1.0, -2.0, // Bottom left - extended even further down 90 | 1.0, -2.0, // Bottom right - extended even further down 91 | -1.0, 1.0, // Top left 92 | 1.0, 1.0, // Top right 93 | ]); 94 | 95 | const buffer = gl.createBuffer(); 96 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer); 97 | gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); 98 | 99 | const position = gl.getAttribLocation(program, 'position'); 100 | gl.enableVertexAttribArray(position); 101 | gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0); 102 | 103 | const timeLocation = gl.getUniformLocation(program, 'uTime'); 104 | let startTime = Date.now(); 105 | 106 | function render() { 107 | const time = (Date.now() - startTime) * 0.001; 108 | gl.uniform1f(timeLocation, time); 109 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 110 | gl.endFrameEXP(); 111 | requestAnimationFrame(render); 112 | } 113 | 114 | render(); 115 | }; 116 | 117 | return ( 118 | 128 | ); 129 | } -------------------------------------------------------------------------------- /src/components/todo/SmartTodoInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react'; 2 | import { useState, useRef, useEffect } from 'react'; 3 | import { 4 | View, 5 | TextInput, 6 | StyleSheet, 7 | Pressable, 8 | Animated, 9 | LayoutAnimation, 10 | Platform, 11 | UIManager, 12 | Keyboard 13 | } from 'react-native'; 14 | import { MaterialIcons } from '@expo/vector-icons'; 15 | import { ParsedTodo } from '../../types/nlp'; 16 | 17 | if (Platform.OS === 'android') { 18 | UIManager.setLayoutAnimationEnabledExperimental?.(true); 19 | } 20 | 21 | const PLACEHOLDER_TEXTS = [ 22 | "What's on your mind?", 23 | "Add a new task...", 24 | "Write your next goal...", 25 | "What needs to be done?", 26 | ]; 27 | 28 | interface SmartTodoInputProps { 29 | onSubmit: (todo: ParsedTodo) => void; 30 | } 31 | 32 | export const SmartTodoInput = forwardRef((props: SmartTodoInputProps, ref) => { 33 | const [input, setInput] = useState(''); 34 | const [isFocused, setIsFocused] = useState(false); 35 | const [placeholder, setPlaceholder] = useState(PLACEHOLDER_TEXTS[0]); 36 | const scaleAnim = useRef(new Animated.Value(1)).current; 37 | const slideAnim = useRef(new Animated.Value(50)).current; 38 | 39 | useEffect(() => { 40 | const interval = setInterval(() => { 41 | setPlaceholder(prev => { 42 | const currentIndex = PLACEHOLDER_TEXTS.indexOf(prev); 43 | return PLACEHOLDER_TEXTS[(currentIndex + 1) % PLACEHOLDER_TEXTS.length]; 44 | }); 45 | }, 5000); 46 | return () => clearInterval(interval); 47 | }, []); 48 | 49 | useEffect(() => { 50 | Animated.spring(slideAnim, { 51 | toValue: 0, 52 | useNativeDriver: true, 53 | tension: 50, 54 | friction: 7 55 | }).start(); 56 | }, []); 57 | 58 | function handleSubmit() { 59 | if (!input.trim()) return; 60 | 61 | const parsedTodo: ParsedTodo = { 62 | title: input, 63 | description: '', 64 | tags: [], 65 | priority: 'medium', 66 | }; 67 | 68 | props.onSubmit(parsedTodo); 69 | setInput(''); 70 | Keyboard.dismiss(); 71 | } 72 | 73 | return ( 74 | 86 | Keyboard.dismiss()} 97 | onFocus={() => setIsFocused(true)} 98 | onBlur={() => setIsFocused(false)} 99 | /> 100 | 105 | 110 | 111 | 112 | ); 113 | }); 114 | 115 | const styles = StyleSheet.create({ 116 | container: { 117 | flexDirection: 'row', 118 | margin: 16, 119 | padding: 12, 120 | alignItems: 'center', 121 | backgroundColor: '#fff', 122 | borderRadius: 12, 123 | shadowColor: '#000', 124 | shadowOffset: { width: 0, height: 2 }, 125 | shadowOpacity: 0.1, 126 | shadowRadius: 4, 127 | elevation: 3, 128 | }, 129 | containerFocused: { 130 | shadowOpacity: 0.15, 131 | shadowRadius: 8, 132 | elevation: 5, 133 | }, 134 | input: { 135 | flex: 1, 136 | fontSize: 16, 137 | minHeight: 40, 138 | maxHeight: 120, 139 | paddingVertical: 8, 140 | paddingHorizontal: 12, 141 | backgroundColor: '#f8f8f8', 142 | borderRadius: 8, 143 | marginRight: 12, 144 | }, 145 | inputFocused: { 146 | backgroundColor: '#fff', 147 | borderWidth: 2, 148 | borderColor: '#007AFF20', 149 | }, 150 | button: { 151 | padding: 8, 152 | borderRadius: 20, 153 | backgroundColor: '#f8f8f8', 154 | }, 155 | buttonFocused: { 156 | backgroundColor: '#fff', 157 | }, 158 | buttonDisabled: { 159 | opacity: 0.5, 160 | }, 161 | }); -------------------------------------------------------------------------------- /src/components/todo/TodoItem.tsx: -------------------------------------------------------------------------------- 1 | import { Pressable, Text, StyleSheet, View } from 'react-native'; 2 | import { MaterialIcons } from '@expo/vector-icons'; 3 | import { format } from 'date-fns'; 4 | import { Todo } from '../../types'; 5 | import Animated, { 6 | useAnimatedStyle, 7 | withTiming, 8 | interpolateColor 9 | } from 'react-native-reanimated'; 10 | import { useEffect, useState } from 'react'; 11 | import { differenceInHours, differenceInMinutes, isAfter, addHours } from 'date-fns'; 12 | 13 | interface TodoItemProps { 14 | todo: Todo & { createdAt: Date }; 15 | onToggle: (id: string) => void; 16 | onPress: (todo: Todo) => void; 17 | onExpire?: (id: string) => void; 18 | } 19 | 20 | export function TodoItem({ todo, onToggle, onPress, onExpire }: TodoItemProps) { 21 | const [timeRemaining, setTimeRemaining] = useState(''); 22 | 23 | useEffect(() => { 24 | console.log('Todo created at:', todo.createdAt); 25 | const createdAtDate = new Date(todo.createdAt); 26 | const expiryTime = addHours(createdAtDate, 18); 27 | 28 | const updateTimer = () => { 29 | const now = new Date(); 30 | if (isAfter(now, expiryTime)) { 31 | onExpire?.(todo.id); 32 | return; 33 | } 34 | 35 | const hoursLeft = Math.max(0, differenceInHours(expiryTime, now)); 36 | const minutesLeft = Math.max(0, differenceInMinutes(expiryTime, now) % 60); 37 | setTimeRemaining(`${hoursLeft}h ${minutesLeft}m remaining`); 38 | }; 39 | 40 | // Initial update 41 | updateTimer(); 42 | 43 | // Update every minute 44 | const timer = setInterval(updateTimer, 60000); 45 | 46 | return () => clearInterval(timer); 47 | }, [todo.createdAt, todo.id, onExpire]); 48 | 49 | const animatedStyle = useAnimatedStyle(() => { 50 | return { 51 | backgroundColor: withTiming( 52 | todo.completed ? 'rgba(0, 122, 255, 0.1)' : 'white' 53 | ), 54 | }; 55 | }); 56 | 57 | const textStyle = useAnimatedStyle(() => { 58 | return { 59 | color: withTiming( 60 | todo.completed ? '#666' : '#000' 61 | ), 62 | textDecorationLine: todo.completed ? 'line-through' : 'none', 63 | }; 64 | }); 65 | 66 | const isOverdue = !todo.completed && new Date() > todo.dueDate; 67 | 68 | return ( 69 | 70 | onToggle(todo.id)} 73 | hitSlop={8} 74 | > 75 | 80 | 81 | 82 | onPress(todo)} 85 | > 86 | 90 | {todo.title} 91 | 92 | 93 | {timeRemaining} 94 | 95 | 96 | {todo.description ? ( 97 | 98 | {todo.description} 99 | 100 | ) : null} 101 | 102 | 108 | {format(todo.dueDate, 'MMM d, yyyy')} 109 | 110 | 111 | 112 | 113 | 119 | 120 | ); 121 | } 122 | 123 | const styles = StyleSheet.create({ 124 | container: { 125 | flexDirection: 'row', 126 | alignItems: 'center', 127 | padding: 12, 128 | borderRadius: 8, 129 | marginBottom: 8, 130 | backgroundColor: 'white', 131 | shadowColor: '#000', 132 | shadowOffset: { width: 0, height: 1 }, 133 | shadowOpacity: 0.1, 134 | shadowRadius: 1, 135 | elevation: 2, 136 | }, 137 | checkbox: { 138 | marginRight: 12, 139 | }, 140 | content: { 141 | flex: 1, 142 | }, 143 | title: { 144 | fontSize: 16, 145 | fontWeight: '500', 146 | marginBottom: 4, 147 | }, 148 | details: { 149 | flexDirection: 'row', 150 | alignItems: 'center', 151 | }, 152 | description: { 153 | fontSize: 14, 154 | color: '#666', 155 | flex: 1, 156 | marginRight: 8, 157 | }, 158 | date: { 159 | fontSize: 12, 160 | color: '#666', 161 | }, 162 | overdue: { 163 | color: '#ff3b30', 164 | }, 165 | chevron: { 166 | marginLeft: 8, 167 | }, 168 | timer: { 169 | fontSize: 12, 170 | color: '#FF5722', 171 | marginBottom: 4, 172 | }, 173 | }); -------------------------------------------------------------------------------- /src/components/todo/constants.ts: -------------------------------------------------------------------------------- 1 | import { Dimensions } from 'react-native'; 2 | 3 | const { width: SCREEN_WIDTH } = Dimensions.get('window'); 4 | 5 | export const CARD_WIDTH = SCREEN_WIDTH * 0.9; 6 | export const CARD_HEIGHT = CARD_WIDTH * 1.4; 7 | export const SWIPE_THRESHOLD = SCREEN_WIDTH * 0.3; 8 | export const MAX_VISIBLE_CARDS = 3; 9 | 10 | export const SPRING_CONFIG = { 11 | damping: 20, 12 | mass: 0.005, 13 | stiffness: 10, 14 | overshootClamping: false, 15 | restSpeedThreshold: 0.01, 16 | restDisplacementThreshold: 0.01, 17 | }; 18 | 19 | export const CARD_SHADER = ` 20 | precision highp float; 21 | // ... your shader code ... 22 | `; -------------------------------------------------------------------------------- /src/contexts/AuthContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | 3 | interface AuthContextType { 4 | isAuthenticated: boolean; 5 | } 6 | 7 | const AuthContext = createContext(undefined); 8 | 9 | export function AuthProvider({ children }: { children: React.ReactNode }) { 10 | // Simplified to always return true since we're removing authentication 11 | const isAuthenticated = true; 12 | 13 | return ( 14 | 15 | {children} 16 | 17 | ); 18 | } 19 | 20 | export function useAuth() { 21 | const context = useContext(AuthContext); 22 | if (!context) { 23 | throw new Error('useAuth must be used within an AuthProvider'); 24 | } 25 | return context; 26 | } -------------------------------------------------------------------------------- /src/contexts/SettingsContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useState, useEffect } from 'react'; 2 | import AsyncStorage from '@react-native-async-storage/async-storage'; 3 | 4 | interface NotificationPreferences { 5 | enabled: boolean; 6 | reminderTiming: number; 7 | sound: boolean; 8 | vibration: boolean; 9 | } 10 | 11 | export interface AppSettings { 12 | notifications: NotificationPreferences; 13 | theme: 'light' | 'dark' | 'system'; 14 | defaultPriority: 'low' | 'medium' | 'high'; 15 | biometricEnabled: boolean; 16 | biometricTimeout: number; 17 | appLockEnabled: boolean; 18 | useFaceId: boolean; 19 | wakeUpTime: string; 20 | bedTime: string; 21 | workStartTime: string; 22 | workEndTime: string; 23 | } 24 | 25 | const DEFAULT_SETTINGS: AppSettings = { 26 | notifications: { 27 | enabled: true, 28 | reminderTiming: 1, 29 | sound: true, 30 | vibration: true, 31 | }, 32 | theme: 'system', 33 | defaultPriority: 'medium', 34 | biometricEnabled: false, 35 | biometricTimeout: 0, 36 | appLockEnabled: false, 37 | useFaceId: false, 38 | wakeUpTime: '07:00', 39 | bedTime: '22:00', 40 | workStartTime: '09:00', 41 | workEndTime: '17:00', 42 | }; 43 | 44 | interface SettingsContextType { 45 | settings: AppSettings; 46 | updateSettings: (newSettings: Partial) => Promise; 47 | isLoading: boolean; 48 | } 49 | 50 | const SettingsContext = createContext(undefined); 51 | 52 | export function SettingsProvider({ children }: { children: React.ReactNode }) { 53 | const [settings, setSettings] = useState(DEFAULT_SETTINGS); 54 | const [isLoading, setIsLoading] = useState(true); 55 | 56 | useEffect(() => { 57 | loadSettings(); 58 | }, []); 59 | 60 | async function loadSettings() { 61 | try { 62 | console.log('[SettingsContext] Loading settings...'); 63 | const storedSettings = await AsyncStorage.getItem('app_settings'); 64 | console.log('[SettingsContext] Stored settings:', storedSettings); 65 | 66 | if (storedSettings) { 67 | const parsedSettings = JSON.parse(storedSettings); 68 | console.log('[SettingsContext] Merged settings:', { 69 | ...DEFAULT_SETTINGS, 70 | ...parsedSettings 71 | }); 72 | setSettings({ ...DEFAULT_SETTINGS, ...parsedSettings }); 73 | } else { 74 | console.log('[SettingsContext] No stored settings, using defaults'); 75 | } 76 | } catch (error) { 77 | console.error('[SettingsContext] Failed to load settings:', error); 78 | } finally { 79 | setIsLoading(false); 80 | } 81 | } 82 | 83 | async function updateSettings(newSettings: Partial) { 84 | const updatedSettings = { ...settings, ...newSettings }; 85 | setSettings(updatedSettings); 86 | 87 | try { 88 | await AsyncStorage.setItem('app_settings', JSON.stringify(updatedSettings)); 89 | } catch (error) { 90 | setSettings(settings); 91 | console.error('[SettingsContext] Failed to save settings:', error); 92 | throw error; 93 | } 94 | } 95 | 96 | return ( 97 | 98 | {children} 99 | 100 | ); 101 | } 102 | 103 | export function useSettings() { 104 | const context = useContext(SettingsContext); 105 | if (!context) { 106 | throw new Error('useSettings must be used within a SettingsProvider'); 107 | } 108 | return context; 109 | } -------------------------------------------------------------------------------- /src/contexts/ThemeContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useState, useEffect } from 'react'; 2 | import { useColorScheme } from 'react-native'; 3 | 4 | interface ThemeColors { 5 | background: string; 6 | surface: string; 7 | text: string; 8 | textSecondary: string; 9 | primary: string; 10 | accent: string; 11 | } 12 | 13 | const darkTheme: ThemeColors = { 14 | background: '#111827', 15 | surface: 'rgba(30, 32, 35, 0.8)', 16 | text: '#ffffff', 17 | textSecondary: '#9ca3af', 18 | primary: '#3b82f6', 19 | accent: '#60a5fa', 20 | }; 21 | 22 | const ThemeContext = createContext(darkTheme); 23 | 24 | export function ThemeProvider({ children }: { children: React.ReactNode }) { 25 | const colorScheme = useColorScheme(); 26 | const [theme, setTheme] = useState(darkTheme); 27 | 28 | return ( 29 | 30 | {children} 31 | 32 | ); 33 | } 34 | 35 | export function useTheme() { 36 | return useContext(ThemeContext); 37 | } -------------------------------------------------------------------------------- /src/contexts/TodoContext.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SQLiteProvider, useSQLiteContext } from 'expo-sqlite'; 3 | import { createContext, useContext, useReducer, useEffect, useCallback, useState } from 'react'; 4 | import { z } from 'zod'; 5 | import { LoadingSpinner } from '../components/LoadingSpinner'; 6 | import AsyncStorage from '@react-native-async-storage/async-storage'; 7 | import { generateReminder, generateTaskBreakdown, generateTimingRecommendation } from '../services/LLMService'; 8 | import { useSettings } from './SettingsContext'; 9 | 10 | function generateId(): string { 11 | return Math.random().toString(36).substring(2) + Date.now().toString(36); 12 | } 13 | 14 | const TodoSchema = z.object({ 15 | id: z.string(), 16 | title: z.string(), 17 | description: z.string().optional(), 18 | dueDate: z.date(), 19 | completed: z.boolean(), 20 | tags: z.array(z.string()), 21 | createdAt: z.date(), 22 | completedAt: z.date().optional(), 23 | taskList: z.array(z.string()).optional(), 24 | reminder: z.string().optional(), 25 | llmAnalysis: z.object({ 26 | recommendedTime: z.string(), 27 | reasoning: z.string(), 28 | confidence: z.number() 29 | }).optional(), 30 | }); 31 | 32 | type Todo = z.infer; 33 | 34 | interface TodoContextType { 35 | todos: Todo[]; 36 | addTodo: (todo: Omit) => Promise; 37 | toggleTodo: (id: string) => Promise; 38 | deleteTodo: (id: string) => Promise; 39 | completeTodo: (id: string) => void; 40 | updateTodo: (id: string, updates: Partial) => Promise; 41 | } 42 | 43 | export const TodoContext = React.createContext(undefined); 44 | 45 | type TodoAction = 46 | | { type: 'SET_TODOS'; payload: Todo[] } 47 | | { type: 'ADD_TODO'; payload: Todo } 48 | | { type: 'DELETE_TODO'; id: string } 49 | | { type: 'COMPLETE_TODO'; id: string } 50 | | { type: 'UPDATE_TODO'; id: string; updates: Partial }; 51 | 52 | function todoReducer(state: Todo[], action: TodoAction): Todo[] { 53 | switch (action.type) { 54 | case 'SET_TODOS': 55 | return action.payload; 56 | case 'ADD_TODO': 57 | return [...state, action.payload]; 58 | case 'DELETE_TODO': 59 | return state.filter(todo => todo.id !== action.id); 60 | case 'COMPLETE_TODO': 61 | return state.map(todo => 62 | todo.id === action.id 63 | ? { ...todo, completed: true } 64 | : todo 65 | ); 66 | case 'UPDATE_TODO': 67 | return state.map(todo => 68 | todo.id === action.id ? { ...todo, ...action.updates } : todo 69 | ); 70 | default: 71 | return state; 72 | } 73 | } 74 | 75 | export function TodoProvider({ children }: { children: React.ReactNode }) { 76 | const db = useSQLiteContext(); 77 | const { settings } = useSettings(); 78 | const [isInitialized, setIsInitialized] = useState(false); 79 | const [todos, dispatch] = useReducer(todoReducer, []); 80 | 81 | // Define all callbacks outside of any conditional blocks 82 | const completeTodo = useCallback(async (id: string) => { 83 | try { 84 | await db.runAsync( 85 | 'UPDATE todos SET completed = ? WHERE id = ?', 86 | [1, id] 87 | ); 88 | dispatch({ type: 'COMPLETE_TODO', id }); 89 | } catch (error) { 90 | console.error('Failed to complete todo:', error); 91 | } 92 | }, [db]); 93 | 94 | const addTodo = async (todoData: Omit) => { 95 | try { 96 | console.log('\n🎯 ==================== ADDING NEW TODO ===================='); 97 | console.log('📝 Todo Data:', todoData); 98 | console.log('⚙️ Current Settings:', settings); 99 | 100 | const timing = await generateTimingRecommendation(todoData, settings); 101 | console.log('🕒 LLM Timing Analysis:', timing); 102 | 103 | const newTodo = { 104 | ...todoData, 105 | id: generateId(), 106 | createdAt: new Date().toISOString(), 107 | completed: false, 108 | llmAnalysis: timing, 109 | }; 110 | console.log('🆕 Created Todo Base:', newTodo); 111 | 112 | // Generate content ONCE 113 | console.log('🤖 Generating AI Content...'); 114 | const [reminder, taskList] = await Promise.all([ 115 | generateReminder(newTodo), 116 | generateTaskBreakdown(newTodo) 117 | ]); 118 | console.log('✨ Generated Reminder:', reminder); 119 | console.log('📋 Generated Task List:', taskList); 120 | 121 | const completeNewTodo = { 122 | ...newTodo, 123 | taskList, 124 | reminder 125 | }; 126 | console.log('✅ Complete Todo Object:', completeNewTodo); 127 | 128 | // Save to AsyncStorage 129 | console.log('💾 Saving to Storage...'); 130 | const existingTodos = await AsyncStorage.getItem('todos'); 131 | const updatedTodos = existingTodos 132 | ? [...JSON.parse(existingTodos), completeNewTodo] 133 | : [completeNewTodo]; 134 | 135 | await AsyncStorage.setItem('todos', JSON.stringify(updatedTodos)); 136 | console.log('✅ Storage Updated Successfully'); 137 | 138 | // Update state 139 | dispatch({ type: 'ADD_TODO', payload: completeNewTodo }); 140 | console.log('🎉 Todo Added Successfully!'); 141 | console.log('=====================================================\n'); 142 | } catch (error) { 143 | console.error('❌ ERROR ADDING TODO ❌'); 144 | console.error('=================='); 145 | console.error(error); 146 | console.error('=================='); 147 | throw error; 148 | } 149 | }; 150 | 151 | const deleteTodo = useCallback(async (id: string) => { 152 | try { 153 | console.log('\n🗑️ ==================== DELETING TODO ===================='); 154 | console.log('🔑 Todo ID:', id); 155 | 156 | await db.runAsync('DELETE FROM todos WHERE id = ?', [id]); 157 | dispatch({ type: 'DELETE_TODO', id }); 158 | 159 | console.log('✅ Todo Deleted Successfully'); 160 | console.log('=====================================================\n'); 161 | } catch (error) { 162 | console.error('❌ ERROR DELETING TODO ❌'); 163 | console.error('=================='); 164 | console.error(error); 165 | console.error('=================='); 166 | } 167 | }, [db]); 168 | 169 | const updateTodo = async (id: string, updates: Partial) => { 170 | try { 171 | console.log('\n📝 ==================== UPDATING TODO ===================='); 172 | console.log('🔑 Todo ID:', id); 173 | console.log('🔄 Updates:', updates); 174 | 175 | dispatch({ type: 'UPDATE_TODO', id, updates }); 176 | 177 | const storedTodos = await AsyncStorage.getItem('todos'); 178 | if (storedTodos) { 179 | const parsedTodos = JSON.parse(storedTodos); 180 | const updatedTodos = parsedTodos.map(todo => 181 | todo.id === id ? { ...todo, ...updates } : todo 182 | ); 183 | await AsyncStorage.setItem('todos', JSON.stringify(updatedTodos)); 184 | console.log('💾 Storage Updated Successfully'); 185 | } 186 | 187 | console.log('✅ Todo Updated Successfully'); 188 | console.log('=====================================================\n'); 189 | } catch (error) { 190 | console.error('❌ ERROR UPDATING TODO ❌'); 191 | console.error('=================='); 192 | console.error(error); 193 | console.error('=================='); 194 | throw error; 195 | } 196 | }; 197 | 198 | useEffect(() => { 199 | const initializeApp = async () => { 200 | try { 201 | console.log('\n🔄 ==================== INITIALIZING APP ===================='); 202 | 203 | // First try AsyncStorage 204 | const storedTodos = await AsyncStorage.getItem('todos'); 205 | if (storedTodos) { 206 | const parsedTodos = JSON.parse(storedTodos); 207 | console.log('📱 Loaded from AsyncStorage:', parsedTodos.length, 'todos'); 208 | dispatch({ type: 'SET_TODOS', payload: parsedTodos }); 209 | setIsInitialized(true); 210 | return; // Exit if we loaded from AsyncStorage 211 | } 212 | 213 | // If no AsyncStorage data, try SQLite as fallback 214 | console.log('💽 No AsyncStorage data, checking SQLite...'); 215 | const result = await db.getAllAsync('SELECT * FROM todos'); 216 | const loadedTodos = result.map(row => ({ 217 | ...row, 218 | dueDate: new Date(row.dueDate), 219 | completed: Boolean(row.completed), 220 | tags: JSON.parse(row.tags || '[]') 221 | })); 222 | 223 | if (loadedTodos.length > 0) { 224 | console.log('📚 Loaded from SQLite:', loadedTodos.length, 'todos'); 225 | // Save to AsyncStorage for future use 226 | await AsyncStorage.setItem('todos', JSON.stringify(loadedTodos)); 227 | dispatch({ type: 'SET_TODOS', payload: loadedTodos }); 228 | } else { 229 | console.log('❌ No todos found in either storage'); 230 | } 231 | 232 | console.log('✅ Initialization Complete'); 233 | console.log('=====================================================\n'); 234 | setIsInitialized(true); 235 | } catch (error) { 236 | console.error('❌ INITIALIZATION ERROR ❌'); 237 | console.error(error); 238 | setIsInitialized(true); // Still set initialized to prevent hanging 239 | } 240 | }; 241 | 242 | initializeApp(); 243 | }, [db]); 244 | 245 | if (!isInitialized) { 246 | return ; 247 | } 248 | 249 | return ( 250 | 257 | {children} 258 | 259 | ); 260 | } 261 | 262 | // Wrap the TodoProvider with SQLiteProvider 263 | export function AppProvider({ children }: { children: React.ReactNode }) { 264 | return ( 265 | { 268 | await db.execAsync('PRAGMA journal_mode = WAL'); 269 | }} 270 | useSuspense 271 | > 272 | 273 | {children} 274 | 275 | 276 | ); 277 | } 278 | 279 | export function useTodos() { 280 | const context = useContext(TodoContext); 281 | if (!context) { 282 | throw new Error('useTodos must be used within a TodoProvider'); 283 | } 284 | return context; 285 | } -------------------------------------------------------------------------------- /src/hooks/useErrorHandler.ts: -------------------------------------------------------------------------------- 1 | import { useState, useCallback } from 'react'; 2 | import { LoggingService } from '../services/LoggingService'; 3 | 4 | export function useErrorHandler(componentName: string) { 5 | const [error, setError] = useState(null); 6 | 7 | const handleError = useCallback( 8 | async (error: Error, details?: any) => { 9 | await LoggingService.error(error, details, componentName); 10 | setError(error); 11 | }, 12 | [componentName] 13 | ); 14 | 15 | const clearError = useCallback(() => { 16 | setError(null); 17 | }, []); 18 | 19 | return { 20 | error, 21 | handleError, 22 | clearError, 23 | }; 24 | } -------------------------------------------------------------------------------- /src/hooks/useNotifications.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import { useTodos } from '../contexts/TodoContext'; 3 | import { useSettings } from '../contexts/SettingsContext'; 4 | import { NotificationService } from '../services/NotificationService'; 5 | 6 | export function useNotifications() { 7 | const { todos } = useTodos(); 8 | const { settings } = useSettings(); 9 | const previousTodos = useRef([]); 10 | 11 | useEffect(() => { 12 | if (!settings.notifications.enabled) return; 13 | 14 | // Only send notification for newly added todos 15 | const newTodos = todos.filter( 16 | todo => !previousTodos.current.find(pt => pt.id === todo.id) 17 | ); 18 | 19 | newTodos.forEach(todo => { 20 | if (!todo.completed) { 21 | NotificationService.scheduleNotification(todo, settings.notifications); 22 | } 23 | }); 24 | 25 | previousTodos.current = todos; 26 | }, [todos, settings.notifications]); 27 | } -------------------------------------------------------------------------------- /src/hooks/useVoiceRecognition.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import Voice from '@react-native-voice/voice'; 3 | 4 | export const useVoiceRecognition = () => { 5 | const [results, setResults] = useState([]); 6 | const [error, setError] = useState(''); 7 | 8 | useEffect(() => { 9 | Voice.onSpeechResults = (e) => { 10 | setResults(e.value ?? []); 11 | }; 12 | 13 | Voice.onSpeechError = (e) => { 14 | setError(e.error?.message ?? 'Unknown error'); 15 | }; 16 | 17 | return () => { 18 | Voice.destroy().then(Voice.removeAllListeners); 19 | }; 20 | }, []); 21 | 22 | return { results, error }; 23 | }; -------------------------------------------------------------------------------- /src/navigation/AppNavigator.tsx: -------------------------------------------------------------------------------- 1 | import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; 2 | import { createStackNavigator } from '@react-navigation/stack'; 3 | import { NavigationContainer } from '@react-navigation/native'; 4 | import { MaterialIcons } from '@expo/vector-icons'; 5 | import { TouchableOpacity, StatusBar } from 'react-native'; 6 | import { HomeScreen } from '../screens/HomeScreen'; 7 | import { AddTodoScreen } from '../screens/AddTodoScreen'; 8 | import { SettingsScreen } from '../screens/SettingsScreen'; 9 | import * as Haptics from 'expo-haptics'; 10 | import { DarkModeShader } from '../components/shaders/DarkModeShader'; 11 | import { View } from 'react-native'; 12 | import { TransitionPresets } from '@react-navigation/stack'; 13 | import { Dimensions } from 'react-native'; 14 | import { AnalyticsScreen } from '../screens/AnalyticsScreen'; 15 | import { createNativeStackNavigator } from '@react-navigation/native-stack'; 16 | 17 | const Tab = createBottomTabNavigator(); 18 | const Stack = createNativeStackNavigator(); 19 | 20 | function TabNavigator({ navigation }) { 21 | const handleAddPress = async () => { 22 | await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); 23 | navigation.navigate('AddTodoModal'); 24 | }; 25 | 26 | return ( 27 | 28 | 29 | null, 47 | tabBarStyle: { 48 | backgroundColor: 'transparent', 49 | borderTopWidth: 0, 50 | position: 'absolute', 51 | elevation: 0, 52 | height: 70, 53 | paddingBottom: 25, 54 | bottom: 0, 55 | left: 0, 56 | right: 0, 57 | margin: 0, 58 | padding: 0, 59 | position: 'absolute', 60 | zIndex: 1, 61 | }, 62 | tabBarBackground: () => null, 63 | }} 64 | > 65 | ( 70 | 71 | ), 72 | }} 73 | /> 74 | ( 79 | 80 | ), 81 | }} 82 | /> 83 | 84 | 107 | 108 | 109 | 110 | ); 111 | } 112 | 113 | export function AppNavigator() { 114 | const { height } = Dimensions.get('window'); 115 | 116 | return ( 117 | 118 | 119 | 120 | ({ 136 | cardStyle: { 137 | transform: [ 138 | { 139 | translateY: progress.interpolate({ 140 | inputRange: [0, 1], 141 | outputRange: [height * 0.5, 0], 142 | }), 143 | }, 144 | ], 145 | backgroundColor: 'transparent' 146 | }, 147 | overlayStyle: { 148 | opacity: 0, 149 | backgroundColor: 'transparent' 150 | }, 151 | containerStyle: { 152 | backgroundColor: 'transparent' 153 | } 154 | }), 155 | }} 156 | > 157 | 162 | 171 | 182 | 183 | 184 | 185 | ); 186 | } -------------------------------------------------------------------------------- /src/screens/AddTodoScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, StyleSheet, Text, TouchableOpacity, TouchableWithoutFeedback, KeyboardAvoidingView, Platform, ScrollView, InteractionManager, Keyboard } from 'react-native'; 3 | import { useNavigation } from '@react-navigation/native'; 4 | import { MaterialIcons } from '@expo/vector-icons'; 5 | import { SmartTodoInput } from '../components/todo/SmartTodoInput'; 6 | import { useTodos } from '../contexts/TodoContext'; 7 | import { useSettings } from '../contexts/SettingsContext'; 8 | import { NotificationService } from '../services/NotificationService'; 9 | import { ParsedTodo } from '../types/nlp'; 10 | import { DarkModeShader } from '../components/shaders/DarkModeShader'; 11 | import { GLView } from 'expo-gl'; 12 | import Animated, { 13 | useSharedValue, 14 | withRepeat, 15 | withTiming, 16 | withSpring, 17 | useAnimatedStyle, 18 | } from 'react-native-reanimated'; 19 | 20 | const FRAGMENT_SHADER = ` 21 | precision highp float; 22 | 23 | varying vec2 vTexCoord; 24 | uniform float uTime; 25 | uniform float uSwipe; 26 | 27 | // Modern, dreamy color palette 28 | const vec3 skyBlue = vec3(0.85, 0.91, 1.0); // #D9E8FF - soft sky blue 29 | const vec3 lavender = vec3(0.93, 0.91, 0.99); // #EDE8FD - gentle lavender 30 | const vec3 mintGreen = vec3(0.90, 0.99, 0.97); // #E5FDF8 - fresh mint 31 | const vec3 peach = vec3(1.0, 0.95, 0.93); // #FFF2EE - soft peach 32 | 33 | void main() { 34 | vec2 uv = vTexCoord; 35 | float t = uTime * 0.015; // Super slow, dreamy movement 36 | 37 | // Create organic flowing movement 38 | vec2 flow = vec2( 39 | sin(t + uv.x * 2.0) * cos(t * 0.4) * 0.3, 40 | cos(t * 0.8 + uv.y * 2.0) * sin(t * 0.3) * 0.3 41 | ); 42 | 43 | vec2 distortedUV = uv + flow; 44 | 45 | // Create smooth, flowing color transitions 46 | float noise1 = sin(distortedUV.x * 3.0 + distortedUV.y * 2.0 + t) * 0.5 + 0.5; 47 | float noise2 = cos(distortedUV.y * 2.0 - distortedUV.x * 3.0 - t * 1.2) * 0.5 + 0.5; 48 | 49 | // Blend the colors in a more interesting way 50 | vec3 gradient = skyBlue; 51 | gradient = mix(gradient, lavender, 52 | smoothstep(0.3, 0.7, noise1) * 0.4 53 | ); 54 | gradient = mix(gradient, mintGreen, 55 | smoothstep(0.4, 0.6, noise2) * 0.3 56 | ); 57 | gradient = mix(gradient, peach, 58 | smoothstep(0.45, 0.55, (noise1 + noise2) * 0.5) * 0.2 59 | ); 60 | 61 | // Add a subtle sparkle effect 62 | float sparkle = sin(uv.x * 40.0 + t) * sin(uv.y * 40.0 - t); 63 | sparkle = pow(max(0.0, sparkle), 20.0) * 0.03; 64 | 65 | // Add very subtle swipe response 66 | float swipeEffect = sin(distortedUV.x * 3.14 + t) * uSwipe * 0.02; 67 | 68 | // Combine everything with a dreamy softness 69 | gradient += vec3(sparkle + swipeEffect); 70 | gradient = mix(gradient, vec3(1.0), 0.1); // Add slight brightness 71 | gradient = smoothstep(0.0, 1.0, gradient); // Extra smoothing 72 | 73 | gl_FragColor = vec4(gradient, 1.0); 74 | } 75 | `; 76 | 77 | const VERTEX_SHADER = ` 78 | attribute vec4 position; 79 | varying vec2 vTexCoord; 80 | 81 | void main() { 82 | vTexCoord = position.xy * 0.5 + 0.5; 83 | gl_Position = position; 84 | } 85 | `; 86 | 87 | function CardShader({ time, swipeProgress }) { 88 | const onContextCreate = (gl) => { 89 | // Create and compile shaders 90 | const vertShader = gl.createShader(gl.VERTEX_SHADER); 91 | gl.shaderSource(vertShader, VERTEX_SHADER); 92 | gl.compileShader(vertShader); 93 | 94 | const fragShader = gl.createShader(gl.FRAGMENT_SHADER); 95 | gl.shaderSource(fragShader, FRAGMENT_SHADER); 96 | gl.compileShader(fragShader); 97 | 98 | // Create program 99 | const program = gl.createProgram(); 100 | gl.attachShader(program, vertShader); 101 | gl.attachShader(program, fragShader); 102 | gl.linkProgram(program); 103 | gl.useProgram(program); 104 | 105 | // Set up buffers 106 | const positions = new Float32Array([ 107 | -1, -1, 108 | 1, -1, 109 | -1, 1, 110 | 1, 1, 111 | ]); 112 | 113 | const buffer = gl.createBuffer(); 114 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer); 115 | gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW); 116 | 117 | // Set up attributes and uniforms 118 | const positionLocation = gl.getAttribLocation(program, "position"); 119 | gl.enableVertexAttribArray(positionLocation); 120 | gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0); 121 | 122 | // Get uniform locations 123 | const timeLocation = gl.getUniformLocation(program, "uTime"); 124 | const swipeLocation = gl.getUniformLocation(program, "uSwipe"); 125 | 126 | // Render function 127 | const render = () => { 128 | gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); 129 | 130 | // Clear with white to match the modal background 131 | gl.clearColor(1, 1, 1, 1); 132 | gl.clear(gl.COLOR_BUFFER_BIT); 133 | 134 | // Update uniforms 135 | gl.uniform1f(timeLocation, time); 136 | gl.uniform1f(swipeLocation, swipeProgress); 137 | 138 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 139 | gl.endFrameEXP(); 140 | }; 141 | 142 | render(); 143 | }; 144 | 145 | return ( 146 | 150 | ); 151 | } 152 | 153 | export function AddTodoScreen() { 154 | const navigation = useNavigation(); 155 | const { addTodo } = useTodos(); 156 | const { settings } = useSettings(); 157 | const slideAnim = useSharedValue(1); 158 | const inputRef = React.useRef(null); 159 | const keyboardHeight = useSharedValue(0); 160 | const shaderTime = useSharedValue(0); 161 | const swipeProgress = useSharedValue(0); 162 | 163 | React.useEffect(() => { 164 | // Show modal first 165 | slideAnim.value = withSpring(0, { 166 | damping: 15, 167 | stiffness: 90 168 | }); 169 | 170 | // Pre-emptively show keyboard 171 | const keyboardShowListener = Keyboard.addListener( 172 | Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', 173 | (e) => { 174 | keyboardHeight.value = withTiming(e.endCoordinates.height, { 175 | duration: Platform.OS === 'ios' ? e.duration : 250 176 | }); 177 | } 178 | ); 179 | 180 | // Focus input after a very short delay 181 | const timer = setTimeout(() => { 182 | inputRef.current?.focus(); 183 | }, 50); 184 | 185 | return () => { 186 | clearTimeout(timer); 187 | keyboardShowListener.remove(); 188 | }; 189 | }, []); 190 | 191 | // Add effect to animate shader time 192 | React.useEffect(() => { 193 | shaderTime.value = withRepeat( 194 | withTiming(Math.PI * 2, { duration: 10000 }), 195 | -1, 196 | false 197 | ); 198 | }, []); 199 | 200 | async function handleSubmit(parsedTodo: ParsedTodo) { 201 | const todo = { 202 | title: parsedTodo.title, 203 | description: parsedTodo.description, 204 | dueDate: parsedTodo.dueDate || new Date(), 205 | completed: false, 206 | tags: parsedTodo.tags, 207 | }; 208 | 209 | await addTodo(todo); 210 | 211 | if (settings.notifications.enabled) { 212 | await NotificationService.scheduleSmartNotification(todo, settings.notifications); 213 | } 214 | 215 | navigation.goBack(); 216 | } 217 | 218 | return ( 219 | 220 | navigation.goBack()}> 221 | 222 | 223 | 224 | ({ 228 | transform: [{ 229 | translateY: withSpring(slideAnim.value * 600) 230 | }] 231 | })) 232 | ]} 233 | > 234 | 238 | 239 | 243 | 244 | 245 | 246 | 253 | navigation.goBack()} 256 | > 257 | 258 | 259 | 260 | 261 | 262 | Create New Task 263 | What would you like to accomplish? 264 | 265 | 266 | 267 | 268 | 269 | New Task 270 | 271 | 272 | 277 | 278 | 279 | 280 | {/* 281 | 282 | 283 | 284 | */} 285 | 286 | 287 | 288 | ); 289 | } 290 | 291 | const styles = StyleSheet.create({ 292 | container: { 293 | flex: 1, 294 | backgroundColor: 'transparent', 295 | justifyContent: 'flex-end', 296 | margin: 0, 297 | }, 298 | dismissArea: { 299 | flex: 1, 300 | backgroundColor: 'transparent', 301 | }, 302 | modalContent: { 303 | backgroundColor: 'transparent', 304 | borderTopLeftRadius: 20, 305 | borderTopRightRadius: 20, 306 | maxHeight: '95%', 307 | minHeight: '85%', 308 | overflow: 'hidden', 309 | margin: 0, 310 | padding: 0, 311 | shadowColor: '#000', 312 | shadowOffset: { 313 | width: 0, 314 | height: -2, 315 | }, 316 | shadowOpacity: 0.25, 317 | shadowRadius: 3.84, 318 | elevation: 5, 319 | }, 320 | shaderContainer: { 321 | borderTopLeftRadius: 20, 322 | borderTopRightRadius: 20, 323 | overflow: 'hidden', 324 | backgroundColor: 'white', 325 | }, 326 | shaderWrapper: { 327 | position: 'absolute', 328 | top: -20, // Extend shader above the visible area 329 | left: 0, 330 | right: 0, 331 | bottom: 0, 332 | borderTopLeftRadius: 20, 333 | borderTopRightRadius: 20, 334 | overflow: 'hidden', 335 | }, 336 | scrollContent: { 337 | padding: 20, 338 | }, 339 | header: { 340 | alignItems: 'center', 341 | marginTop: 40, 342 | marginBottom: 30, 343 | }, 344 | title: { 345 | fontSize: 32, 346 | fontWeight: 'bold', 347 | color: '#000', 348 | marginTop: 16, 349 | marginBottom: 8, 350 | textAlign: 'center', 351 | }, 352 | subtitle: { 353 | fontSize: 18, 354 | color: '#666', 355 | textAlign: 'center', 356 | }, 357 | card: { 358 | backgroundColor: '#fff', 359 | borderRadius: 16, 360 | padding: 20, 361 | shadowColor: '#000', 362 | shadowOffset: { width: 0, height: 2 }, 363 | shadowOpacity: 0.1, 364 | shadowRadius: 8, 365 | elevation: 4, 366 | }, 367 | cardHeader: { 368 | flexDirection: 'row', 369 | alignItems: 'center', 370 | marginBottom: 16, 371 | }, 372 | cardTitle: { 373 | fontSize: 20, 374 | fontWeight: '600', 375 | marginLeft: 12, 376 | color: '#007AFF', 377 | }, 378 | inputContainer: { 379 | marginBottom: 16, 380 | }, 381 | decorationContainer: { 382 | flexDirection: 'row', 383 | justifyContent: 'center', 384 | marginTop: 30, 385 | }, 386 | decoration: { 387 | margin: 8, 388 | transform: [{ rotate: '-15deg' }], 389 | }, 390 | closeButton: { 391 | position: 'absolute', 392 | top: 20, 393 | right: 20, 394 | zIndex: 1, 395 | padding: 8, 396 | backgroundColor: '#fff', 397 | borderRadius: 20, 398 | shadowColor: '#000', 399 | shadowOffset: { width: 0, height: 2 }, 400 | shadowOpacity: 0.1, 401 | shadowRadius: 4, 402 | elevation: 2, 403 | }, 404 | }); -------------------------------------------------------------------------------- /src/screens/AnalyticsScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { View, Text, StyleSheet, Dimensions } from 'react-native'; 3 | import { LineChart } from 'react-native-chart-kit'; 4 | import AsyncStorage from '@react-native-async-storage/async-storage'; 5 | import { DarkModeShader } from '../components/shaders/DarkModeShader'; 6 | import { useFocusEffect } from '@react-navigation/native'; 7 | import { useTodos } from '../contexts/TodoContext'; 8 | 9 | export function AnalyticsScreen() { 10 | const { todos } = useTodos(); 11 | const [analyticsData, setAnalyticsData] = React.useState({ 12 | totalTasks: 0, 13 | completedTasks: 0, 14 | weeklyProgress: { 15 | total: [0, 0, 0, 0, 0, 0, 0], 16 | completed: [0, 0, 0, 0, 0, 0, 0] 17 | } 18 | }); 19 | const [selectedIndex, setSelectedIndex] = useState(null); 20 | 21 | React.useEffect(() => { 22 | console.log('\n📊 ==================== ANALYTICS ===================='); 23 | console.log('📝 Current Todos:', todos); 24 | 25 | // Reset analytics if there are no todos 26 | if (!todos || todos.length === 0) { 27 | console.log('❌ No todos available - Resetting analytics'); 28 | setAnalyticsData({ 29 | totalTasks: 0, 30 | completedTasks: 0, 31 | weeklyProgress: { 32 | total: [0, 0, 0, 0, 0, 0, 0], 33 | completed: [0, 0, 0, 0, 0, 0, 0] 34 | } 35 | }); 36 | return; 37 | } 38 | 39 | function calculateAnalytics() { 40 | const totalTasks = todos.length; 41 | const completedTasks = todos.filter(todo => todo.completed).length; 42 | console.log('📈 Total Tasks:', totalTasks); 43 | console.log('✅ Completed Tasks:', completedTasks); 44 | 45 | // Get today's date 46 | const now = new Date(); 47 | const startOfWeek = new Date(now); 48 | startOfWeek.setDate(now.getDate() - now.getDay() + (now.getDay() === 0 ? -6 : 1)); 49 | startOfWeek.setHours(0, 0, 0, 0); 50 | console.log('📅 Start of Week:', startOfWeek); 51 | 52 | // Initialize arrays 53 | const weeklyTotal = Array(7).fill(0); 54 | const weeklyCompleted = Array(7).fill(0); 55 | 56 | // Process each todo 57 | todos.forEach(todo => { 58 | const createdDate = new Date(todo.createdAt); 59 | console.log(`📌 Processing Todo: ${todo.title}`); 60 | console.log(` Created: ${createdDate}`); 61 | 62 | // Only count tasks from current week 63 | if (createdDate >= startOfWeek) { 64 | const dayIndex = (createdDate.getDay() + 6) % 7; 65 | weeklyTotal[dayIndex]++; 66 | console.log(` Added to day ${dayIndex} (total: ${weeklyTotal[dayIndex]})`); 67 | 68 | if (todo.completed && todo.completedAt) { 69 | const completedDate = new Date(todo.completedAt); 70 | console.log(` Completed: ${completedDate}`); 71 | if (completedDate >= startOfWeek) { 72 | const completedDayIndex = (completedDate.getDay() + 6) % 7; 73 | weeklyCompleted[completedDayIndex]++; 74 | console.log(` Added to completed day ${completedDayIndex} (total: ${weeklyCompleted[completedDayIndex]})`); 75 | } 76 | } 77 | } 78 | }); 79 | 80 | console.log('📊 Weekly Totals:', weeklyTotal); 81 | console.log('✅ Weekly Completed:', weeklyCompleted); 82 | 83 | setAnalyticsData({ 84 | totalTasks, 85 | completedTasks, 86 | weeklyProgress: { 87 | total: weeklyTotal, 88 | completed: weeklyCompleted 89 | } 90 | }); 91 | } 92 | 93 | calculateAnalytics(); 94 | console.log('=====================================================\n'); 95 | }, [todos]); 96 | 97 | const completionRate = analyticsData.totalTasks > 0 98 | ? Math.round((analyticsData.completedTasks / analyticsData.totalTasks) * 100) 99 | : 0; 100 | 101 | // Update the chart configuration 102 | const data = { 103 | labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], 104 | datasets: [ 105 | { 106 | data: analyticsData.weeklyProgress.total, 107 | color: (opacity = 1) => `rgba(255, 149, 0, ${opacity})`, // Orange for total tasks 108 | strokeWidth: 2 109 | }, 110 | { 111 | data: analyticsData.weeklyProgress.completed, 112 | color: (opacity = 1) => `rgba(52, 199, 89, ${opacity})`, // Green for completed tasks 113 | strokeWidth: 2 114 | } 115 | ], 116 | legend: ['Total Tasks', 'Completed Tasks'] 117 | }; 118 | 119 | return ( 120 | 121 | 122 | Analytics Overview 123 | 124 | 125 | 126 | Total Tasks 127 | {analyticsData.totalTasks} 128 | 129 | 130 | Completed Tasks 131 | {analyticsData.completedTasks} 132 | 133 | 134 | Success Rate 135 | {completionRate}% 136 | 137 | 138 | 139 | 140 | Weekly Progress 141 | `rgba(255, 255, 255, ${opacity * 0.7})`, 151 | labelColor: (opacity = 1) => `rgba(255, 255, 255, ${opacity * 0.7})`, 152 | propsForBackgroundLines: { 153 | strokeDasharray: "", 154 | stroke: "rgba(255, 255, 255, 0.05)", 155 | }, 156 | yAxisMin: 0, 157 | yAxisMax: Math.max(...analyticsData.weeklyProgress.total, 1), 158 | paddingRight: 0, 159 | paddingLeft: 0, 160 | fillShadowGradientFrom: 'transparent', 161 | fillShadowGradientTo: 'transparent', 162 | }} 163 | withVerticalLabels={true} 164 | withHorizontalLabels={false} 165 | withVerticalLines={false} 166 | withHorizontalLines={false} 167 | fromZero={true} 168 | transparent={true} 169 | style={{ 170 | marginLeft: -16, 171 | backgroundColor: 'transparent', 172 | }} 173 | /> 174 | 175 | 176 | ); 177 | } 178 | 179 | const Tooltip = ({x, y, value, visible}) => { 180 | if (!visible) return null; 181 | 182 | return ( 183 | 195 | {value} 196 | 197 | ); 198 | }; 199 | 200 | const styles = StyleSheet.create({ 201 | container: { 202 | flex: 1, 203 | padding: 24, 204 | backgroundColor: '#000', 205 | paddingTop: 48, 206 | }, 207 | screenTitle: { 208 | fontSize: 24, 209 | fontWeight: '600', 210 | color: '#FFF', 211 | marginBottom: 32, 212 | marginTop: 84, 213 | }, 214 | statsContainer: { 215 | flexDirection: 'row', 216 | justifyContent: 'space-between', 217 | marginBottom: 32, 218 | }, 219 | statBox: { 220 | backgroundColor: 'rgba(255, 255, 255, 0.05)', 221 | borderRadius: 12, 222 | padding: 16, 223 | flex: 1, 224 | borderWidth: 1, 225 | borderColor: 'rgba(255, 255, 255, 0.08)', 226 | }, 227 | middleStatBox: { 228 | marginHorizontal: 16, 229 | }, 230 | statLabel: { 231 | fontSize: 12, 232 | color: 'rgba(255, 255, 255, 0.5)', 233 | fontWeight: '500', 234 | marginBottom: 8, 235 | textTransform: 'uppercase', 236 | letterSpacing: 0.5, 237 | }, 238 | statNumber: { 239 | fontSize: 24, 240 | fontWeight: '600', 241 | color: '#FFF', 242 | }, 243 | chartContainer: { 244 | backgroundColor: 'rgba(255, 255, 255, 0.05)', 245 | borderRadius: 16, 246 | padding: 24, 247 | borderWidth: 1, 248 | borderColor: 'rgba(255, 255, 255, 0.08)', 249 | alignItems: 'center', 250 | width: Dimensions.get('window').width - 48, 251 | alignSelf: 'center', 252 | }, 253 | chartTitle: { 254 | fontSize: 16, 255 | fontWeight: '600', 256 | color: 'rgba(255, 255, 255, 0.9)', 257 | marginBottom: 24, 258 | textTransform: 'uppercase', 259 | letterSpacing: 0.5, 260 | textAlign: 'center', 261 | }, 262 | }); -------------------------------------------------------------------------------- /src/screens/AuthScreen.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { View, Text, StyleSheet, Pressable } from 'react-native'; 3 | import { useAuth } from '../contexts/AuthContext'; 4 | import { MaterialIcons } from '@expo/vector-icons'; 5 | import * as LocalAuthentication from 'expo-local-authentication'; 6 | 7 | export function AuthScreen() { 8 | const { authenticate, hasHardware } = useAuth(); 9 | 10 | useEffect(() => { 11 | checkBiometrics(); 12 | }, []); 13 | 14 | async function checkBiometrics() { 15 | const supportedTypes = await LocalAuthentication.supportedAuthenticationTypesAsync(); 16 | const hasFaceId = supportedTypes.includes(LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION); 17 | 18 | if (hasFaceId) { 19 | authenticate(); 20 | } 21 | } 22 | 23 | return ( 24 | 25 | 26 | Secure Todo 27 | 28 | {hasHardware 29 | ? 'Please use Face ID to access your todos' 30 | : 'Face ID is not available on this device'} 31 | 32 | {hasHardware && ( 33 | 34 | Use Face ID 35 | 36 | )} 37 | 38 | ); 39 | } 40 | 41 | const styles = StyleSheet.create({ 42 | container: { 43 | flex: 1, 44 | justifyContent: 'center', 45 | alignItems: 'center', 46 | padding: 16, 47 | backgroundColor: '#fff', 48 | }, 49 | title: { 50 | fontSize: 24, 51 | fontWeight: 'bold', 52 | marginTop: 16, 53 | marginBottom: 8, 54 | }, 55 | subtitle: { 56 | fontSize: 16, 57 | textAlign: 'center', 58 | color: '#666', 59 | marginBottom: 24, 60 | }, 61 | button: { 62 | backgroundColor: '#007AFF', 63 | paddingHorizontal: 24, 64 | paddingVertical: 12, 65 | borderRadius: 8, 66 | }, 67 | buttonText: { 68 | color: '#fff', 69 | fontSize: 16, 70 | fontWeight: '600', 71 | }, 72 | }); -------------------------------------------------------------------------------- /src/screens/DebugScreen.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { View, Text, StyleSheet, ScrollView, Pressable, Share } from 'react-native'; 3 | import { LoggingService } from '../services/LoggingService'; 4 | import { useErrorHandler } from '../hooks/useErrorHandler'; 5 | 6 | export function DebugScreen() { 7 | const { error, handleError, clearError } = useErrorHandler('DebugScreen'); 8 | const [logs, setLogs] = useState(''); 9 | 10 | async function handleExportLogs() { 11 | try { 12 | const exportedLogs = await LoggingService.exportLogs(); 13 | await Share.share({ 14 | message: exportedLogs, 15 | title: 'Application Logs', 16 | }); 17 | } catch (error) { 18 | if (error instanceof Error) { 19 | handleError(error); 20 | } 21 | } 22 | } 23 | 24 | return ( 25 | 26 | {error && ( 27 | 28 | {error.message} 29 | 30 | Dismiss 31 | 32 | 33 | )} 34 | 35 | 36 | Export Logs 37 | 38 | 39 | {logs ? ( 40 | {logs} 41 | ) : null} 42 | 43 | ); 44 | } 45 | 46 | const styles = StyleSheet.create({ 47 | container: { 48 | flex: 1, 49 | padding: 16, 50 | backgroundColor: '#fff', 51 | }, 52 | errorContainer: { 53 | backgroundColor: '#ffebee', 54 | padding: 16, 55 | borderRadius: 8, 56 | marginBottom: 16, 57 | }, 58 | errorText: { 59 | color: '#c62828', 60 | marginBottom: 8, 61 | }, 62 | errorButton: { 63 | color: '#007AFF', 64 | }, 65 | button: { 66 | backgroundColor: '#007AFF', 67 | padding: 16, 68 | borderRadius: 8, 69 | alignItems: 'center', 70 | }, 71 | buttonText: { 72 | color: '#fff', 73 | fontSize: 16, 74 | fontWeight: '600', 75 | }, 76 | logs: { 77 | marginTop: 16, 78 | fontFamily: 'monospace', 79 | fontSize: 12, 80 | }, 81 | }); -------------------------------------------------------------------------------- /src/screens/HomeScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, TouchableOpacity, SafeAreaView, AppState, Platform, Linking, IntentLauncher } from 'react-native'; 3 | import { TodoList } from '../components/todo/TodoList'; 4 | import { DarkModeShader } from '../components/shaders/DarkModeShader'; 5 | import { Ionicons } from '@expo/vector-icons'; 6 | import { useNavigation } from '@react-navigation/native'; 7 | import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; 8 | import * as Notifications from 'expo-notifications'; 9 | 10 | type RootStackParamList = { 11 | Home: undefined; 12 | SettingsScreen: undefined; 13 | }; 14 | 15 | type NavigationProp = NativeStackNavigationProp; 16 | 17 | export function HomeScreen() { 18 | const navigation = useNavigation(); 19 | const [notificationsAllowed, setNotificationsAllowed] = React.useState(true); 20 | 21 | React.useEffect(() => { 22 | checkNotificationPermissions(); 23 | 24 | // Add subscription to permission changes 25 | const subscription = Notifications.addNotificationResponseReceivedListener(() => { 26 | checkNotificationPermissions(); 27 | }); 28 | 29 | // Check permissions when app comes to foreground 30 | const foregroundSubscription = AppState.addEventListener('change', (nextAppState) => { 31 | if (nextAppState === 'active') { 32 | checkNotificationPermissions(); 33 | } 34 | }); 35 | 36 | return () => { 37 | subscription.remove(); 38 | foregroundSubscription.remove(); 39 | }; 40 | }, []); 41 | 42 | const checkNotificationPermissions = async () => { 43 | const { status } = await Notifications.getPermissionsAsync(); 44 | setNotificationsAllowed(status === 'granted'); 45 | }; 46 | 47 | const requestNotificationPermission = async () => { 48 | console.log('Requesting notification permission...'); 49 | try { 50 | const { status: existingStatus } = await Notifications.getPermissionsAsync(); 51 | 52 | if (existingStatus === 'denied') { 53 | // On iOS, we can use Linking to take users to app settings 54 | if (Platform.OS === 'ios') { 55 | Linking.openSettings(); 56 | } else { 57 | // For Android, we can use IntentLauncher 58 | await IntentLauncher.startActivityAsync( 59 | IntentLauncher.ActivityAction.NOTIFICATION_SETTINGS 60 | ); 61 | } 62 | return; 63 | } 64 | 65 | const { status } = await Notifications.requestPermissionsAsync({ 66 | ios: { 67 | allowAlert: true, 68 | allowBadge: true, 69 | allowSound: true, 70 | }, 71 | android: true 72 | }); 73 | console.log('Permission status:', status); 74 | setNotificationsAllowed(status === 'granted'); 75 | } catch (error) { 76 | console.error('Error requesting notification permission:', error); 77 | } 78 | }; 79 | 80 | React.useEffect(() => { 81 | console.log('Current notification status:', notificationsAllowed); 82 | navigation.setOptions({ 83 | headerLeft: () => ( 84 | notificationsAllowed ? null : ( 85 | { 88 | console.log('TouchableOpacity pressed'); 89 | requestNotificationPermission(); 90 | }} 91 | > 92 | 93 | 94 | ) 95 | ), 96 | headerRight: () => ( 97 | navigation.navigate('SettingsScreen')} 100 | > 101 | 102 | 103 | ), 104 | }); 105 | }, [navigation, notificationsAllowed, requestNotificationPermission]); 106 | 107 | return ( 108 | 109 | 110 | 111 | 112 | ); 113 | } -------------------------------------------------------------------------------- /src/screens/SettingsScreen.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, StyleSheet, Switch, ScrollView, ActivityIndicator, SafeAreaView, TouchableOpacity, Alert, Platform, Linking } from 'react-native'; 2 | import { useState, useEffect } from 'react'; 3 | import { useSettings } from '../contexts/SettingsContext'; 4 | import { Picker } from '@react-native-picker/picker'; 5 | import { DarkModeShader } from '../components/shaders/DarkModeShader'; 6 | import { MaterialIcons } from '@expo/vector-icons'; 7 | import { useNavigation } from '@react-navigation/native'; 8 | import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; 9 | import { format } from 'date-fns'; 10 | import DateTimePicker from '@react-native-community/datetimepicker'; 11 | import { useSafeAreaInsets } from 'react-native-safe-area-context'; 12 | 13 | type RootStackParamList = { 14 | Home: undefined; 15 | SettingsScreen: undefined; 16 | }; 17 | 18 | type NavigationProp = NativeStackNavigationProp; 19 | 20 | type TimeSettings = { 21 | wakeUpTime: string; 22 | bedTime: string; 23 | workStartTime: string; 24 | workEndTime: string; 25 | }; 26 | 27 | type TimePickerState = { 28 | show: boolean; 29 | mode: 'date' | 'time'; 30 | current: keyof TimeSettings | null; 31 | }; 32 | 33 | const styles = StyleSheet.create({ 34 | container: { 35 | flex: 1, 36 | backgroundColor: 'transparent', 37 | }, 38 | centered: { 39 | flex: 1, 40 | justifyContent: 'center', 41 | alignItems: 'center', 42 | }, 43 | section: { 44 | marginHorizontal: 16, 45 | marginVertical: 12, 46 | paddingVertical: 16, 47 | borderRadius: 16, 48 | backgroundColor: 'rgba(255, 255, 255, 0.08)', 49 | shadowColor: "#000", 50 | shadowOffset: { 51 | width: 0, 52 | height: 2, 53 | }, 54 | shadowOpacity: 0.25, 55 | shadowRadius: 3.84, 56 | elevation: 5, 57 | }, 58 | sectionTitle: { 59 | fontSize: 18, 60 | fontWeight: '700', 61 | marginLeft: 16, 62 | marginBottom: 8, 63 | color: 'rgba(255, 255, 255, 0.95)', 64 | }, 65 | setting: { 66 | flexDirection: 'row', 67 | alignItems: 'center', 68 | justifyContent: 'space-between', 69 | paddingHorizontal: 16, 70 | paddingVertical: 14, 71 | }, 72 | settingLabel: { 73 | fontSize: 16, 74 | fontWeight: '500', 75 | color: 'rgba(255, 255, 255, 0.9)', 76 | }, 77 | picker: { 78 | width: 150, 79 | color: '#fff', 80 | }, 81 | backButton: { 82 | padding: 8, 83 | }, 84 | backButtonContainer: { 85 | position: 'absolute', 86 | top: Platform.OS === 'ios' ? 60 : 20, 87 | left: 16, 88 | right: 16, 89 | zIndex: 2, 90 | flexDirection: 'row', 91 | alignItems: 'center', 92 | backgroundColor: 'transparent', 93 | }, 94 | savingOverlay: { 95 | position: 'absolute', 96 | top: 0, 97 | left: 0, 98 | right: 0, 99 | bottom: 0, 100 | backgroundColor: 'rgba(0,0,0,0.7)', 101 | justifyContent: 'center', 102 | alignItems: 'center', 103 | flexDirection: 'row', 104 | }, 105 | savingText: { 106 | color: '#fff', 107 | marginLeft: 8, 108 | fontSize: 16, 109 | }, 110 | scrollContent: { 111 | paddingTop: Platform.OS === 'ios' ? 120 : 80, 112 | paddingBottom: 32, 113 | }, 114 | infoText: { 115 | fontSize: 14, 116 | color: 'rgba(255, 255, 255, 0.6)', 117 | paddingHorizontal: 16, 118 | paddingBottom: 16, 119 | lineHeight: 20, 120 | }, 121 | timeInput: { 122 | backgroundColor: 'rgba(255, 255, 255, 0.12)', 123 | borderRadius: 12, 124 | padding: 12, 125 | minWidth: 100, 126 | alignItems: 'center', 127 | borderWidth: 1, 128 | borderColor: 'rgba(255, 255, 255, 0.15)', 129 | }, 130 | timeText: { 131 | color: '#fff', 132 | fontSize: 16, 133 | fontWeight: '600', 134 | }, 135 | timeSeparator: { 136 | color: 'rgba(255, 255, 255, 0.7)', 137 | fontSize: 16, 138 | marginHorizontal: 12, 139 | fontWeight: '500', 140 | }, 141 | badge: { 142 | backgroundColor: '#007AFF', 143 | paddingHorizontal: 8, 144 | paddingVertical: 4, 145 | borderRadius: 12, 146 | marginLeft: 8, 147 | }, 148 | badgeText: { 149 | color: '#fff', 150 | fontSize: 12, 151 | fontWeight: '600', 152 | }, 153 | titleContainer: { 154 | position: 'absolute', 155 | left: 0, 156 | right: 0, 157 | alignItems: 'center', 158 | }, 159 | titleText: { 160 | fontSize: 18, 161 | fontWeight: '600', 162 | color: '#fff', 163 | }, 164 | supportSection: { 165 | marginHorizontal: 16, 166 | marginVertical: 12, 167 | paddingVertical: 24, 168 | paddingHorizontal: 20, 169 | borderRadius: 16, 170 | backgroundColor: 'rgba(255, 255, 255, 0.08)', 171 | alignItems: 'center', 172 | }, 173 | heartIcon: { 174 | marginBottom: 16, 175 | }, 176 | supportTitle: { 177 | fontSize: 20, 178 | fontWeight: '700', 179 | color: '#fff', 180 | marginBottom: 12, 181 | textAlign: 'center', 182 | }, 183 | supportText: { 184 | fontSize: 15, 185 | color: 'rgba(255, 255, 255, 0.8)', 186 | textAlign: 'center', 187 | lineHeight: 22, 188 | marginBottom: 24, 189 | }, 190 | donateButton: { 191 | backgroundColor: '#007AFF', 192 | paddingHorizontal: 24, 193 | paddingVertical: 12, 194 | borderRadius: 25, 195 | flexDirection: 'row', 196 | alignItems: 'center', 197 | }, 198 | donateButtonText: { 199 | color: '#fff', 200 | fontSize: 16, 201 | fontWeight: '600', 202 | marginLeft: 8, 203 | }, 204 | }); 205 | 206 | export function SettingsScreen() { 207 | const navigation = useNavigation(); 208 | const { settings, updateSettings, isLoading } = useSettings(); 209 | const [isSaving, setIsSaving] = useState(false); 210 | const [timeSettings, setTimeSettings] = useState({ 211 | wakeUpTime: settings.wakeUpTime || '07:00', 212 | bedTime: settings.bedTime || '22:00', 213 | workStartTime: settings.workStartTime || '09:00', 214 | workEndTime: settings.workEndTime || '17:00', 215 | }); 216 | const [timePicker, setTimePicker] = useState({ 217 | show: false, 218 | mode: 'time', 219 | current: null 220 | }); 221 | 222 | const dynamicStyles = { 223 | sectionTitle: { 224 | color: 'rgba(255, 255, 255, 0.95)', 225 | }, 226 | settingLabel: { 227 | color: 'rgba(255, 255, 255, 0.9)', 228 | }, 229 | }; 230 | 231 | const handleSettingChange = async (key: keyof AppSettings, value: any) => { 232 | try { 233 | console.log('[Settings] Updating setting:', key, 'to:', value); 234 | setIsSaving(true); 235 | await updateSettings({ [key]: value }); 236 | console.log('[Settings] Successfully updated:', key); 237 | } catch (error) { 238 | console.error('[Settings] Error updating setting:', error); 239 | } finally { 240 | setIsSaving(false); 241 | } 242 | }; 243 | 244 | const handleBack = () => { 245 | requestAnimationFrame(() => { 246 | navigation.goBack(); 247 | }); 248 | }; 249 | 250 | const handleTimeChange = async (key: keyof TimeSettings, value: string) => { 251 | try { 252 | // Validate work start time is after wake-up time 253 | if (key === 'workStartTime') { 254 | const [wakeHours, wakeMinutes] = timeSettings.wakeUpTime.split(':'); 255 | const [startHours, startMinutes] = value.split(':'); 256 | 257 | const wakeTime = parseInt(wakeHours) * 60 + parseInt(wakeMinutes); 258 | const startTime = parseInt(startHours) * 60 + parseInt(startMinutes); 259 | 260 | if (startTime <= wakeTime) { 261 | throw new Error('Work start time must be at least one minute after wake-up time'); 262 | } 263 | } 264 | 265 | // Validate times before saving 266 | if (key === 'wakeUpTime' || key === 'bedTime') { 267 | const [bedHours, bedMinutes] = (key === 'bedTime' ? value : timeSettings.bedTime).split(':'); 268 | const [wakeHours, wakeMinutes] = (key === 'wakeUpTime' ? value : timeSettings.wakeUpTime).split(':'); 269 | 270 | const bedTime = parseInt(bedHours) * 60 + parseInt(bedMinutes); 271 | const wakeTime = parseInt(wakeHours) * 60 + parseInt(wakeMinutes); 272 | 273 | // Normalize times for comparison 274 | const normalizedWakeTime = wakeTime; 275 | const normalizedBedTime = bedTime < wakeTime ? bedTime + (24 * 60) : bedTime; 276 | 277 | // Calculate duration in hours 278 | const durationInHours = (normalizedWakeTime - (normalizedBedTime - 24 * 60)) / 60; 279 | 280 | if (durationInHours > 16) { 281 | throw new Error('Sleep duration cannot be more than 16 hours'); 282 | } 283 | 284 | // For wake-up time, validate against work start time 285 | if (key === 'wakeUpTime') { 286 | const [startHours, startMinutes] = timeSettings.workStartTime.split(':'); 287 | const startTime = parseInt(startHours) * 60 + parseInt(startMinutes); 288 | 289 | if (wakeTime >= startTime) { 290 | throw new Error('Wake-up time must be at least one minute before work start time'); 291 | } 292 | } 293 | 294 | if ((key === 'wakeUpTime' && normalizedWakeTime >= normalizedBedTime) || 295 | (key === 'bedTime' && normalizedBedTime <= normalizedWakeTime)) { 296 | throw new Error('Invalid time combination'); 297 | } 298 | } 299 | 300 | setTimeSettings(prev => ({ ...prev, [key]: value })); 301 | await handleSettingChange(key, value); 302 | } catch (error) { 303 | console.error('[Settings] Error updating time setting:', error); 304 | Alert.alert('Error', error instanceof Error ? error.message : 'Failed to update time setting'); 305 | } 306 | }; 307 | 308 | const showTimePicker = (setting: keyof TimeSettings) => { 309 | setTimePicker({ 310 | show: true, 311 | mode: 'time', 312 | current: setting 313 | }); 314 | }; 315 | 316 | const onTimeChange = (event: any, selectedDate?: Date) => { 317 | setTimePicker(prev => ({ ...prev, show: false })); 318 | 319 | if (event.type === 'dismissed' || !selectedDate || !timePicker.current) { 320 | return; 321 | } 322 | 323 | const timeString = format(selectedDate, 'HH:mm'); 324 | handleTimeChange(timePicker.current, timeString); 325 | }; 326 | 327 | const handleTimePickerConfirm = (selectedDate: Date) => { 328 | if (!timePicker.current) return; 329 | 330 | const timeString = format(selectedDate, 'HH:mm'); 331 | 332 | // Work hours validation 333 | if (timePicker.current === 'workEndTime') { 334 | const [startHours, startMinutes] = timeSettings.workStartTime.split(':'); 335 | const startTime = parseInt(startHours) * 60 + parseInt(startMinutes); 336 | const [endHours, endMinutes] = timeString.split(':'); 337 | const endTime = parseInt(endHours) * 60 + parseInt(endMinutes); 338 | 339 | if (endTime <= startTime) { 340 | Alert.alert( 341 | 'Invalid Time', 342 | 'Work end time must be after work start time', 343 | [{ text: 'OK' }] 344 | ); 345 | return; 346 | } 347 | } 348 | 349 | // Validate work start time is after wake-up time 350 | if (timePicker.current === 'workStartTime') { 351 | const [wakeHours, wakeMinutes] = timeSettings.wakeUpTime.split(':'); 352 | const [startHours, startMinutes] = timeString.split(':'); 353 | 354 | const wakeTime = parseInt(wakeHours) * 60 + parseInt(wakeMinutes); 355 | const startTime = parseInt(startHours) * 60 + parseInt(startMinutes); 356 | 357 | if (startTime <= wakeTime) { 358 | Alert.alert( 359 | 'Invalid Time', 360 | 'Work start time must be at least one minute after wake-up time', 361 | [{ text: 'OK' }] 362 | ); 363 | return; 364 | } 365 | } 366 | 367 | // Sleep schedule validation 368 | if (timePicker.current === 'wakeUpTime' || timePicker.current === 'bedTime') { 369 | const [bedHours, bedMinutes] = (timePicker.current === 'bedTime' ? timeString : timeSettings.bedTime).split(':'); 370 | const [wakeHours, wakeMinutes] = (timePicker.current === 'wakeUpTime' ? timeString : timeSettings.wakeUpTime).split(':'); 371 | 372 | const bedTime = parseInt(bedHours) * 60 + parseInt(bedMinutes); 373 | const wakeTime = parseInt(wakeHours) * 60 + parseInt(wakeMinutes); 374 | 375 | // Convert times to a 24-hour cycle where we assume: 376 | // - Bedtime is in the evening (PM) 377 | // - Wake time is in the morning (AM) 378 | const normalizedWakeTime = wakeTime; 379 | const normalizedBedTime = bedTime < wakeTime ? bedTime + (24 * 60) : bedTime; 380 | 381 | // Calculate duration in hours 382 | const durationInHours = (normalizedWakeTime - (normalizedBedTime - 24 * 60)) / 60; 383 | 384 | if (durationInHours > 16) { 385 | Alert.alert( 386 | 'Invalid Sleep Duration', 387 | 'Sleep duration cannot be more than 16 hours', 388 | [{ text: 'OK' }] 389 | ); 390 | return; 391 | } 392 | 393 | // For wake-up time, also validate against work start time 394 | if (timePicker.current === 'wakeUpTime') { 395 | const [startHours, startMinutes] = timeSettings.workStartTime.split(':'); 396 | const startTime = parseInt(startHours) * 60 + parseInt(startMinutes); 397 | 398 | if (wakeTime >= startTime) { 399 | Alert.alert( 400 | 'Invalid Time', 401 | 'Wake-up time must be at least one minute before work start time', 402 | [{ text: 'OK' }] 403 | ); 404 | return; 405 | } 406 | } 407 | 408 | if (timePicker.current === 'wakeUpTime' && normalizedWakeTime >= normalizedBedTime) { 409 | Alert.alert( 410 | 'Invalid Time', 411 | 'Wake-up time must be after bedtime', 412 | [{ text: 'OK' }] 413 | ); 414 | return; 415 | } 416 | 417 | if (timePicker.current === 'bedTime' && normalizedBedTime <= normalizedWakeTime) { 418 | Alert.alert( 419 | 'Invalid Time', 420 | 'Bedtime must be before wake-up time', 421 | [{ text: 'OK' }] 422 | ); 423 | return; 424 | } 425 | } 426 | 427 | handleTimeChange(timePicker.current, timeString); 428 | setTimePicker(prev => ({ ...prev, show: false })); 429 | }; 430 | 431 | const insets = useSafeAreaInsets(); 432 | 433 | if (isLoading) { 434 | return ( 435 | 436 | 437 | 438 | 439 | 440 | 441 | ); 442 | } 443 | 444 | return ( 445 | 446 | 447 | 448 | 449 | 454 | 455 | 456 | 457 | Settings 458 | 459 | 460 | 461 | 465 | 466 | Personalization 467 | 468 | Help us understand your daily routine to provide better-timed notifications 469 | 470 | 471 | 472 | Sleep Schedule 473 | 474 | showTimePicker('bedTime')} 477 | > 478 | {timeSettings.bedTime} 479 | 480 | to 481 | showTimePicker('wakeUpTime')} 484 | > 485 | {timeSettings.wakeUpTime} 486 | 487 | 488 | 489 | 490 | 491 | Work Hours 492 | 493 | showTimePicker('workStartTime')} 496 | > 497 | {timeSettings.workStartTime} 498 | 499 | to 500 | showTimePicker('workEndTime')} 503 | > 504 | {timeSettings.workEndTime} 505 | 506 | 507 | 508 | 509 | {/* 510 | Preferred Notification Times 511 | 512 | Coming Soon 513 | 514 | */} 515 | 516 | 517 | {/* 518 | Appearance 519 | 520 | Theme 521 | handleSettingChange('theme', value)} 525 | > 526 | 527 | 528 | 529 | 530 | 531 | */} 532 | 533 | {/* 534 | Defaults 535 | 536 | Default Priority 537 | handleSettingChange('defaultPriority', value)} 541 | > 542 | 543 | 544 | 545 | 546 | 547 | */} 548 | 549 | 550 | 556 | Made with Love 557 | 558 | This app is completely free and we collect absolutely no data from our users. 559 | We believe in creating tools that respect your privacy while helping you stay productive. 560 | 561 | Linking.openURL('https://ko-fi.com/dotomo')} 564 | activeOpacity={0.8} 565 | > 566 | 567 | Support the App 568 | 569 | 570 | 571 | 572 | {isSaving && ( 573 | 574 | 575 | Saving... 576 | 577 | )} 578 | 579 | {timePicker.show && ( 580 | 590 | setTimePicker(prev => ({ ...prev, show: false }))} 600 | activeOpacity={1} 601 | /> 602 | 603 | 617 | 626 | setTimePicker(prev => ({ ...prev, show: false }))} 628 | style={{ minWidth: 60 }} 629 | > 630 | 634 | Cancel 635 | 636 | 637 | 638 | 643 | {(() => { 644 | switch(timePicker.current) { 645 | case 'wakeUpTime': return 'Wake-up Time'; 646 | case 'bedTime': return 'Bedtime'; 647 | case 'workStartTime': return 'Work Start'; 648 | case 'workEndTime': return 'Work End'; 649 | default: return 'Select Time'; 650 | } 651 | })()} 652 | 653 | 654 | { 656 | const [hours, minutes] = timeSettings[timePicker.current!].split(':'); 657 | const date = new Date(); 658 | date.setHours(parseInt(hours, 10)); 659 | date.setMinutes(parseInt(minutes, 10)); 660 | handleTimePickerConfirm(date); 661 | }} 662 | style={{ minWidth: 60, alignItems: 'flex-end' }} 663 | > 664 | 669 | Done 670 | 671 | 672 | 673 | 674 | { 676 | const [hours, minutes] = (timeSettings[timePicker.current!] || '00:00').split(':'); 677 | const date = new Date(); 678 | date.setHours(parseInt(hours, 10)); 679 | date.setMinutes(parseInt(minutes, 10)); 680 | return date; 681 | })()} 682 | minimumDate={(() => { 683 | if (timePicker.current === 'workEndTime') { 684 | const [hours, minutes] = timeSettings.workStartTime.split(':'); 685 | const date = new Date(); 686 | date.setHours(parseInt(hours, 10)); 687 | date.setMinutes(parseInt(minutes, 10)); 688 | return date; 689 | } 690 | return undefined; 691 | })()} 692 | mode={timePicker.mode} 693 | is24Hour={true} 694 | display="spinner" 695 | onChange={(event, date) => { 696 | if (date && event.type !== 'dismissed') { 697 | const timeString = format(date, 'HH:mm'); 698 | setTimeSettings(prev => ({ 699 | ...prev, 700 | [timePicker.current!]: timeString 701 | })); 702 | } 703 | }} 704 | textColor="#FFFFFF" 705 | themeVariant="dark" 706 | style={{ 707 | height: 200, 708 | backgroundColor: '#1C1C1E', 709 | }} 710 | /> 711 | 712 | 713 | )} 714 | 715 | ); 716 | } -------------------------------------------------------------------------------- /src/screens/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Stack } from 'expo-router'; 2 | import { Platform } from 'react-native'; 3 | 4 | export default function Layout() { 5 | return ( 6 | 22 | ); 23 | } -------------------------------------------------------------------------------- /src/services/ApiService.ts: -------------------------------------------------------------------------------- 1 | import { LoggingService } from './LoggingService'; 2 | 3 | export class ApiError extends Error { 4 | constructor( 5 | message: string, 6 | public statusCode?: number, 7 | public response?: any 8 | ) { 9 | super(message); 10 | this.name = 'ApiError'; 11 | } 12 | } 13 | 14 | export class ApiService { 15 | static async handleRequest( 16 | request: Promise, 17 | endpoint: string 18 | ): Promise { 19 | try { 20 | const response = await request; 21 | const data = await response.json(); 22 | 23 | if (!response.ok) { 24 | throw new ApiError( 25 | data.message || 'API request failed', 26 | response.status, 27 | data 28 | ); 29 | } 30 | 31 | await LoggingService.info(`API call successful: ${endpoint}`, { 32 | status: response.status, 33 | }); 34 | 35 | return data; 36 | } catch (error) { 37 | if (error instanceof ApiError) { 38 | throw error; 39 | } 40 | 41 | await LoggingService.error( 42 | error instanceof Error ? error : new Error('Unknown error'), 43 | { endpoint } 44 | ); 45 | 46 | throw new ApiError('Network request failed'); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/services/DatabaseService.ts: -------------------------------------------------------------------------------- 1 | import * as SQLite from 'expo-sqlite'; 2 | import { Todo } from '../types/Todo'; 3 | 4 | class DatabaseService { 5 | private static instance: DatabaseService; 6 | private db: SQLite.SQLiteDatabase | null = null; 7 | 8 | private constructor() {} 9 | 10 | static getInstance(): DatabaseService { 11 | if (!DatabaseService.instance) { 12 | DatabaseService.instance = new DatabaseService(); 13 | } 14 | return DatabaseService.instance; 15 | } 16 | 17 | async initialize(): Promise { 18 | if (!this.db) { 19 | this.db = await SQLite.openDatabaseAsync('todos.db'); 20 | await this.db.execAsync(` 21 | PRAGMA journal_mode = WAL; 22 | CREATE TABLE IF NOT EXISTS todos ( 23 | id TEXT PRIMARY KEY, 24 | title TEXT NOT NULL, 25 | description TEXT, 26 | dueDate TEXT NOT NULL, 27 | completed INTEGER NOT NULL DEFAULT 0, 28 | tags TEXT, 29 | priority TEXT NOT NULL 30 | ); 31 | `); 32 | } 33 | } 34 | 35 | async getTodos(): Promise { 36 | if (!this.db) throw new Error('Database not initialized'); 37 | const result = await this.db.getAllAsync('SELECT * FROM todos'); 38 | return result.map(row => ({ 39 | ...row, 40 | dueDate: new Date(row.dueDate), 41 | completed: Boolean(row.completed), 42 | tags: JSON.parse(row.tags || '[]') 43 | })); 44 | } 45 | 46 | async addTodo(todo: Omit): Promise { 47 | if (!this.db) throw new Error('Database not initialized'); 48 | const id = Math.random().toString(36).slice(2); 49 | await this.db.runAsync( 50 | 'INSERT INTO todos (id, title, description, dueDate, completed, tags, priority) VALUES (?, ?, ?, ?, ?, ?, ?)', 51 | [ 52 | id, 53 | todo.title, 54 | todo.description || null, 55 | todo.dueDate.toISOString(), 56 | todo.completed ? 1 : 0, 57 | JSON.stringify(todo.tags), 58 | todo.priority 59 | ] 60 | ); 61 | } 62 | 63 | async deleteTodo(id: string): Promise { 64 | if (!this.db) throw new Error('Database not initialized'); 65 | await this.db.runAsync('DELETE FROM todos WHERE id = ?', [id]); 66 | } 67 | 68 | async updateTodo(todo: Todo): Promise { 69 | if (!this.db) throw new Error('Database not initialized'); 70 | await this.db.runAsync( 71 | 'UPDATE todos SET title = ?, description = ?, dueDate = ?, completed = ?, tags = ?, priority = ? WHERE id = ?', 72 | [ 73 | todo.title, 74 | todo.description || null, 75 | todo.dueDate.toISOString(), 76 | todo.completed ? 1 : 0, 77 | JSON.stringify(todo.tags), 78 | todo.priority, 79 | todo.id 80 | ] 81 | ); 82 | } 83 | } 84 | 85 | export const db = DatabaseService.getInstance(); -------------------------------------------------------------------------------- /src/services/LLMService.ts: -------------------------------------------------------------------------------- 1 | import { Todo } from '../contexts/TodoContext'; 2 | import { LoggingService } from './LoggingService'; 3 | import { format } from 'date-fns'; 4 | import Constants from 'expo-constants'; 5 | import { AppSettings } from '../contexts/SettingsContext'; 6 | 7 | const API_URL = 'https://api.openai.com/v1/chat/completions'; 8 | const MAX_RETRIES = 3; 9 | const RETRY_DELAY = 1000; 10 | 11 | function createPrompt(todo: Todo): string { 12 | return `Create a brief, engaging notification for this task: 13 | Title: ${todo.title} 14 | Description: ${todo.description || 'N/A'} 15 | Due Date: ${format(todo.dueDate, 'PPP')} 16 | Tags: ${todo.tags.join(', ') || 'none'} 17 | 18 | Make it motivational and concise (under 70 characters). Focus on urgency and importance.`; 19 | } 20 | 21 | interface TimingRecommendation { 22 | recommendedTime: string; // HH:mm format 23 | reasoning: string; 24 | confidence: number; // 0-1 25 | } 26 | 27 | function createTimingPrompt(todo: Todo, settings?: AppSettings): string { 28 | if (!settings) { 29 | console.log('⚠️ No settings provided, using context defaults'); 30 | return ''; // Or handle this case differently 31 | } 32 | 33 | const formatTimeValue = (time: string) => { 34 | if (!time) return 'Not set'; 35 | return time.includes(':') ? time : `${time}:00`; 36 | }; 37 | 38 | console.log('================ 📅 USER SCHEDULE ================'); 39 | console.log(`🌅 Wake Up: ${formatTimeValue(settings.wakeUpTime)}`); 40 | console.log(`🌙 Bed Time: ${formatTimeValue(settings.bedTime)}`); 41 | console.log(`💼 Work Start: ${formatTimeValue(settings.workStartTime)}`); 42 | console.log(`🏡 Work End: ${formatTimeValue(settings.workEndTime)}`); 43 | console.log('==============================================='); 44 | 45 | return `Analyze this task and recommend the optimal notification time for tomorrow: 46 | Title: ${todo.title} 47 | Description: ${todo.description || 'N/A'} 48 | Due Date: ${format(todo.dueDate, 'PPP')} 49 | Tags: ${todo.tags.join(', ') || 'none'} 50 | 51 | User Schedule: 52 | - Wake up time: ${formatTimeValue(settings.wakeUpTime)} 53 | - Bed time: ${formatTimeValue(settings.bedTime)} 54 | - Work hours: ${formatTimeValue(settings.workStartTime)} to ${formatTimeValue(settings.workEndTime)} 55 | 56 | Consider: 57 | 1. Task urgency and importance 58 | 2. User's specified working hours 59 | 3. Task complexity and preparation needs 60 | 4. Human productivity patterns 61 | 62 | Respond in JSON format: 63 | { 64 | "recommendedTime": "HH:mm", 65 | "reasoning": "brief explanation", 66 | "confidence": 0.0-1.0 67 | }`; 68 | } 69 | 70 | export async function generateReminder(todo: Todo): Promise { 71 | const apiKey = Constants.expoConfig?.extra?.openAiKey; 72 | 73 | if (!apiKey) { 74 | LoggingService.error('OpenAI API key not found'); 75 | throw new Error('OpenAI API key is required. Please add it to your .env file'); 76 | } 77 | 78 | for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { 79 | try { 80 | const response = await fetch(API_URL, { 81 | method: 'POST', 82 | headers: { 83 | 'Content-Type': 'application/json', 84 | 'Authorization': `Bearer ${apiKey}`, 85 | }, 86 | body: JSON.stringify({ 87 | model: 'gpt-3.5-turbo', 88 | messages: [ 89 | { 90 | role: 'system', 91 | content: 'You are an AI designed to remind users of their tasks in a style that\'s sharp, direct, and infused with humor and a lot of insult, reminiscent of a motivational coach who uses tough love.', 92 | }, 93 | { 94 | role: 'user', 95 | content: createPrompt(todo), 96 | }, 97 | ], 98 | max_tokens: 60, 99 | temperature: 0.7, 100 | }), 101 | }); 102 | 103 | if (!response.ok) { 104 | const errorData = await response.json(); 105 | throw new Error(`API error: ${response.status} - ${JSON.stringify(errorData)}`); 106 | } 107 | 108 | const data = await response.json(); 109 | return data.choices[0].message.content.trim(); 110 | } catch (error) { 111 | LoggingService.error('LLM API error', { error, attempt, todoId: todo.id }); 112 | 113 | if (attempt === MAX_RETRIES - 1) { 114 | throw error; 115 | } 116 | await new Promise(resolve => setTimeout(resolve, RETRY_DELAY)); 117 | } 118 | } 119 | 120 | throw new Error('Failed to generate reminder after max retries'); 121 | } 122 | 123 | export async function generateTaskBreakdown(todo: Partial): Promise { 124 | const apiKey = Constants.expoConfig?.extra?.openAiKey; 125 | 126 | if (!apiKey) { 127 | LoggingService.error('OpenAI API key not found'); 128 | throw new Error('OpenAI API key is required'); 129 | } 130 | 131 | try { 132 | const response = await fetch(API_URL, { 133 | method: 'POST', 134 | headers: { 135 | 'Content-Type': 'application/json', 136 | 'Authorization': `Bearer ${apiKey}`, 137 | }, 138 | body: JSON.stringify({ 139 | model: 'gpt-3.5-turbo', 140 | messages: [ 141 | { 142 | role: 'system', 143 | content: 'You are a task breakdown assistant. Break down tasks into clear, specific, actionable steps.', 144 | }, 145 | { 146 | role: 'user', 147 | content: `Break down this todo task into exactly 3 specific, actionable steps: 148 | Task: ${todo.title} 149 | ${todo.description ? `Description: ${todo.description}` : ''} 150 | Priority: ${todo.priority || 'medium'}`, 151 | }, 152 | ], 153 | max_tokens: 150, 154 | temperature: 0.7, 155 | }), 156 | }); 157 | 158 | if (!response.ok) { 159 | const errorData = await response.json(); 160 | LoggingService.error('API error response:', errorData); 161 | throw new Error(`API error: ${response.status} - ${JSON.stringify(errorData)}`); 162 | } 163 | 164 | const data = await response.json(); 165 | if (!data.choices?.[0]?.message?.content) { 166 | LoggingService.error('Invalid API response format:', data); 167 | throw new Error('Invalid API response format'); 168 | } 169 | 170 | const content = data.choices[0].message.content.trim(); 171 | const tasks = content.split('\n') 172 | .map(task => task.trim()) 173 | .filter(task => task.length > 0) 174 | .map(task => task.replace(/^[-*•\d.]\s*/, '')); // Remove list markers 175 | 176 | if (tasks.length === 0) { 177 | LoggingService.error('No tasks generated from content:', content); 178 | throw new Error('No tasks generated from API response'); 179 | } 180 | 181 | return tasks; 182 | } catch (error) { 183 | LoggingService.error('Failed to generate task breakdown:', { error, todoId: todo.id }); 184 | // Return a default task list instead of empty array 185 | return [ 186 | `Start working on ${todo.title}`, 187 | 'Review progress', 188 | 'Complete and verify' 189 | ]; 190 | } 191 | } 192 | 193 | export async function generateTimingRecommendation(todo: Todo, settings?: AppSettings): Promise { 194 | if (!settings) { 195 | console.warn('No settings provided to generateTimingRecommendation'); 196 | // Use the default settings from SettingsContext 197 | settings = DEFAULT_SETTINGS; 198 | } 199 | 200 | console.log('\n🔍 LLM Service - Generating Timing Recommendation'); 201 | console.log('📥 User Schedule Settings:', settings || 'Using default schedule'); 202 | 203 | const apiKey = Constants.expoConfig?.extra?.openAiKey; 204 | 205 | if (!apiKey) { 206 | console.log('❌ No API key found!'); 207 | LoggingService.error('OpenAI API key not found'); 208 | throw new Error('OpenAI API key is required'); 209 | } 210 | 211 | try { 212 | console.log('🤖 Sending request to OpenAI...'); 213 | const response = await fetch(API_URL, { 214 | method: 'POST', 215 | headers: { 216 | 'Content-Type': 'application/json', 217 | 'Authorization': `Bearer ${apiKey}`, 218 | }, 219 | body: JSON.stringify({ 220 | model: 'gpt-3.5-turbo', 221 | messages: [ 222 | { 223 | role: 'system', 224 | content: 'You are an AI assistant that analyzes tasks and recommends optimal notification timing. Always respond in valid JSON format.', 225 | }, 226 | { 227 | role: 'user', 228 | content: createTimingPrompt(todo, settings), 229 | }, 230 | ], 231 | max_tokens: 150, 232 | temperature: 0.7, 233 | }), 234 | }); 235 | 236 | if (!response.ok) { 237 | console.log('❌ API Response Error:', response.status); 238 | throw new Error(`API error: ${response.status}`); 239 | } 240 | 241 | const data = await response.json(); 242 | console.log('📊 Raw API Response:', data); 243 | 244 | const recommendation = JSON.parse(data.choices[0].message.content); 245 | console.log('✅ Parsed Recommendation:', recommendation); 246 | 247 | return { 248 | recommendedTime: recommendation.recommendedTime, 249 | reasoning: recommendation.reasoning, 250 | confidence: recommendation.confidence, 251 | }; 252 | } catch (error) { 253 | console.log('❌ Error in generateTimingRecommendation:', error); 254 | LoggingService.error('Failed to generate timing recommendation:', { error, todoId: todo.id }); 255 | // Default to 9 AM if there's an error 256 | return { 257 | recommendedTime: "09:00", 258 | reasoning: "Default morning reminder due to API error", 259 | confidence: 0.5 260 | }; 261 | } 262 | } -------------------------------------------------------------------------------- /src/services/LoggingService.ts: -------------------------------------------------------------------------------- 1 | import Constants from 'expo-constants'; 2 | 3 | interface LogData { 4 | [key: string]: any; 5 | } 6 | 7 | class LoggingServiceClass { 8 | private isInitialized = false; 9 | private readonly environment = Constants.expoConfig?.extra?.environment || 'development'; 10 | 11 | initialize() { 12 | if (this.isInitialized) { 13 | return; 14 | } 15 | this.isInitialized = true; 16 | this.info('LoggingService initialized', { environment: this.environment }); 17 | } 18 | 19 | info(message: string, data?: LogData) { 20 | this.ensureInitialized(); 21 | this.log('INFO', message, data); 22 | } 23 | 24 | error(message: string, error?: Error | LogData) { 25 | this.ensureInitialized(); 26 | let errorData: LogData; 27 | 28 | if (error instanceof Error) { 29 | errorData = { 30 | name: error.name, 31 | message: error.message, 32 | stack: error.stack, 33 | }; 34 | } else if (error && typeof error === 'object') { 35 | errorData = error; 36 | } else { 37 | errorData = { details: error }; 38 | } 39 | 40 | this.log('ERROR', message, errorData); 41 | } 42 | 43 | warn(message: string, data?: LogData) { 44 | this.ensureInitialized(); 45 | this.log('WARN', message, data); 46 | } 47 | 48 | private ensureInitialized() { 49 | if (!this.isInitialized) { 50 | this.initialize(); 51 | } 52 | } 53 | 54 | private log(level: 'INFO' | 'ERROR' | 'WARN', message: string, data?: any) { 55 | try { 56 | const timestamp = new Date().toLocaleString('en-US', { 57 | year: 'numeric', 58 | month: '2-digit', 59 | day: '2-digit', 60 | hour: '2-digit', 61 | minute: '2-digit', 62 | second: '2-digit', 63 | hour12: false, 64 | }); 65 | 66 | const logData = { 67 | timestamp, 68 | level, 69 | message, 70 | data: data || {}, 71 | environment: this.environment, 72 | }; 73 | 74 | const formattedData = JSON.stringify(logData.data, null, 2); 75 | const separator = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; 76 | 77 | switch (level) { 78 | case 'ERROR': 79 | console.error(`\n${separator}\n❌ [${timestamp}] ERROR: ${message}\n${formattedData}\n${separator}\n`); 80 | break; 81 | case 'WARN': 82 | console.warn(`\n${separator}\n⚠️ [${timestamp}] WARNING: ${message}\n${formattedData}\n${separator}\n`); 83 | break; 84 | default: 85 | console.log(`\n${separator}\n📝 [${timestamp}] INFO: ${message}\n${formattedData}\n${separator}\n`); 86 | } 87 | } catch (err) { 88 | console.error('❌ Logging failed:', err); 89 | } 90 | } 91 | } 92 | 93 | export const LoggingService = new LoggingServiceClass(); -------------------------------------------------------------------------------- /src/services/MockLLMService.ts: -------------------------------------------------------------------------------- 1 | import { Todo } from '../contexts/TodoContext'; 2 | import { format } from 'date-fns'; 3 | 4 | const PRIORITY_PHRASES = { 5 | high: ['This is a high-priority task that needs your attention.', 'Don\'t forget this important task!'], 6 | medium: ['Keep this task in mind.', 'Make sure to complete this task on time.'], 7 | low: ['When you have time, take care of this task.', 'This task is on your list.'], 8 | }; 9 | 10 | const TIME_PHRASES = { 11 | soon: 'The deadline is approaching.', 12 | future: 'You have some time, but plan accordingly.', 13 | urgent: 'This task is due very soon!', 14 | }; 15 | 16 | export async function mockGenerateNotificationContent(todo: Todo): Promise { 17 | const priorityPhrase = PRIORITY_PHRASES[todo.priority][Math.floor(Math.random() * PRIORITY_PHRASES[todo.priority].length)]; 18 | 19 | const timeUntilDue = todo.dueDate.getTime() - Date.now(); 20 | const hoursUntilDue = timeUntilDue / (1000 * 60 * 60); 21 | 22 | let timePhrase = TIME_PHRASES.future; 23 | if (hoursUntilDue < 2) timePhrase = TIME_PHRASES.urgent; 24 | else if (hoursUntilDue < 24) timePhrase = TIME_PHRASES.soon; 25 | 26 | const content = [ 27 | priorityPhrase, 28 | timePhrase, 29 | `Due: ${format(todo.dueDate, 'PPP')}`, 30 | todo.description && `Details: ${todo.description}`, 31 | todo.tags.length > 0 && `Tags: ${todo.tags.join(', ')}`, 32 | ] 33 | .filter(Boolean) 34 | .join('\n'); 35 | 36 | return content; 37 | } -------------------------------------------------------------------------------- /src/services/NLPService.ts: -------------------------------------------------------------------------------- 1 | import { ParsedTodo, TokenMatch } from '../types/nlp'; 2 | 3 | export class NLPService { 4 | private static datePatterns = [ 5 | { 6 | pattern: /today/i, 7 | getValue: () => new Date(), 8 | }, 9 | { 10 | pattern: /tomorrow/i, 11 | getValue: () => { 12 | const date = new Date(); 13 | date.setDate(date.getDate() + 1); 14 | return date; 15 | }, 16 | }, 17 | { 18 | pattern: /next (monday|tuesday|wednesday|thursday|friday|saturday|sunday)/i, 19 | getValue: (match: string) => { 20 | const days = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']; 21 | const targetDay = days.indexOf(match.split(' ')[1].toLowerCase()); 22 | const today = new Date(); 23 | const currentDay = today.getDay(); 24 | const daysUntilTarget = (targetDay + 7 - currentDay) % 7; 25 | const result = new Date(); 26 | result.setDate(today.getDate() + daysUntilTarget); 27 | return result; 28 | }, 29 | }, 30 | { 31 | pattern: /in (\d+) (day|days|week|weeks)/i, 32 | getValue: (match: string) => { 33 | const [_, number, unit] = match.toLowerCase().match(/in (\d+) (day|days|week|weeks)/i) || []; 34 | const date = new Date(); 35 | const value = parseInt(number); 36 | if (unit.startsWith('week')) { 37 | date.setDate(date.getDate() + (value * 7)); 38 | } else { 39 | date.setDate(date.getDate() + value); 40 | } 41 | return date; 42 | }, 43 | }, 44 | ]; 45 | 46 | private static priorityPatterns = [ 47 | { pattern: /!high|!h|!1/i, value: 'high' }, 48 | { pattern: /!medium|!m|!2/i, value: 'medium' }, 49 | { pattern: /!low|!l|!3/i, value: 'low' }, 50 | ]; 51 | 52 | static parseTodo(input: string): ParsedTodo { 53 | const matches: TokenMatch[] = []; 54 | let dueDate: Date | null = null; 55 | let priority: ParsedTodo['priority'] = null; 56 | const tags: string[] = []; 57 | 58 | // Find date patterns 59 | for (const { pattern, getValue } of this.datePatterns) { 60 | const match = input.match(pattern); 61 | if (match) { 62 | dueDate = getValue(match[0]); 63 | matches.push({ 64 | type: 'date', 65 | value: match[0], 66 | index: match.index!, 67 | }); 68 | } 69 | } 70 | 71 | // Find priority patterns 72 | for (const { pattern, value } of this.priorityPatterns) { 73 | const match = input.match(pattern); 74 | if (match) { 75 | priority = value as ParsedTodo['priority']; 76 | matches.push({ 77 | type: 'priority', 78 | value: match[0], 79 | index: match.index!, 80 | }); 81 | } 82 | } 83 | 84 | // Find tags (#tag) 85 | const tagMatches = input.match(/#\w+/g) || []; 86 | tagMatches.forEach((tag) => { 87 | tags.push(tag.slice(1)); 88 | matches.push({ 89 | type: 'tag', 90 | value: tag, 91 | index: input.indexOf(tag), 92 | }); 93 | }); 94 | 95 | // Clean up the title by removing matched patterns 96 | let title = input; 97 | matches 98 | .sort((a, b) => b.index - a.index) 99 | .forEach((match) => { 100 | title = title.slice(0, match.index) + title.slice(match.index + match.value.length); 101 | }); 102 | 103 | title = title.trim(); 104 | 105 | return { 106 | title, 107 | dueDate, 108 | priority, 109 | tags, 110 | }; 111 | } 112 | 113 | static getSuggestions(input: string): string[] { 114 | const suggestions: string[] = []; 115 | 116 | if (!input.match(/today|tomorrow|next|in \d+/i)) { 117 | suggestions.push('Add "today", "tomorrow", or "in X days" for due date'); 118 | } 119 | 120 | if (!input.match(/![hml1-3]/i)) { 121 | suggestions.push('Add "!h", "!m", or "!l" for priority'); 122 | } 123 | 124 | if (!input.match(/#\w+/)) { 125 | suggestions.push('Add "#tag" to categorize your todo'); 126 | } 127 | 128 | return suggestions; 129 | } 130 | } -------------------------------------------------------------------------------- /src/services/NotificationService.ts: -------------------------------------------------------------------------------- 1 | import * as Notifications from 'expo-notifications'; 2 | import * as Device from 'expo-device'; 3 | import { Platform } from 'react-native'; 4 | import { generateReminder, generateTimingRecommendation } from './LLMService'; 5 | import { LoggingService } from './LoggingService'; 6 | import { Todo } from '../contexts/TodoContext'; 7 | // import { NotificationPreferences } from '../contexts/SettingsContext'; 8 | import { format } from 'date-fns'; 9 | import { NotificationPreferences } from '../types/settings'; 10 | 11 | Notifications.setNotificationHandler({ 12 | handleNotification: async () => ({ 13 | shouldShowAlert: true, 14 | shouldPlaySound: true, 15 | shouldSetBadge: true, 16 | }), 17 | }); 18 | 19 | export class NotificationService { 20 | static async sendImmediateNotification(todo: Todo, settings: NotificationPreferences) { 21 | console.log('⚠️ WARNING: Using immediate notification - should only be used for testing'); 22 | try { 23 | const hasPermission = await this.requestPermissions(); 24 | if (!hasPermission) { 25 | LoggingService.warn('Notification permissions not granted'); 26 | return; 27 | } 28 | 29 | // Get AI-generated reminder using LLM service 30 | const reminder = await generateReminder(todo); 31 | 32 | await Notifications.scheduleNotificationAsync({ 33 | content: { 34 | title: `[TEST] Task Reminder: ${todo.title}`, 35 | body: `[TEST] ${reminder}`, 36 | data: { todoId: todo.id }, 37 | sound: settings.sound, 38 | vibrate: settings.vibration ? [0, 250, 250, 250] : undefined, 39 | badge: 1, 40 | }, 41 | trigger: null, // immediate notification 42 | }); 43 | 44 | LoggingService.info('LLM notification sent', { 45 | todoId: todo.id, 46 | reminderText: reminder, 47 | }); 48 | } catch (error) { 49 | LoggingService.error('Failed to send LLM notification', { 50 | error, 51 | todoId: todo.id 52 | }); 53 | } 54 | } 55 | 56 | static async scheduleSmartNotification(todo: Todo, notificationPrefs: NotificationPreferences) { 57 | try { 58 | console.log('\n🚀 STARTING SMART NOTIFICATION SCHEDULING...'); 59 | 60 | const hasPermission = await this.requestPermissions(); 61 | if (!hasPermission) { 62 | console.log('❌ No notification permissions!'); // Debug log 63 | LoggingService.warn('Notification permissions not granted'); 64 | return; 65 | } 66 | 67 | console.log('✅ Permissions OK, getting LLM recommendations...'); // Debug log 68 | 69 | // Get AI-generated timing recommendation 70 | const timing = await generateTimingRecommendation(todo); 71 | console.log('📊 Received LLM timing:', timing); 72 | 73 | // Get AI-generated reminder text 74 | const reminder = await generateReminder(todo); 75 | 76 | // Parse the recommended time 77 | const [hours, minutes] = timing.recommendedTime.split(':').map(Number); 78 | 79 | // Create notification time based on due date 80 | const notificationTime = new Date(todo.dueDate); 81 | notificationTime.setHours(hours); 82 | notificationTime.setMinutes(minutes); 83 | notificationTime.setSeconds(0); 84 | notificationTime.setMilliseconds(0); 85 | 86 | // Safety check - ensure notification is at least 1 minute in the future 87 | const now = new Date(); 88 | if (notificationTime <= now) { 89 | console.log('⚠️ Warning: Notification time was in the past or too soon'); 90 | // Move to tomorrow 91 | notificationTime.setDate(notificationTime.getDate() + 1); 92 | } 93 | 94 | // Double check we're not scheduling too soon 95 | if (notificationTime.getTime() - now.getTime() < 60000) { // 60000ms = 1 minute 96 | throw new Error('Notification time too soon - must be at least 1 minute in the future'); 97 | } 98 | 99 | console.log('🕒 Scheduling notification for:', notificationTime.toLocaleString()); 100 | 101 | console.log('🕒 Notification scheduling details:', { 102 | currentTime: new Date().toLocaleString(), 103 | dueDate: new Date(todo.dueDate).toLocaleString(), 104 | recommendedTime: timing.recommendedTime, 105 | finalNotificationTime: notificationTime.toLocaleString() 106 | }); 107 | 108 | console.log('\n📅 TRIGGER DETAILS 📅'); 109 | console.log('------------------'); 110 | console.log(`🎯 Trigger Type: date`); 111 | console.log(`⏰ Scheduled For: ${format(notificationTime, 'PPP HH:mm:ss')}`); 112 | console.log('------------------\n'); 113 | 114 | // Add debug log 115 | console.log('Scheduling for timestamp:', notificationTime.getTime(), 'Current time:', Date.now()); 116 | 117 | // Before scheduling, log intended time 118 | console.log('\n🎯 SCHEDULING ATTEMPT:'); 119 | console.log('Intended time:', notificationTime.toLocaleString()); 120 | 121 | // Calculate seconds from now until notification time 122 | const secondsUntilNotification = Math.floor((notificationTime.getTime() - Date.now()) / 1000); 123 | 124 | console.log('Using seconds-based trigger:', { 125 | secondsUntilNotification, 126 | fromTime: new Date().toLocaleString(), 127 | targetTime: notificationTime.toLocaleString() 128 | }); 129 | 130 | await Notifications.scheduleNotificationAsync({ 131 | content: { 132 | title: `Task Reminder: ${todo.title}`, 133 | body: `${reminder}`, 134 | data: { 135 | todoId: todo.id, 136 | timing: timing.reasoning, 137 | intendedTime: notificationTime.getTime() 138 | }, 139 | sound: notificationPrefs.sound, 140 | vibrate: notificationPrefs.vibration ? [0, 250, 250, 250] : undefined, 141 | badge: 1, 142 | }, 143 | trigger: { 144 | type: 'timeInterval', // Changed from 'date' to 'timeInterval' 145 | seconds: secondsUntilNotification, // Use seconds from now 146 | channelId: Platform.OS === 'android' ? 'default' : undefined, 147 | }, 148 | }); 149 | 150 | // Add this debug log right after scheduling 151 | const scheduledNotifs = await Notifications.getAllScheduledNotificationsAsync(); 152 | const thisNotif = scheduledNotifs.find(n => n.content.data?.todoId === todo.id); 153 | console.log('IMMEDIATE VERIFICATION:', { 154 | found: !!thisNotif, 155 | scheduledTrigger: thisNotif?.trigger, 156 | expectedTimestamp: notificationTime.getTime(), 157 | expectedDate: new Date(notificationTime).toLocaleString() 158 | }); 159 | 160 | // 1. Verify immediate scheduling success 161 | const verifiedNotifications = await Notifications.getAllScheduledNotificationsAsync(); 162 | const thisNotification = verifiedNotifications.find(n => n.content.data?.todoId === todo.id); 163 | 164 | if (!thisNotification) { 165 | throw new Error('Notification was not scheduled successfully'); 166 | } 167 | 168 | // 2. Calculate actual scheduled time based on timeInterval 169 | const actualScheduledTime = new Date(Date.now() + ((thisNotification.trigger as any).seconds * 1000)); 170 | 171 | // 3. Verify all scheduled notifications 172 | const scheduledNotifications = await Notifications.getAllScheduledNotificationsAsync(); 173 | console.log('\n📋 SCHEDULED NOTIFICATIONS:'); 174 | scheduledNotifications.forEach(notification => { 175 | console.log(`- Title: ${notification.content.title}`); 176 | console.log('Raw trigger:', notification.trigger); 177 | const triggerDate = (notification.trigger as any).date; 178 | console.log('Trigger date value:', triggerDate); 179 | console.log(` Scheduled for: ${triggerDate ? new Date(triggerDate).toLocaleString() : 'Invalid date'}`); 180 | }); 181 | 182 | // 4. Get iOS notification settings to verify permissions 183 | const settings = await Notifications.getPermissionsAsync(); 184 | if (!settings.granted) { 185 | throw new Error('Notification permissions not granted'); 186 | } 187 | 188 | // Verify the notification was scheduled 189 | let verifiedScheduledTime: Date | undefined; 190 | if (thisNotification.trigger) { 191 | if ((thisNotification.trigger as any).type === 'date') { 192 | verifiedScheduledTime = new Date((thisNotification.trigger as any).date); 193 | } else if ((thisNotification.trigger as any).type === 'timeInterval') { 194 | verifiedScheduledTime = new Date(Date.now() + ((thisNotification.trigger as any).seconds * 1000)); 195 | } 196 | } 197 | 198 | if (!verifiedScheduledTime || isNaN(verifiedScheduledTime.getTime())) { 199 | console.log('⚠️ ERROR: Invalid scheduled date'); 200 | console.log('Notification trigger:', JSON.stringify(thisNotification.trigger, null, 2)); 201 | } 202 | 203 | // 5. Final verification summary 204 | console.log('\n🚨 FINAL NOTIFICATION DETAILS 🚨'); 205 | console.log('--------------------------------'); 206 | console.log(`📅 SCHEDULED TIME: ${format(notificationTime, 'PPP HH:mm:ss')}`); 207 | console.log(`📝 TITLE: ${todo.title}`); 208 | console.log(`💬 BODY: ${reminder}`); 209 | console.log(`✅ FOUND IN SYSTEM: YES`); 210 | console.log(`📱 PERMISSIONS OK: ${settings.granted}`); 211 | console.log(`⏰ SECONDS UNTIL NOTIFICATION: ${Math.round((notificationTime.getTime() - Date.now()) / 1000)}`); 212 | console.log('--------------------------------\n'); 213 | 214 | // Also log to LoggingService 215 | LoggingService.info('Smart notification scheduled', { 216 | todoId: todo.id, 217 | taskTitle: todo.title, 218 | originalDueDate: format(todo.dueDate, 'PPP HH:mm'), 219 | notificationTime: format(notificationTime, 'PPP HH:mm'), 220 | llmAnalysis: timing 221 | }); 222 | 223 | } catch (error) { 224 | console.log('❌ ERROR scheduling notification:', error); // Debug log 225 | LoggingService.error('Failed to schedule smart notification', { 226 | error, 227 | todoId: todo.id 228 | }); 229 | } 230 | } 231 | 232 | static async requestPermissions() { 233 | if (Platform.OS === 'web') return false; 234 | 235 | if (!Device.isDevice) { 236 | LoggingService.warn('Must use physical device for notifications'); 237 | return false; 238 | } 239 | 240 | try { 241 | const { status: existingStatus } = await Notifications.getPermissionsAsync(); 242 | if (existingStatus === 'granted') return true; 243 | 244 | const { status } = await Notifications.requestPermissionsAsync(); 245 | return status === 'granted'; 246 | } catch (error) { 247 | LoggingService.error('Failed to request permissions', { error }); 248 | return false; 249 | } 250 | } 251 | 252 | static async cancelNotification(todoId: string) { 253 | try { 254 | const notifications = await Notifications.getAllScheduledNotificationsAsync(); 255 | const toCancel = notifications.filter(n => n.content.data?.todoId === todoId); 256 | 257 | await Promise.all( 258 | toCancel.map(n => Notifications.cancelScheduledNotificationAsync(n.identifier)) 259 | ); 260 | } catch (error) { 261 | console.error('Failed to cancel notification:', error); 262 | } 263 | } 264 | } -------------------------------------------------------------------------------- /src/services/SettingsService.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-async-storage/async-storage'; 2 | import { AppSettings } from '../types/settings'; 3 | 4 | const SETTINGS_KEY = 'app_settings'; 5 | 6 | const DEFAULT_SETTINGS: AppSettings = { 7 | notifications: { 8 | enabled: true, 9 | reminderTiming: 1, // 1 hour before 10 | sound: true, 11 | vibration: true, 12 | }, 13 | theme: 'system', 14 | defaultPriority: 'medium', 15 | defaultReminderTime: 1, 16 | showCompletedTodos: true, 17 | }; 18 | 19 | export class SettingsService { 20 | static async getSettings(): Promise { 21 | try { 22 | const settings = await AsyncStorage.getItem(SETTINGS_KEY); 23 | return settings ? { ...DEFAULT_SETTINGS, ...JSON.parse(settings) } : DEFAULT_SETTINGS; 24 | } catch (error) { 25 | console.error('Failed to load settings:', error); 26 | return DEFAULT_SETTINGS; 27 | } 28 | } 29 | 30 | static async updateSettings(settings: Partial): Promise { 31 | try { 32 | const currentSettings = await this.getSettings(); 33 | const newSettings = { ...currentSettings, ...settings }; 34 | await AsyncStorage.setItem(SETTINGS_KEY, JSON.stringify(newSettings)); 35 | } catch (error) { 36 | console.error('Failed to save settings:', error); 37 | throw error; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/services/database.ts: -------------------------------------------------------------------------------- 1 | import * as SQLite from 'expo-sqlite'; 2 | import { Todo } from '../types'; 3 | 4 | const db = SQLite.openDatabase('todos.db'); 5 | 6 | export const DatabaseService = { 7 | init(): Promise { 8 | return new Promise((resolve, reject) => { 9 | db.transaction( 10 | (tx) => { 11 | tx.executeSql( 12 | `CREATE TABLE IF NOT EXISTS todos ( 13 | id TEXT PRIMARY KEY NOT NULL, 14 | title TEXT NOT NULL, 15 | description TEXT, 16 | dueDate TEXT NOT NULL, 17 | completed INTEGER NOT NULL DEFAULT 0, 18 | createdAt TEXT NOT NULL 19 | );`, 20 | [], 21 | () => resolve(), 22 | (_, error) => { 23 | reject(error); 24 | return false; 25 | } 26 | ); 27 | }, 28 | (error) => reject(error) 29 | ); 30 | }); 31 | }, 32 | 33 | getTodos(): Promise { 34 | return new Promise((resolve, reject) => { 35 | db.transaction( 36 | (tx) => { 37 | tx.executeSql( 38 | 'SELECT * FROM todos ORDER BY createdAt DESC;', 39 | [], 40 | (_, { rows: { _array } }) => { 41 | const todos = _array.map((row) => ({ 42 | ...row, 43 | completed: Boolean(row.completed), 44 | dueDate: new Date(row.dueDate), 45 | createdAt: new Date(row.createdAt), 46 | })); 47 | resolve(todos); 48 | }, 49 | (_, error) => { 50 | reject(error); 51 | return false; 52 | } 53 | ); 54 | }, 55 | (error) => reject(error) 56 | ); 57 | }); 58 | }, 59 | 60 | addTodo(todo: Todo): Promise { 61 | return new Promise((resolve, reject) => { 62 | db.transaction( 63 | (tx) => { 64 | tx.executeSql( 65 | `INSERT INTO todos (id, title, description, dueDate, completed, createdAt) 66 | VALUES (?, ?, ?, ?, ?, ?);`, 67 | [ 68 | todo.id, 69 | todo.title, 70 | todo.description, 71 | todo.dueDate.toISOString(), 72 | todo.completed ? 1 : 0, 73 | todo.createdAt.toISOString(), 74 | ], 75 | () => resolve(), 76 | (_, error) => { 77 | reject(error); 78 | return false; 79 | } 80 | ); 81 | }, 82 | (error) => reject(error) 83 | ); 84 | }); 85 | }, 86 | 87 | updateTodo(todo: Todo): Promise { 88 | return new Promise((resolve, reject) => { 89 | db.transaction( 90 | (tx) => { 91 | tx.executeSql( 92 | `UPDATE todos 93 | SET title = ?, description = ?, dueDate = ?, completed = ? 94 | WHERE id = ?;`, 95 | [ 96 | todo.title, 97 | todo.description, 98 | todo.dueDate.toISOString(), 99 | todo.completed ? 1 : 0, 100 | todo.id, 101 | ], 102 | () => resolve(), 103 | (_, error) => { 104 | reject(error); 105 | return false; 106 | } 107 | ); 108 | }, 109 | (error) => reject(error) 110 | ); 111 | }); 112 | }, 113 | 114 | deleteTodo(id: string): Promise { 115 | return new Promise((resolve, reject) => { 116 | db.transaction( 117 | (tx) => { 118 | tx.executeSql( 119 | 'DELETE FROM todos WHERE id = ?;', 120 | [id], 121 | () => resolve(), 122 | (_, error) => { 123 | reject(error); 124 | return false; 125 | } 126 | ); 127 | }, 128 | (error) => reject(error) 129 | ); 130 | }); 131 | }, 132 | }; -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface Todo { 2 | id: string; 3 | title: string; 4 | description?: string; 5 | dueDate: Date; 6 | completed: boolean; 7 | createdAt: Date; 8 | notificationId?: string; 9 | } 10 | 11 | export interface TodoContextType { 12 | todos: Todo[]; 13 | addTodo: (title: string) => void; 14 | toggleTodo: (id: string) => void; 15 | deleteTodo: (id: string) => void; 16 | } -------------------------------------------------------------------------------- /src/types/nlp.ts: -------------------------------------------------------------------------------- 1 | export interface ParsedTodo { 2 | title: string; 3 | dueDate: Date | null; 4 | priority: 'low' | 'medium' | 'high' | null; 5 | tags: string[]; 6 | description?: string; 7 | } 8 | 9 | export interface TokenMatch { 10 | type: 'date' | 'priority' | 'tag'; 11 | value: string; 12 | index: number; 13 | } -------------------------------------------------------------------------------- /src/types/settings.ts: -------------------------------------------------------------------------------- 1 | export interface NotificationPreferences { 2 | enabled: boolean; 3 | reminderTiming: number; // hours before due date 4 | sound: boolean; 5 | vibration: boolean; 6 | } 7 | 8 | export interface AppSettings { 9 | notifications: NotificationPreferences; 10 | theme: 'light' | 'dark' | 'system'; 11 | defaultPriority: 'low' | 'medium' | 'high'; 12 | defaultReminderTime: number; 13 | showCompletedTodos: boolean; 14 | } -------------------------------------------------------------------------------- /src/utils/animations.ts: -------------------------------------------------------------------------------- 1 | import { Animated } from 'react-native'; 2 | 3 | export const startWaveAnimation = (animatedValue: Animated.Value) => { 4 | Animated.loop( 5 | Animated.sequence([ 6 | Animated.timing(animatedValue, { 7 | toValue: 1, 8 | duration: 1000, 9 | useNativeDriver: true, 10 | }), 11 | Animated.timing(animatedValue, { 12 | toValue: 0, 13 | duration: 0, 14 | useNativeDriver: true, 15 | }), 16 | ]) 17 | ).start(); 18 | }; 19 | 20 | export const stopWaveAnimation = (animatedValue: Animated.Value) => { 21 | animatedValue.stopAnimation(); 22 | animatedValue.setValue(0); 23 | }; -------------------------------------------------------------------------------- /src/utils/storage.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-async-storage/async-storage'; 2 | import { Todo } from '../types'; 3 | 4 | const TODOS_KEY = 'todos'; 5 | 6 | export async function saveTodos(todos: Todo[]): Promise { 7 | try { 8 | await AsyncStorage.setItem(TODOS_KEY, JSON.stringify(todos)); 9 | } catch (error) { 10 | console.error('Error saving todos:', error); 11 | } 12 | } 13 | 14 | export async function loadTodos(): Promise { 15 | try { 16 | const todosJson = await AsyncStorage.getItem(TODOS_KEY); 17 | if (todosJson) { 18 | const todos = JSON.parse(todosJson); 19 | // Convert string dates back to Date objects 20 | return todos.map((todo: Todo) => ({ 21 | ...todo, 22 | createdAt: new Date(todo.createdAt), 23 | dueDate: new Date(todo.dueDate), 24 | })); 25 | } 26 | return []; 27 | } catch (error) { 28 | console.error('Error loading todos:', error); 29 | return []; 30 | } 31 | } -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | export interface Todo { 2 | id: string; 3 | title: string; 4 | description?: string; 5 | completed: boolean; 6 | dueDate: Date; 7 | createdAt: Date | string; 8 | // ... other fields 9 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "jsx": "react-native" 6 | } 7 | } 8 | --------------------------------------------------------------------------------