gitshark

Clone repository

git clone https://git.vilip.de/git/vilip/taskflow.git
git clone git@git.vilip.de:vilip/taskflow.git

← Commits

✨ Add notifications

c700c663d9b5ecce8c69378fd47f84903b7709a5 · vvilip · 2025-11-10T14:51:49Z

Changes

6 files changed, +433 -30

MODIFY app.json +9 -0
diff --git a/app.json b/app.json
index e7e4569..95a4c9c 100644
--- a/app.json
+++ b/app.json
@@ -37,6 +37,15 @@
37 37 "backgroundColor": "#000000"
38 38 }
39 39 }
40 + ],
41 + [
42 + "expo-notifications",
43 + {
44 + "icon": "./assets/images/Logo.png",
45 + "color": "#0a7ea4",
46 + "sounds": [],
47 + "mode": "production"
48 + }
40 49 ]
41 50 ],
42 51 "experiments": {
MODIFY app/(tabs)/settings.tsx +85 -1
diff --git "a/app/\050tabs\051/settings.tsx" "b/app/\050tabs\051/settings.tsx"
index 6e544d2..3a81ead 100644
--- "a/app/\050tabs\051/settings.tsx"
+++ "b/app/\050tabs\051/settings.tsx"
@@ -5,7 +5,7 @@
5 5 import * as DocumentPicker from 'expo-document-picker';
6 6 import { ThemedView } from '@/components/themed-view';
7 7 import { ThemedText } from '@/components/themed-text';
8 -import { storageService, taskService, webdavService } from '@/services';
8 +import { storageService, taskService, webdavService, notificationService } from '@/services';
9 9 import { useTheme } from '@/contexts/theme-context';
10 10 import { Colors } from '@/constants/theme';
11 11 import { useColorScheme } from '@/hooks/use-color-scheme';
@@ -13,6 +13,8 @@
13 13 export default function SettingsScreen() {
14 14 const [syncing, setSyncing] = useState(false);
15 15 const [autoArchive, setAutoArchive] = useState(false);
16 + const [dailyNotifications, setDailyNotifications] = useState(false);
17 + const [notificationsEnabled, setNotificationsEnabled] = useState(false);
16 18 const [webdavConfigured, setWebdavConfigured] = useState(false);
17 19 const [webdavInfo, setWebdavInfo] = useState<{ url: string; username: string } | null>(null);
18 20 const { themeMode, setThemeMode } = useTheme();
@@ -22,6 +24,7 @@
22 24 useFocusEffect(
23 25 React.useCallback(() => {
24 26 checkWebDAVStatus();
27 + checkNotificationStatus();
25 28 }, [])
26 29 );
27 30
@@ -33,6 +36,15 @@
33 36 }
34 37 };
35 38
39 + const checkNotificationStatus = async () => {
40 + const enabled = await notificationService.isEnabled();
41 + setNotificationsEnabled(enabled);
42 +
43 + // Check if there are scheduled notifications
44 + const scheduled = await notificationService.getScheduledNotifications();
45 + setDailyNotifications(scheduled.length > 0);
46 + };
47 +
36 48 const handleExport = async () => {
37 49 try {
38 50 await storageService.exportToFile();
@@ -187,6 +199,47 @@
187 199 );
188 200 };
189 201
202 + const handleToggleDailyNotifications = async (enabled: boolean) => {
203 + if (enabled) {
204 + const hasPermission = await notificationService.requestPermissions();
205 + if (!hasPermission) {
206 + Alert.alert(
207 + 'Permission Required',
208 + 'Please enable notifications in your device settings to receive daily task reminders.'
209 + );
210 + return;
211 + }
212 +
213 + const success = await notificationService.scheduleDailyNotificationWithCount();
214 + if (success) {
215 + setDailyNotifications(true);
216 + setNotificationsEnabled(true);
217 + Alert.alert(
218 + 'Notifications Enabled',
219 + 'You will receive a daily reminder at 12:00 PM'
220 + );
221 + }
222 + } else {
223 + await notificationService.cancelDailyNotification();
224 + setDailyNotifications(false);
225 + Alert.alert('Notifications Disabled', 'Daily reminders have been turned off');
226 + }
227 + };
228 +
229 + const handleTestNotification = async () => {
230 + const hasPermission = await notificationService.requestPermissions();
231 + if (!hasPermission) {
232 + Alert.alert(
233 + 'Permission Required',
234 + 'Please enable notifications in your device settings.'
235 + );
236 + return;
237 + }
238 +
239 + await notificationService.sendTestNotification();
240 + Alert.alert('Test Sent', 'Check your notifications');
241 + };
242 +
190 243 return (
191 244 <ThemedView style={styles.container}>
192 245 <SafeAreaView style={styles.safeArea} edges={['top']}>
@@ -245,6 +298,37 @@
245 298 </ThemedView>
246 299
247 300 <ThemedView style={styles.section}>
301 + <ThemedText type="subtitle" style={styles.sectionTitle}>Notifications</ThemedText>
302 +
303 + <ThemedView style={[styles.option, { borderBottomColor: colors.border }]}>
304 + <ThemedView style={styles.optionWithSwitch}>
305 + <ThemedView style={styles.optionTextContainer}>
306 + <ThemedText style={styles.optionText}>Daily Reminder</ThemedText>
307 + <ThemedText style={styles.optionDescription}>
308 + Get notified daily at 12:00 PM
309 + </ThemedText>
310 + </ThemedView>
311 + <Switch
312 + value={dailyNotifications}
313 + onValueChange={handleToggleDailyNotifications}
314 + />
315 + </ThemedView>
316 + </ThemedView>
317 +
318 + {dailyNotifications && (
319 + <TouchableOpacity
320 + style={[styles.option, { borderBottomColor: colors.border }]}
321 + onPress={handleTestNotification}
322 + >
323 + <ThemedText style={styles.optionText}>Send Test Notification</ThemedText>
324 + <ThemedText style={styles.optionDescription}>
325 + Test your notification settings
326 + </ThemedText>
327 + </TouchableOpacity>
328 + )}
329 + </ThemedView>
330 +
331 + <ThemedView style={styles.section}>
248 332 <ThemedText type="subtitle" style={styles.sectionTitle}>Data</ThemedText>
249 333
250 334 <TouchableOpacity style={[styles.option, { borderBottomColor: colors.border }]} onPress={handleExport}>
MODIFY package-lock.json +116 -29
diff --git a/package-lock.json b/package-lock.json
index 9628483..c5b289f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,6 +22,7 @@
22 22 "expo-haptics": "~15.0.7",
23 23 "expo-image": "~3.0.10",
24 24 "expo-linking": "~8.0.8",
25 + "expo-notifications": "^0.32.12",
25 26 "expo-router": "~6.0.14",
26 27 "expo-sharing": "^14.0.7",
27 28 "expo-splash-screen": "~31.0.10",
@@ -2349,6 +2350,12 @@
2349 2350 "url": "https://github.com/sponsors/nzakas"
2350 2351 }
2351 2352 },
2353 + "node_modules/@ide/backoff": {
2354 + "version": "1.0.0",
2355 + "resolved": "https://registry.npmjs.org/@ide/backoff/-/backoff-1.0.0.tgz",
2356 + "integrity": "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==",
2357 + "license": "MIT"
2358 + },
2352 2359 "node_modules/@isaacs/cliui": {
2353 2360 "version": "8.0.2",
2354 2361 "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -4380,6 +4387,19 @@
4380 4387 "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
4381 4388 "license": "MIT"
4382 4389 },
4390 + "node_modules/assert": {
4391 + "version": "2.1.0",
4392 + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz",
4393 + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==",
4394 + "license": "MIT",
4395 + "dependencies": {
4396 + "call-bind": "^1.0.2",
4397 + "is-nan": "^1.3.2",
4398 + "object-is": "^1.1.5",
4399 + "object.assign": "^4.1.4",
4400 + "util": "^0.12.5"
4401 + }
4402 + },
4383 4403 "node_modules/async-function": {
4384 4404 "version": "1.0.0",
4385 4405 "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
@@ -4400,7 +4420,6 @@
4400 4420 "version": "1.0.7",
4401 4421 "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
4402 4422 "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
4403 - "dev": true,
4404 4423 "license": "MIT",
4405 4424 "dependencies": {
4406 4425 "possible-typed-array-names": "^1.0.0"
@@ -4621,6 +4640,12 @@
4621 4640 "@babel/core": "^7.0.0"
4622 4641 }
4623 4642 },
4643 + "node_modules/badgin": {
4644 + "version": "1.2.3",
4645 + "resolved": "https://registry.npmjs.org/badgin/-/badgin-1.2.3.tgz",
4646 + "integrity": "sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw==",
4647 + "license": "MIT"
4648 + },
4624 4649 "node_modules/balanced-match": {
4625 4650 "version": "1.0.2",
4626 4651 "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -4835,7 +4860,6 @@
4835 4860 "version": "1.0.8",
4836 4861 "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
4837 4862 "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
4838 - "dev": true,
4839 4863 "license": "MIT",
4840 4864 "dependencies": {
4841 4865 "call-bind-apply-helpers": "^1.0.0",
@@ -4854,7 +4878,6 @@
4854 4878 "version": "1.0.2",
4855 4879 "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
4856 4880 "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
4857 - "dev": true,
4858 4881 "license": "MIT",
4859 4882 "dependencies": {
4860 4883 "es-errors": "^1.3.0",
@@ -4868,7 +4891,6 @@
4868 4891 "version": "1.0.4",
4869 4892 "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
4870 4893 "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
4871 - "dev": true,
4872 4894 "license": "MIT",
4873 4895 "dependencies": {
4874 4896 "call-bind-apply-helpers": "^1.0.2",
@@ -5443,7 +5465,6 @@
5443 5465 "version": "1.1.4",
5444 5466 "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
5445 5467 "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
5446 - "dev": true,
5447 5468 "license": "MIT",
5448 5469 "dependencies": {
5449 5470 "es-define-property": "^1.0.0",
@@ -5470,7 +5491,6 @@
5470 5491 "version": "1.2.1",
5471 5492 "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
5472 5493 "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
5473 - "dev": true,
5474 5494 "license": "MIT",
5475 5495 "dependencies": {
5476 5496 "define-data-property": "^1.0.1",
@@ -5562,7 +5582,6 @@
5562 5582 "version": "1.0.1",
5563 5583 "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
5564 5584 "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
5565 - "dev": true,
5566 5585 "license": "MIT",
5567 5586 "dependencies": {
5568 5587 "call-bind-apply-helpers": "^1.0.1",
@@ -5709,7 +5728,6 @@
5709 5728 "version": "1.0.1",
5710 5729 "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
5711 5730 "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
5712 - "dev": true,
5713 5731 "license": "MIT",
5714 5732 "engines": {
5715 5733 "node": ">= 0.4"
@@ -5719,7 +5737,6 @@
5719 5737 "version": "1.3.0",
5720 5738 "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
5721 5739 "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
5722 - "dev": true,
5723 5740 "license": "MIT",
5724 5741 "engines": {
5725 5742 "node": ">= 0.4"
@@ -5757,7 +5774,6 @@
5757 5774 "version": "1.1.1",
5758 5775 "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
5759 5776 "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
5760 - "dev": true,
5761 5777 "license": "MIT",
5762 5778 "dependencies": {
5763 5779 "es-errors": "^1.3.0"
@@ -6330,6 +6346,15 @@
6330 6346 }
6331 6347 }
6332 6348 },
6349 + "node_modules/expo-application": {
6350 + "version": "7.0.7",
6351 + "resolved": "https://registry.npmjs.org/expo-application/-/expo-application-7.0.7.tgz",
6352 + "integrity": "sha512-Jt1/qqnoDUbZ+bK91+dHaZ1vrPDtRBOltRa681EeedkisqguuEeUx4UHqwVyDK2oHWsK6lO3ojetoA4h8OmNcg==",
6353 + "license": "MIT",
6354 + "peerDependencies": {
6355 + "expo": "*"
6356 + }
6357 + },
6333 6358 "node_modules/expo-asset": {
6334 6359 "version": "12.0.9",
6335 6360 "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-12.0.9.tgz",
@@ -6474,6 +6499,26 @@
6474 6499 "react-native": "*"
6475 6500 }
6476 6501 },
6502 + "node_modules/expo-notifications": {
6503 + "version": "0.32.12",
6504 + "resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.32.12.tgz",
6505 + "integrity": "sha512-FVJ5W4rOpKvmrLJ1Sd5pxiVTV4a7ApgTlKro+E5X8M2TBbXmEVOjs09klzdalXTjlzmU/Gu8aRw9xr7Ea/gZdw==",
6506 + "license": "MIT",
6507 + "dependencies": {
6508 + "@expo/image-utils": "^0.8.7",
6509 + "@ide/backoff": "^1.0.0",
6510 + "abort-controller": "^3.0.0",
6511 + "assert": "^2.0.0",
6512 + "badgin": "^1.1.5",
6513 + "expo-application": "~7.0.7",
6514 + "expo-constants": "~18.0.9"
6515 + },
6516 + "peerDependencies": {
6517 + "expo": "*",
6518 + "react": "*",
6519 + "react-native": "*"
6520 + }
6521 + },
6477 6522 "node_modules/expo-router": {
6478 6523 "version": "6.0.14",
6479 6524 "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-6.0.14.tgz",
@@ -7299,7 +7344,6 @@
7299 7344 "version": "0.3.5",
7300 7345 "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
7301 7346 "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
7302 - "dev": true,
7303 7347 "license": "MIT",
7304 7348 "dependencies": {
7305 7349 "is-callable": "^1.2.7"
@@ -7421,7 +7465,6 @@
7421 7465 "version": "2.0.1",
7422 7466 "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
7423 7467 "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
7424 - "dev": true,
7425 7468 "license": "MIT",
7426 7469 "engines": {
7427 7470 "node": ">= 0.4"
@@ -7449,7 +7492,6 @@
7449 7492 "version": "1.3.0",
7450 7493 "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
7451 7494 "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
7452 - "dev": true,
7453 7495 "license": "MIT",
7454 7496 "dependencies": {
7455 7497 "call-bind-apply-helpers": "^1.0.2",
@@ -7492,7 +7534,6 @@
7492 7534 "version": "1.0.1",
7493 7535 "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
7494 7536 "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
7495 - "dev": true,
7496 7537 "license": "MIT",
7497 7538 "dependencies": {
7498 7539 "dunder-proto": "^1.0.1",
@@ -7645,7 +7686,6 @@
7645 7686 "version": "1.2.0",
7646 7687 "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
7647 7688 "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
7648 - "dev": true,
7649 7689 "license": "MIT",
7650 7690 "engines": {
7651 7691 "node": ">= 0.4"
@@ -7693,7 +7733,6 @@
7693 7733 "version": "1.0.2",
7694 7734 "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
7695 7735 "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
7696 - "dev": true,
7697 7736 "license": "MIT",
7698 7737 "dependencies": {
7699 7738 "es-define-property": "^1.0.0"
@@ -7722,7 +7761,6 @@
7722 7761 "version": "1.1.0",
7723 7762 "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
7724 7763 "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
7725 - "dev": true,
7726 7764 "license": "MIT",
7727 7765 "engines": {
7728 7766 "node": ">= 0.4"
@@ -7735,7 +7773,6 @@
7735 7773 "version": "1.0.2",
7736 7774 "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
7737 7775 "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
7738 - "dev": true,
7739 7776 "license": "MIT",
7740 7777 "dependencies": {
7741 7778 "has-symbols": "^1.0.3"
@@ -7993,6 +8030,22 @@
7993 8030 "loose-envify": "^1.0.0"
7994 8031 }
7995 8032 },
8033 + "node_modules/is-arguments": {
8034 + "version": "1.2.0",
8035 + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
8036 + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
8037 + "license": "MIT",
8038 + "dependencies": {
8039 + "call-bound": "^1.0.2",
8040 + "has-tostringtag": "^1.0.2"
8041 + },
8042 + "engines": {
8043 + "node": ">= 0.4"
8044 + },
8045 + "funding": {
8046 + "url": "https://github.com/sponsors/ljharb"
8047 + }
8048 + },
7996 8049 "node_modules/is-array-buffer": {
7997 8050 "version": "3.0.5",
7998 8051 "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -8103,7 +8156,6 @@
8103 8156 "version": "1.2.7",
8104 8157 "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
8105 8158 "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
8106 - "dev": true,
8107 8159 "license": "MIT",
8108 8160 "engines": {
8109 8161 "node": ">= 0.4"
@@ -8216,7 +8268,6 @@
8216 8268 "version": "1.1.2",
8217 8269 "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
8218 8270 "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
8219 - "dev": true,
8220 8271 "license": "MIT",
8221 8272 "dependencies": {
8222 8273 "call-bound": "^1.0.4",
@@ -8258,6 +8309,22 @@
8258 8309 "url": "https://github.com/sponsors/ljharb"
8259 8310 }
8260 8311 },
8312 + "node_modules/is-nan": {
8313 + "version": "1.3.2",
8314 + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz",
8315 + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==",
8316 + "license": "MIT",
8317 + "dependencies": {
8318 + "call-bind": "^1.0.0",
8319 + "define-properties": "^1.1.3"
8320 + },
8321 + "engines": {
8322 + "node": ">= 0.4"
8323 + },
8324 + "funding": {
8325 + "url": "https://github.com/sponsors/ljharb"
8326 + }
8327 + },
8261 8328 "node_modules/is-negative-zero": {
8262 8329 "version": "2.0.3",
8263 8330 "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
@@ -8310,7 +8377,6 @@
8310 8377 "version": "1.2.1",
8311 8378 "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
8312 8379 "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
8313 - "dev": true,
8314 8380 "license": "MIT",
8315 8381 "dependencies": {
8316 8382 "call-bound": "^1.0.2",
@@ -8393,7 +8459,6 @@
8393 8459 "version": "1.1.15",
8394 8460 "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
8395 8461 "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
8396 - "dev": true,
8397 8462 "license": "MIT",
8398 8463 "dependencies": {
8399 8464 "which-typed-array": "^1.1.16"
@@ -9292,7 +9357,6 @@
9292 9357 "version": "1.1.0",
9293 9358 "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
9294 9359 "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
9295 - "dev": true,
9296 9360 "license": "MIT",
9297 9361 "engines": {
9298 9362 "node": ">= 0.4"
@@ -9967,11 +10031,26 @@
9967 10031 "url": "https://github.com/sponsors/ljharb"
9968 10032 }
9969 10033 },
10034 + "node_modules/object-is": {
10035 + "version": "1.1.6",
10036 + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
10037 + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
10038 + "license": "MIT",
10039 + "dependencies": {
10040 + "call-bind": "^1.0.7",
10041 + "define-properties": "^1.2.1"
10042 + },
10043 + "engines": {
10044 + "node": ">= 0.4"
10045 + },
10046 + "funding": {
10047 + "url": "https://github.com/sponsors/ljharb"
10048 + }
10049 + },
9970 10050 "node_modules/object-keys": {
9971 10051 "version": "1.1.1",
9972 10052 "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
9973 10053 "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
9974 - "dev": true,
9975 10054 "license": "MIT",
9976 10055 "engines": {
9977 10056 "node": ">= 0.4"
@@ -9981,7 +10060,6 @@
9981 10060 "version": "4.1.7",
9982 10061 "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
9983 10062 "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
9984 - "dev": true,
9985 10063 "license": "MIT",
9986 10064 "dependencies": {
9987 10065 "call-bind": "^1.0.8",
@@ -10464,7 +10542,6 @@
10464 10542 "version": "1.1.0",
10465 10543 "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
10466 10544 "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
10467 - "dev": true,
10468 10545 "license": "MIT",
10469 10546 "engines": {
10470 10547 "node": ">= 0.4"
@@ -11508,7 +11585,6 @@
11508 11585 "version": "1.1.0",
11509 11586 "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
11510 11587 "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
11511 - "dev": true,
11512 11588 "license": "MIT",
11513 11589 "dependencies": {
11514 11590 "call-bound": "^1.0.2",
@@ -11646,7 +11722,6 @@
11646 11722 "version": "1.2.2",
11647 11723 "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
11648 11724 "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
11649 - "dev": true,
11650 11725 "license": "MIT",
11651 11726 "dependencies": {
11652 11727 "define-data-property": "^1.1.4",
@@ -12994,6 +13069,19 @@
12994 13069 "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
12995 13070 }
12996 13071 },
13072 + "node_modules/util": {
13073 + "version": "0.12.5",
13074 + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
13075 + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
13076 + "license": "MIT",
13077 + "dependencies": {
13078 + "inherits": "^2.0.3",
13079 + "is-arguments": "^1.0.4",
13080 + "is-generator-function": "^1.0.7",
13081 + "is-typed-array": "^1.1.3",
13082 + "which-typed-array": "^1.1.2"
13083 + }
13084 + },
12997 13085 "node_modules/utils-merge": {
12998 13086 "version": "1.0.1",
12999 13087 "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -13457,7 +13545,6 @@
13457 13545 "version": "1.1.19",
13458 13546 "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
13459 13547 "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
13460 - "dev": true,
13461 13548 "license": "MIT",
13462 13549 "dependencies": {
13463 13550 "available-typed-arrays": "^1.0.7",
MODIFY package.json +1 -0
diff --git a/package.json b/package.json
index b9f75ac..7c88a19 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
25 25 "expo-haptics": "~15.0.7",
26 26 "expo-image": "~3.0.10",
27 27 "expo-linking": "~8.0.8",
28 + "expo-notifications": "^0.32.12",
28 29 "expo-router": "~6.0.14",
29 30 "expo-sharing": "^14.0.7",
30 31 "expo-splash-screen": "~31.0.10",
MODIFY services/index.ts +1 -0
diff --git a/services/index.ts b/services/index.ts
index 3576c14..834fa1d 100644
--- a/services/index.ts
+++ b/services/index.ts
@@ -7,3 +7,4 @@
7 7 export { default as projectService } from './project.service';
8 8 export { default as tagService } from './tag.service';
9 9 export { default as webdavService } from './webdav.service';
10 +export { default as notificationService } from './notification.service';
ADD services/notification.service.ts +221 -0
diff --git a/services/notification.service.ts b/services/notification.service.ts
new file mode 100644
index 0000000..4644106
--- /dev/null
+++ b/services/notification.service.ts
@@ -0,0 +1,221 @@
1 +/**
2 + * Notification Service
3 + * Handles daily task notifications
4 + */
5 +
6 +import * as Notifications from 'expo-notifications';
7 +import { Platform } from 'react-native';
8 +import taskService from './task.service';
9 +
10 +// Configure notification behavior
11 +Notifications.setNotificationHandler({
12 + handleNotification: async () => ({
13 + shouldShowAlert: true,
14 + shouldPlaySound: true,
15 + shouldSetBadge: true,
16 + } as Notifications.NotificationBehavior),
17 +});
18 +
19 +class NotificationService {
20 + private notificationId: string | null = null;
21 +
22 + /**
23 + * Request notification permissions
24 + */
25 + async requestPermissions(): Promise<boolean> {
26 + try {
27 + const { status: existingStatus } = await Notifications.getPermissionsAsync();
28 + let finalStatus = existingStatus;
29 +
30 + if (existingStatus !== 'granted') {
31 + const { status } = await Notifications.requestPermissionsAsync();
32 + finalStatus = status;
33 + }
34 +
35 + if (finalStatus !== 'granted') {
36 + console.log('Notification permission not granted');
37 + return false;
38 + }
39 +
40 + // For Android, create notification channel
41 + if (Platform.OS === 'android') {
42 + await Notifications.setNotificationChannelAsync('daily-tasks', {
43 + name: 'Daily Task Reminders',
44 + importance: Notifications.AndroidImportance.HIGH,
45 + vibrationPattern: [0, 250, 250, 250],
46 + sound: 'default',
47 + });
48 + }
49 +
50 + return true;
51 + } catch (error) {
52 + console.error('Failed to request notification permissions:', error);
53 + return false;
54 + }
55 + }
56 +
57 + /**
58 + * Schedule daily notification at noon
59 + */
60 + async scheduleDailyNotification(): Promise<boolean> {
61 + try {
62 + // Cancel existing notification if any
63 + if (this.notificationId) {
64 + await Notifications.cancelScheduledNotificationAsync(this.notificationId);
65 + }
66 +
67 + // Schedule notification for noon every day
68 + this.notificationId = await Notifications.scheduleNotificationAsync({
69 + content: {
70 + title: 'Daily Task Reminder',
71 + body: 'Check your tasks for today',
72 + sound: true,
73 + priority: Notifications.AndroidNotificationPriority.HIGH,
74 + },
75 + trigger: {
76 + type: Notifications.SchedulableTriggerInputTypes.DAILY,
77 + hour: 12,
78 + minute: 0,
79 + repeats: true,
80 + } as any,
81 + });
82 +
83 + console.log('Daily notification scheduled:', this.notificationId);
84 + return true;
85 + } catch (error) {
86 + console.error('Failed to schedule notification:', error);
87 + return false;
88 + }
89 + }
90 +
91 + /**
92 + * Schedule notification with task count
93 + */
94 + async scheduleDailyNotificationWithCount(): Promise<boolean> {
95 + try {
96 + const hasPermission = await this.requestPermissions();
97 + if (!hasPermission) {
98 + return false;
99 + }
100 +
101 + // Cancel existing notification if any
102 + if (this.notificationId) {
103 + await Notifications.cancelScheduledNotificationAsync(this.notificationId);
104 + }
105 +
106 + // Get today's task count
107 + const todayTasks = await taskService.getTodayTasks();
108 + const taskCount = todayTasks.length;
109 +
110 + let body = 'Check your tasks for today';
111 + if (taskCount > 0) {
112 + body = `You have ${taskCount} task${taskCount > 1 ? 's' : ''} scheduled for today`;
113 + }
114 +
115 + // Schedule notification for noon every day
116 + this.notificationId = await Notifications.scheduleNotificationAsync({
117 + content: {
118 + title: '📋 Daily Task Reminder',
119 + body,
120 + sound: true,
121 + priority: Notifications.AndroidNotificationPriority.HIGH,
122 + data: { screen: '/(tabs)/today' },
123 + },
124 + trigger: {
125 + type: Notifications.SchedulableTriggerInputTypes.DAILY,
126 + hour: 12,
127 + minute: 0,
128 + repeats: true,
129 + } as any,
130 + });
131 +
132 + console.log('Daily notification scheduled with count:', this.notificationId);
133 + return true;
134 + } catch (error) {
135 + console.error('Failed to schedule notification with count:', error);
136 + return false;
137 + }
138 + }
139 +
140 + /**
141 + * Cancel daily notification
142 + */
143 + async cancelDailyNotification(): Promise<void> {
144 + try {
145 + if (this.notificationId) {
146 + await Notifications.cancelScheduledNotificationAsync(this.notificationId);
147 + this.notificationId = null;
148 + console.log('Daily notification cancelled');
149 + }
150 + } catch (error) {
151 + console.error('Failed to cancel notification:', error);
152 + }
153 + }
154 +
155 + /**
156 + * Check if notifications are enabled
157 + */
158 + async isEnabled(): Promise<boolean> {
159 + try {
160 + const { status } = await Notifications.getPermissionsAsync();
161 + return status === 'granted';
162 + } catch (error) {
163 + return false;
164 + }
165 + }
166 +
167 + /**
168 + * Get all scheduled notifications
169 + */
170 + async getScheduledNotifications(): Promise<Notifications.NotificationRequest[]> {
171 + try {
172 + return await Notifications.getAllScheduledNotificationsAsync();
173 + } catch (error) {
174 + console.error('Failed to get scheduled notifications:', error);
175 + return [];
176 + }
177 + }
178 +
179 + /**
180 + * Cancel all notifications
181 + */
182 + async cancelAllNotifications(): Promise<void> {
183 + try {
184 + await Notifications.cancelAllScheduledNotificationsAsync();
185 + this.notificationId = null;
186 + console.log('All notifications cancelled');
187 + } catch (error) {
188 + console.error('Failed to cancel all notifications:', error);
189 + }
190 + }
191 +
192 + /**
193 + * Send immediate test notification
194 + */
195 + async sendTestNotification(): Promise<void> {
196 + try {
197 + const todayTasks = await taskService.getTodayTasks();
198 + const taskCount = todayTasks.length;
199 +
200 + let body = 'No tasks scheduled for today';
201 + if (taskCount > 0) {
202 + body = `You have ${taskCount} task${taskCount > 1 ? 's' : ''} scheduled for today`;
203 + }
204 +
205 + await Notifications.scheduleNotificationAsync({
206 + content: {
207 + title: '📋 Daily Task Reminder (Test)',
208 + body,
209 + sound: true,
210 + priority: Notifications.AndroidNotificationPriority.HIGH,
211 + data: { screen: '/(tabs)/today' },
212 + },
213 + trigger: null, // Send immediately
214 + });
215 + } catch (error) {
216 + console.error('Failed to send test notification:', error);
217 + }
218 + }
219 +}
220 +
221 +export default new NotificationService();

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog