✨ Add notifications
Changes
6 files changed, +433 -30
MODIFY
app.json
+9 -0
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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();