@umituz/react-native-notifications 1.0.6 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/infrastructure/hooks/actions/useNotificationActions.d.ts +4 -13
- package/lib/infrastructure/hooks/actions/useNotificationActions.d.ts.map +1 -1
- package/lib/infrastructure/hooks/actions/useNotificationActions.js +4 -70
- package/lib/infrastructure/hooks/actions/useNotificationActions.js.map +1 -1
- package/lib/infrastructure/hooks/actions/useNotificationManagementActions.d.ts +8 -0
- package/lib/infrastructure/hooks/actions/useNotificationManagementActions.d.ts.map +1 -0
- package/lib/infrastructure/hooks/actions/useNotificationManagementActions.js +78 -0
- package/lib/infrastructure/hooks/actions/useNotificationManagementActions.js.map +1 -0
- package/lib/infrastructure/hooks/state/useNotificationsState.d.ts +8 -8
- package/lib/infrastructure/hooks/useNotificationSettings.d.ts +2 -2
- package/lib/infrastructure/hooks/useNotifications.d.ts +21 -1
- package/lib/infrastructure/hooks/useNotifications.d.ts.map +1 -1
- package/lib/infrastructure/hooks/useNotifications.js +30 -9
- package/lib/infrastructure/hooks/useNotifications.js.map +1 -1
- package/lib/infrastructure/hooks/utils/useNotificationRefresh.d.ts +5 -10
- package/lib/infrastructure/hooks/utils/useNotificationRefresh.d.ts.map +1 -1
- package/lib/infrastructure/hooks/utils/useNotificationRefresh.js +32 -15
- package/lib/infrastructure/hooks/utils/useNotificationRefresh.js.map +1 -1
- package/lib/infrastructure/services/NotificationBadgeManager.d.ts +5 -0
- package/lib/infrastructure/services/NotificationBadgeManager.d.ts.map +1 -0
- package/lib/infrastructure/services/NotificationBadgeManager.js +29 -0
- package/lib/infrastructure/services/NotificationBadgeManager.js.map +1 -0
- package/lib/infrastructure/services/NotificationManager.d.ts +5 -84
- package/lib/infrastructure/services/NotificationManager.d.ts.map +1 -1
- package/lib/infrastructure/services/NotificationManager.js +36 -203
- package/lib/infrastructure/services/NotificationManager.js.map +1 -1
- package/lib/infrastructure/services/NotificationPermissions.d.ts +6 -0
- package/lib/infrastructure/services/NotificationPermissions.d.ts.map +1 -0
- package/lib/infrastructure/services/NotificationPermissions.js +75 -0
- package/lib/infrastructure/services/NotificationPermissions.js.map +1 -0
- package/lib/infrastructure/services/NotificationScheduler.d.ts +8 -0
- package/lib/infrastructure/services/NotificationScheduler.d.ts.map +1 -0
- package/lib/infrastructure/services/NotificationScheduler.js +72 -0
- package/lib/infrastructure/services/NotificationScheduler.js.map +1 -0
- package/lib/infrastructure/services/delivery/NotificationDelivery.d.ts +2 -8
- package/lib/infrastructure/services/delivery/NotificationDelivery.d.ts.map +1 -1
- package/lib/infrastructure/services/delivery/NotificationDelivery.js +27 -13
- package/lib/infrastructure/services/delivery/NotificationDelivery.js.map +1 -1
- package/lib/infrastructure/storage/NotificationsStore.d.ts +8 -1
- package/lib/infrastructure/storage/NotificationsStore.d.ts.map +1 -1
- package/lib/infrastructure/storage/NotificationsStore.js +2 -1
- package/lib/infrastructure/storage/NotificationsStore.js.map +1 -1
- package/lib/infrastructure/utils/dev.d.ts +5 -0
- package/lib/infrastructure/utils/dev.d.ts.map +1 -0
- package/lib/infrastructure/utils/dev.js +24 -0
- package/lib/infrastructure/utils/dev.js.map +1 -0
- package/lib/presentation/screens/NotificationsScreen.d.ts +14 -4
- package/lib/presentation/screens/NotificationsScreen.d.ts.map +1 -1
- package/lib/presentation/screens/NotificationsScreen.js +12 -15
- package/lib/presentation/screens/NotificationsScreen.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/NotificationManager.test.ts +215 -0
- package/src/__tests__/useNotificationActions.test.ts +189 -0
- package/src/__tests__/useNotificationRefresh.test.ts +213 -0
- package/src/infrastructure/hooks/actions/useNotificationActions.ts +8 -110
- package/src/infrastructure/hooks/actions/useNotificationManagementActions.ts +131 -0
- package/src/infrastructure/hooks/useNotifications.ts +37 -11
- package/src/infrastructure/hooks/utils/useNotificationRefresh.ts +40 -16
- package/src/infrastructure/services/NotificationBadgeManager.ts +28 -0
- package/src/infrastructure/services/NotificationManager.ts +51 -217
- package/src/infrastructure/services/NotificationPermissions.ts +80 -0
- package/src/infrastructure/services/NotificationScheduler.ts +77 -0
- package/src/infrastructure/services/delivery/NotificationDelivery.ts +32 -14
- package/src/infrastructure/storage/NotificationsStore.ts +3 -2
- package/src/infrastructure/utils/dev.ts +25 -0
- package/src/presentation/screens/NotificationsScreen.tsx +31 -18
- package/src/types/global.d.ts +255 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { useEffect } from 'react';
|
|
1
|
+
import { useEffect, useCallback } from 'react';
|
|
2
2
|
import { useNotificationsState } from './state/useNotificationsState';
|
|
3
3
|
import { useNotificationActions } from './actions/useNotificationActions';
|
|
4
|
+
import { useNotificationManagementActions } from './actions/useNotificationManagementActions';
|
|
4
5
|
import { useNotificationRefresh } from './utils/useNotificationRefresh';
|
|
5
6
|
import type { UseNotificationsOptions } from './types';
|
|
7
|
+
import { devLog } from '../utils/dev';
|
|
6
8
|
|
|
7
9
|
export * from './types';
|
|
8
10
|
|
|
@@ -31,10 +33,25 @@ export function useNotifications(userId: string, options: UseNotificationsOption
|
|
|
31
33
|
};
|
|
32
34
|
|
|
33
35
|
const actions = useNotificationActions(state, setters);
|
|
36
|
+
const managementActions = useNotificationManagementActions(state, setters);
|
|
34
37
|
const refresh = useNotificationRefresh(pageSize, setters);
|
|
35
38
|
|
|
36
|
-
const loadMoreNotifications = () =>
|
|
37
|
-
refresh.loadMoreNotifications(state.notifications.length, state.hasMore, state.loading)
|
|
39
|
+
const loadMoreNotifications = useCallback(() =>
|
|
40
|
+
refresh.loadMoreNotifications(state.notifications.length, state.hasMore, state.loading),
|
|
41
|
+
[refresh, state.notifications.length, state.hasMore, state.loading]
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const cleanup = useCallback(() => {
|
|
45
|
+
setNotifications([]);
|
|
46
|
+
setChannels([]);
|
|
47
|
+
setUnreadCount(0);
|
|
48
|
+
setPreferences(null);
|
|
49
|
+
setError(null);
|
|
50
|
+
setLoading(false);
|
|
51
|
+
setHasMore(true);
|
|
52
|
+
|
|
53
|
+
devLog('[useNotifications] Cleaned up notification state');
|
|
54
|
+
}, [setNotifications, setChannels, setUnreadCount, setPreferences, setError, setLoading, setHasMore]);
|
|
38
55
|
|
|
39
56
|
// Load initial data
|
|
40
57
|
useEffect(() => {
|
|
@@ -43,14 +60,11 @@ export function useNotifications(userId: string, options: UseNotificationsOption
|
|
|
43
60
|
refresh.refreshChannels();
|
|
44
61
|
refresh.refreshPreferences();
|
|
45
62
|
} else {
|
|
46
|
-
|
|
47
|
-
setChannels([]);
|
|
48
|
-
setUnreadCount(0);
|
|
49
|
-
setPreferences(null);
|
|
63
|
+
cleanup();
|
|
50
64
|
}
|
|
51
|
-
}, [userId]);
|
|
65
|
+
}, [userId, cleanup]);
|
|
52
66
|
|
|
53
|
-
// Auto-refresh setup
|
|
67
|
+
// Auto-refresh setup with proper cleanup
|
|
54
68
|
useEffect(() => {
|
|
55
69
|
if (!autoRefresh || !userId) return;
|
|
56
70
|
|
|
@@ -58,13 +72,25 @@ export function useNotifications(userId: string, options: UseNotificationsOption
|
|
|
58
72
|
refresh.refreshNotifications();
|
|
59
73
|
}, refreshInterval);
|
|
60
74
|
|
|
61
|
-
return () =>
|
|
75
|
+
return () => {
|
|
76
|
+
clearInterval(interval);
|
|
77
|
+
devLog('[useNotifications] Auto-refresh interval cleared');
|
|
78
|
+
};
|
|
62
79
|
}, [autoRefresh, userId, refreshInterval]);
|
|
63
80
|
|
|
81
|
+
// Cleanup on unmount
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
return () => {
|
|
84
|
+
devLog('[useNotifications] Component unmounted, cleaning up');
|
|
85
|
+
};
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
64
88
|
return {
|
|
65
89
|
...state,
|
|
66
90
|
...actions,
|
|
91
|
+
...managementActions,
|
|
67
92
|
...refresh,
|
|
68
93
|
loadMoreNotifications,
|
|
94
|
+
cleanup,
|
|
69
95
|
};
|
|
70
|
-
}
|
|
96
|
+
}
|
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
import { useCallback } from 'react';
|
|
1
|
+
import { useCallback, useRef } from 'react';
|
|
2
2
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
3
3
|
import type { Notification } from '../types';
|
|
4
4
|
import { ChannelManager } from '../../services/channels/ChannelManager';
|
|
5
5
|
import { PreferencesManager } from '../../services/preferences/PreferencesManager';
|
|
6
|
+
import { devLog, devError } from '../../utils/dev';
|
|
6
7
|
|
|
7
|
-
/**
|
|
8
|
-
* useNotificationRefresh - Offline Notification Refresh
|
|
9
|
-
*
|
|
10
|
-
* Uses AsyncStorage for local data.
|
|
11
|
-
* NO backend - pure offline.
|
|
12
|
-
*/
|
|
13
8
|
export const useNotificationRefresh = (pageSize: number, setters: any) => {
|
|
14
9
|
const {
|
|
15
10
|
setNotifications,
|
|
@@ -21,39 +16,57 @@ export const useNotificationRefresh = (pageSize: number, setters: any) => {
|
|
|
21
16
|
setHasMore,
|
|
22
17
|
} = setters;
|
|
23
18
|
|
|
24
|
-
const channelManager = new ChannelManager();
|
|
25
|
-
const preferencesManager = new PreferencesManager();
|
|
19
|
+
const channelManager = useRef(new ChannelManager()).current;
|
|
20
|
+
const preferencesManager = useRef(new PreferencesManager()).current;
|
|
21
|
+
const abortController = useRef<AbortController | null>(null);
|
|
22
|
+
|
|
23
|
+
const cleanup = useCallback(() => {
|
|
24
|
+
if (abortController.current) {
|
|
25
|
+
abortController.current.abort();
|
|
26
|
+
abortController.current = null;
|
|
27
|
+
}
|
|
28
|
+
}, []);
|
|
26
29
|
|
|
27
30
|
const refreshNotifications = useCallback(async () => {
|
|
28
31
|
try {
|
|
32
|
+
cleanup();
|
|
33
|
+
abortController.current = new AbortController();
|
|
34
|
+
|
|
29
35
|
setLoading(true);
|
|
30
36
|
setError(null);
|
|
31
37
|
|
|
32
|
-
// Load from AsyncStorage
|
|
33
38
|
const data = await AsyncStorage.getItem('@notifications:list');
|
|
34
39
|
const allNotifications: Notification[] = data ? JSON.parse(data) : [];
|
|
35
40
|
|
|
36
|
-
// Paginate
|
|
37
41
|
const paginated = allNotifications.slice(0, pageSize);
|
|
38
42
|
const unread = allNotifications.filter((n) => !n.read).length;
|
|
39
43
|
|
|
40
44
|
setNotifications(paginated);
|
|
41
45
|
setUnreadCount(unread);
|
|
42
46
|
setHasMore(allNotifications.length > pageSize);
|
|
47
|
+
|
|
48
|
+
devLog('[useNotificationRefresh] Refreshed notifications:', paginated.length);
|
|
43
49
|
} catch (err) {
|
|
50
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
44
53
|
setError(
|
|
45
54
|
err instanceof Error ? err.message : 'Failed to load notifications'
|
|
46
55
|
);
|
|
47
56
|
} finally {
|
|
48
57
|
setLoading(false);
|
|
58
|
+
cleanup();
|
|
49
59
|
}
|
|
50
|
-
}, [pageSize, setNotifications, setUnreadCount, setHasMore, setLoading, setError]);
|
|
60
|
+
}, [pageSize, setNotifications, setUnreadCount, setHasMore, setLoading, setError, cleanup]);
|
|
51
61
|
|
|
52
62
|
const loadMoreNotifications = useCallback(
|
|
53
63
|
async (currentLength: number, hasMore: boolean, loading: boolean) => {
|
|
54
64
|
if (!hasMore || loading) return;
|
|
55
65
|
|
|
56
66
|
try {
|
|
67
|
+
cleanup();
|
|
68
|
+
abortController.current = new AbortController();
|
|
69
|
+
|
|
57
70
|
setLoading(true);
|
|
58
71
|
setError(null);
|
|
59
72
|
|
|
@@ -67,7 +80,12 @@ export const useNotificationRefresh = (pageSize: number, setters: any) => {
|
|
|
67
80
|
|
|
68
81
|
setNotifications((prev: any[]) => [...prev, ...moreNotifications]);
|
|
69
82
|
setHasMore(allNotifications.length > currentLength + pageSize);
|
|
83
|
+
|
|
84
|
+
devLog('[useNotificationRefresh] Loaded more notifications:', moreNotifications.length);
|
|
70
85
|
} catch (err) {
|
|
86
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
71
89
|
setError(
|
|
72
90
|
err instanceof Error
|
|
73
91
|
? err.message
|
|
@@ -75,17 +93,20 @@ export const useNotificationRefresh = (pageSize: number, setters: any) => {
|
|
|
75
93
|
);
|
|
76
94
|
} finally {
|
|
77
95
|
setLoading(false);
|
|
96
|
+
cleanup();
|
|
78
97
|
}
|
|
79
98
|
},
|
|
80
|
-
[pageSize, setNotifications, setHasMore, setLoading, setError]
|
|
99
|
+
[pageSize, setNotifications, setHasMore, setLoading, setError, cleanup]
|
|
81
100
|
);
|
|
82
101
|
|
|
83
102
|
const refreshChannels = useCallback(async () => {
|
|
84
103
|
try {
|
|
85
104
|
const channelsData = await channelManager.getActiveChannels();
|
|
86
105
|
setChannels(channelsData);
|
|
106
|
+
|
|
107
|
+
devLog('[useNotificationRefresh] Refreshed channels:', channelsData.length);
|
|
87
108
|
} catch (err) {
|
|
88
|
-
|
|
109
|
+
devError('[useNotificationRefresh] Failed to refresh channels:', err);
|
|
89
110
|
}
|
|
90
111
|
}, [setChannels]);
|
|
91
112
|
|
|
@@ -93,8 +114,10 @@ export const useNotificationRefresh = (pageSize: number, setters: any) => {
|
|
|
93
114
|
try {
|
|
94
115
|
const prefsData = await preferencesManager.get();
|
|
95
116
|
setPreferences(prefsData);
|
|
117
|
+
|
|
118
|
+
devLog('[useNotificationRefresh] Refreshed preferences');
|
|
96
119
|
} catch (err) {
|
|
97
|
-
|
|
120
|
+
devError('[useNotificationRefresh] Failed to refresh preferences:', err);
|
|
98
121
|
}
|
|
99
122
|
}, [setPreferences]);
|
|
100
123
|
|
|
@@ -103,5 +126,6 @@ export const useNotificationRefresh = (pageSize: number, setters: any) => {
|
|
|
103
126
|
loadMoreNotifications,
|
|
104
127
|
refreshChannels,
|
|
105
128
|
refreshPreferences,
|
|
129
|
+
cleanup,
|
|
106
130
|
};
|
|
107
|
-
};
|
|
131
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as Notifications from 'expo-notifications';
|
|
2
|
+
import { Platform } from 'react-native';
|
|
3
|
+
import { devError } from '../utils/dev';
|
|
4
|
+
|
|
5
|
+
export class NotificationBadgeManager {
|
|
6
|
+
async getBadgeCount(): Promise<number> {
|
|
7
|
+
try {
|
|
8
|
+
if (Platform.OS === 'ios') {
|
|
9
|
+
return await Notifications.getBadgeCountAsync();
|
|
10
|
+
}
|
|
11
|
+
return 0;
|
|
12
|
+
} catch (error) {
|
|
13
|
+
devError('[NotificationBadgeManager] Get badge count failed:', error);
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async setBadgeCount(count: number): Promise<void> {
|
|
19
|
+
try {
|
|
20
|
+
if (Platform.OS === 'ios') {
|
|
21
|
+
await Notifications.setBadgeCountAsync(count);
|
|
22
|
+
}
|
|
23
|
+
} catch (error) {
|
|
24
|
+
devError('[NotificationBadgeManager] Set badge count failed:', error);
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -1,42 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NotificationManager
|
|
2
|
+
* NotificationManager - Core Notification Operations
|
|
3
3
|
*
|
|
4
4
|
* Offline-first notification system using expo-notifications.
|
|
5
5
|
* Works in ALL apps (offline, online, hybrid) - no backend required.
|
|
6
|
-
*
|
|
7
|
-
* Features:
|
|
8
|
-
* - Schedule notifications for specific dates/times
|
|
9
|
-
* - Repeating notifications (daily, weekly, monthly)
|
|
10
|
-
* - Android notification channels
|
|
11
|
-
* - Permission handling
|
|
12
|
-
* - Cancel individual or all notifications
|
|
13
|
-
* - Works completely offline
|
|
14
|
-
*
|
|
15
|
-
* Use Cases:
|
|
16
|
-
* - Reminders (bills, tasks, appointments)
|
|
17
|
-
* - Habit tracking (daily streaks)
|
|
18
|
-
* - Medication reminders
|
|
19
|
-
* - Any app needing local notifications
|
|
20
|
-
*
|
|
21
|
-
* @module NotificationManager
|
|
22
6
|
*/
|
|
23
7
|
|
|
24
8
|
import * as Notifications from 'expo-notifications';
|
|
25
9
|
import * as Device from 'expo-device';
|
|
26
10
|
import { Platform } from 'react-native';
|
|
11
|
+
import { NotificationPermissions } from './NotificationPermissions';
|
|
12
|
+
import { NotificationScheduler } from './NotificationScheduler';
|
|
13
|
+
import { NotificationBadgeManager } from './NotificationBadgeManager';
|
|
14
|
+
import { devLog, devError } from '../utils/dev';
|
|
27
15
|
|
|
28
|
-
/**
|
|
29
|
-
* Trigger types for notifications
|
|
30
|
-
*/
|
|
31
16
|
export type NotificationTrigger =
|
|
32
17
|
| { type: 'date'; date: Date }
|
|
33
18
|
| { type: 'daily'; hour: number; minute: number }
|
|
34
19
|
| { type: 'weekly'; weekday: number; hour: number; minute: number }
|
|
35
20
|
| { type: 'monthly'; day: number; hour: number; minute: number };
|
|
36
21
|
|
|
37
|
-
/**
|
|
38
|
-
* Options for scheduling a notification
|
|
39
|
-
*/
|
|
40
22
|
export interface ScheduleNotificationOptions {
|
|
41
23
|
title: string;
|
|
42
24
|
body: string;
|
|
@@ -47,9 +29,6 @@ export interface ScheduleNotificationOptions {
|
|
|
47
29
|
categoryIdentifier?: string;
|
|
48
30
|
}
|
|
49
31
|
|
|
50
|
-
/**
|
|
51
|
-
* Scheduled notification details
|
|
52
|
-
*/
|
|
53
32
|
export interface ScheduledNotification {
|
|
54
33
|
identifier: string;
|
|
55
34
|
content: {
|
|
@@ -60,16 +39,17 @@ export interface ScheduledNotification {
|
|
|
60
39
|
trigger: any;
|
|
61
40
|
}
|
|
62
41
|
|
|
63
|
-
/**
|
|
64
|
-
* NotificationManager
|
|
65
|
-
*
|
|
66
|
-
* Handles all notification operations using expo-notifications.
|
|
67
|
-
* Works completely offline - no backend, no user IDs, just device-local notifications.
|
|
68
|
-
*/
|
|
69
42
|
export class NotificationManager {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
43
|
+
private permissions: NotificationPermissions;
|
|
44
|
+
private scheduler: NotificationScheduler;
|
|
45
|
+
private badgeManager: NotificationBadgeManager;
|
|
46
|
+
|
|
47
|
+
constructor() {
|
|
48
|
+
this.permissions = new NotificationPermissions();
|
|
49
|
+
this.scheduler = new NotificationScheduler();
|
|
50
|
+
this.badgeManager = new NotificationBadgeManager();
|
|
51
|
+
}
|
|
52
|
+
|
|
73
53
|
static configure() {
|
|
74
54
|
Notifications.setNotificationHandler({
|
|
75
55
|
handleNotification: async () => ({
|
|
@@ -78,250 +58,104 @@ export class NotificationManager {
|
|
|
78
58
|
shouldSetBadge: true,
|
|
79
59
|
}),
|
|
80
60
|
});
|
|
61
|
+
|
|
62
|
+
devLog('[NotificationManager] Configured notification handler');
|
|
81
63
|
}
|
|
82
64
|
|
|
83
|
-
/**
|
|
84
|
-
* Request notification permissions
|
|
85
|
-
* iOS: Shows system permission dialog
|
|
86
|
-
* Android: Permissions granted by default (Android 13+ requires runtime permission)
|
|
87
|
-
*/
|
|
88
65
|
async requestPermissions(): Promise<boolean> {
|
|
89
66
|
try {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const permissionsResponse = await Notifications.getPermissionsAsync();
|
|
96
|
-
const existingStatus = (permissionsResponse as any).status || ((permissionsResponse as any).granted ? 'granted' : 'denied');
|
|
97
|
-
let finalStatus = existingStatus;
|
|
98
|
-
|
|
99
|
-
if (existingStatus !== 'granted') {
|
|
100
|
-
const requestResponse = await Notifications.requestPermissionsAsync();
|
|
101
|
-
finalStatus = (requestResponse as any).status || ((requestResponse as any).granted ? 'granted' : 'denied');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (Platform.OS === 'android') {
|
|
105
|
-
await this.createAndroidChannels();
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return finalStatus === 'granted';
|
|
67
|
+
const result = await this.permissions.requestPermissions();
|
|
68
|
+
|
|
69
|
+
devLog('[NotificationManager] Permissions requested:', result);
|
|
70
|
+
|
|
71
|
+
return result;
|
|
109
72
|
} catch (error) {
|
|
110
|
-
|
|
73
|
+
devError('[NotificationManager] Permission request failed:', error);
|
|
111
74
|
return false;
|
|
112
75
|
}
|
|
113
76
|
}
|
|
114
77
|
|
|
115
|
-
/**
|
|
116
|
-
* Check if notification permissions are granted
|
|
117
|
-
*/
|
|
118
78
|
async hasPermissions(): Promise<boolean> {
|
|
119
79
|
try {
|
|
120
|
-
|
|
121
|
-
const permissionsResponse = await Notifications.getPermissionsAsync();
|
|
122
|
-
return (permissionsResponse as any).status === 'granted' || (permissionsResponse as any).granted === true;
|
|
80
|
+
return await this.permissions.hasPermissions();
|
|
123
81
|
} catch (error) {
|
|
124
|
-
|
|
82
|
+
devError('[NotificationManager] Permission check failed:', error);
|
|
125
83
|
return false;
|
|
126
84
|
}
|
|
127
85
|
}
|
|
128
86
|
|
|
129
|
-
/**
|
|
130
|
-
* Create Android notification channels (required for Android 8+)
|
|
131
|
-
*/
|
|
132
|
-
private async createAndroidChannels(): Promise<void> {
|
|
133
|
-
if (Platform.OS !== 'android') return;
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
await Notifications.setNotificationChannelAsync('default', {
|
|
137
|
-
name: 'Default',
|
|
138
|
-
importance: Notifications.AndroidImportance.DEFAULT,
|
|
139
|
-
vibrationPattern: [0, 250, 250, 250],
|
|
140
|
-
sound: 'default',
|
|
141
|
-
lightColor: '#3B82F6',
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
await Notifications.setNotificationChannelAsync('reminders', {
|
|
145
|
-
name: 'Reminders',
|
|
146
|
-
importance: Notifications.AndroidImportance.HIGH,
|
|
147
|
-
vibrationPattern: [0, 250, 250, 250],
|
|
148
|
-
sound: 'default',
|
|
149
|
-
lightColor: '#3B82F6',
|
|
150
|
-
enableVibrate: true,
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
await Notifications.setNotificationChannelAsync('urgent', {
|
|
154
|
-
name: 'Urgent',
|
|
155
|
-
importance: Notifications.AndroidImportance.MAX,
|
|
156
|
-
vibrationPattern: [0, 500, 250, 500],
|
|
157
|
-
sound: 'default',
|
|
158
|
-
lightColor: '#EF4444',
|
|
159
|
-
enableVibrate: true,
|
|
160
|
-
});
|
|
161
|
-
} catch (error) {
|
|
162
|
-
console.error('[NotificationManager] Android channel creation failed:', error);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Schedule a notification
|
|
168
|
-
*
|
|
169
|
-
* @example
|
|
170
|
-
* // Specific date
|
|
171
|
-
* const id = await manager.scheduleNotification({
|
|
172
|
-
* title: 'Bill Reminder',
|
|
173
|
-
* body: 'Electricity bill due today',
|
|
174
|
-
* trigger: { type: 'date', date: new Date('2025-01-15T09:00:00') }
|
|
175
|
-
* });
|
|
176
|
-
*
|
|
177
|
-
* @example
|
|
178
|
-
* // Daily reminder
|
|
179
|
-
* const id = await manager.scheduleNotification({
|
|
180
|
-
* title: 'Daily Workout',
|
|
181
|
-
* body: 'Time for your morning workout!',
|
|
182
|
-
* trigger: { type: 'daily', hour: 7, minute: 0 }
|
|
183
|
-
* });
|
|
184
|
-
*/
|
|
185
87
|
async scheduleNotification(options: ScheduleNotificationOptions): Promise<string> {
|
|
186
88
|
try {
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
notificationTrigger = {
|
|
193
|
-
date: trigger.date,
|
|
194
|
-
channelId: categoryIdentifier || 'default',
|
|
195
|
-
};
|
|
196
|
-
} else if (trigger.type === 'daily') {
|
|
197
|
-
notificationTrigger = {
|
|
198
|
-
hour: trigger.hour,
|
|
199
|
-
minute: trigger.minute,
|
|
200
|
-
repeats: true,
|
|
201
|
-
channelId: categoryIdentifier || 'reminders',
|
|
202
|
-
};
|
|
203
|
-
} else if (trigger.type === 'weekly') {
|
|
204
|
-
notificationTrigger = {
|
|
205
|
-
weekday: trigger.weekday,
|
|
206
|
-
hour: trigger.hour,
|
|
207
|
-
minute: trigger.minute,
|
|
208
|
-
repeats: true,
|
|
209
|
-
channelId: categoryIdentifier || 'reminders',
|
|
210
|
-
};
|
|
211
|
-
} else if (trigger.type === 'monthly') {
|
|
212
|
-
notificationTrigger = {
|
|
213
|
-
day: trigger.day,
|
|
214
|
-
hour: trigger.hour,
|
|
215
|
-
minute: trigger.minute,
|
|
216
|
-
repeats: true,
|
|
217
|
-
channelId: categoryIdentifier || 'reminders',
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const notificationId = await Notifications.scheduleNotificationAsync({
|
|
222
|
-
content: {
|
|
223
|
-
title,
|
|
224
|
-
body,
|
|
225
|
-
data,
|
|
226
|
-
sound: sound === true ? 'default' : sound || undefined,
|
|
227
|
-
badge,
|
|
228
|
-
categoryIdentifier,
|
|
229
|
-
priority: Notifications.AndroidNotificationPriority.HIGH,
|
|
230
|
-
vibrate: [0, 250, 250, 250],
|
|
231
|
-
},
|
|
232
|
-
trigger: notificationTrigger,
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
return notificationId;
|
|
89
|
+
const id = await this.scheduler.scheduleNotification(options);
|
|
90
|
+
|
|
91
|
+
devLog('[NotificationManager] Notification scheduled:', id);
|
|
92
|
+
|
|
93
|
+
return id;
|
|
236
94
|
} catch (error) {
|
|
237
|
-
|
|
95
|
+
devError('[NotificationManager] Schedule notification failed:', error);
|
|
238
96
|
throw error;
|
|
239
97
|
}
|
|
240
98
|
}
|
|
241
99
|
|
|
242
|
-
/**
|
|
243
|
-
* Cancel a scheduled notification
|
|
244
|
-
*/
|
|
245
100
|
async cancelNotification(notificationId: string): Promise<void> {
|
|
246
101
|
try {
|
|
247
|
-
await
|
|
102
|
+
await this.scheduler.cancelNotification(notificationId);
|
|
103
|
+
|
|
104
|
+
devLog('[NotificationManager] Notification cancelled:', notificationId);
|
|
248
105
|
} catch (error) {
|
|
249
|
-
|
|
106
|
+
devError('[NotificationManager] Cancel notification failed:', error);
|
|
250
107
|
throw error;
|
|
251
108
|
}
|
|
252
109
|
}
|
|
253
110
|
|
|
254
|
-
/**
|
|
255
|
-
* Cancel all scheduled notifications
|
|
256
|
-
*/
|
|
257
111
|
async cancelAllNotifications(): Promise<void> {
|
|
258
112
|
try {
|
|
259
|
-
await
|
|
113
|
+
await this.scheduler.cancelAllNotifications();
|
|
114
|
+
|
|
115
|
+
devLog('[NotificationManager] All notifications cancelled');
|
|
260
116
|
} catch (error) {
|
|
261
|
-
|
|
117
|
+
devError('[NotificationManager] Cancel all notifications failed:', error);
|
|
262
118
|
throw error;
|
|
263
119
|
}
|
|
264
120
|
}
|
|
265
121
|
|
|
266
|
-
/**
|
|
267
|
-
* Get all scheduled notifications
|
|
268
|
-
*/
|
|
269
122
|
async getScheduledNotifications(): Promise<ScheduledNotification[]> {
|
|
270
123
|
try {
|
|
271
|
-
|
|
272
|
-
return notifications.map(notification => ({
|
|
273
|
-
identifier: notification.identifier,
|
|
274
|
-
content: {
|
|
275
|
-
title: notification.content.title || '',
|
|
276
|
-
body: notification.content.body || '',
|
|
277
|
-
data: notification.content.data as Record<string, any>,
|
|
278
|
-
},
|
|
279
|
-
trigger: notification.trigger,
|
|
280
|
-
}));
|
|
124
|
+
return await this.scheduler.getScheduledNotifications();
|
|
281
125
|
} catch (error) {
|
|
282
|
-
|
|
126
|
+
devError('[NotificationManager] Get scheduled notifications failed:', error);
|
|
283
127
|
return [];
|
|
284
128
|
}
|
|
285
129
|
}
|
|
286
130
|
|
|
287
|
-
/**
|
|
288
|
-
* Dismiss all delivered notifications (clear from notification center)
|
|
289
|
-
*/
|
|
290
131
|
async dismissAllNotifications(): Promise<void> {
|
|
291
132
|
try {
|
|
292
133
|
await Notifications.dismissAllNotificationsAsync();
|
|
134
|
+
|
|
135
|
+
devLog('[NotificationManager] All notifications dismissed');
|
|
293
136
|
} catch (error) {
|
|
294
|
-
|
|
137
|
+
devError('[NotificationManager] Dismiss all notifications failed:', error);
|
|
295
138
|
throw error;
|
|
296
139
|
}
|
|
297
140
|
}
|
|
298
141
|
|
|
299
|
-
/**
|
|
300
|
-
* Get notification badge count (iOS only)
|
|
301
|
-
*/
|
|
302
142
|
async getBadgeCount(): Promise<number> {
|
|
303
143
|
try {
|
|
304
|
-
|
|
305
|
-
return await Notifications.getBadgeCountAsync();
|
|
306
|
-
}
|
|
307
|
-
return 0;
|
|
144
|
+
return await this.badgeManager.getBadgeCount();
|
|
308
145
|
} catch (error) {
|
|
309
|
-
|
|
146
|
+
devError('[NotificationManager] Get badge count failed:', error);
|
|
310
147
|
return 0;
|
|
311
148
|
}
|
|
312
149
|
}
|
|
313
150
|
|
|
314
|
-
/**
|
|
315
|
-
* Set notification badge count (iOS only)
|
|
316
|
-
*/
|
|
317
151
|
async setBadgeCount(count: number): Promise<void> {
|
|
318
152
|
try {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
153
|
+
await this.badgeManager.setBadgeCount(count);
|
|
154
|
+
|
|
155
|
+
devLog('[NotificationManager] Badge count set:', count);
|
|
322
156
|
} catch (error) {
|
|
323
|
-
|
|
157
|
+
devError('[NotificationManager] Set badge count failed:', error);
|
|
324
158
|
throw error;
|
|
325
159
|
}
|
|
326
160
|
}
|
|
327
|
-
}
|
|
161
|
+
}
|