├── .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 |
--------------------------------------------------------------------------------