@umituz/react-native-notifications 1.1.7 → 1.3.0
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/package.json +18 -40
- package/src/index.ts +84 -13
- package/src/infrastructure/config/reminderPresets.ts +120 -0
- package/src/infrastructure/hooks/useQuietHoursActions.ts +52 -0
- package/src/infrastructure/hooks/useReminderActions.ts +147 -0
- package/src/infrastructure/services/types.ts +137 -44
- package/src/infrastructure/storage/RemindersStore.ts +150 -0
- package/src/presentation/components/FormButton.tsx +66 -0
- package/src/presentation/components/FrequencySelector.tsx +72 -0
- package/src/presentation/components/NotificationsSection.tsx +95 -160
- package/src/presentation/components/QuietHoursCard.tsx +105 -0
- package/src/presentation/components/ReminderForm.tsx +165 -0
- package/src/presentation/components/ReminderItem.tsx +124 -0
- package/src/presentation/components/TimePresetSelector.tsx +100 -0
- package/src/presentation/components/WeekdaySelector.tsx +61 -0
- package/src/presentation/screens/NotificationSettingsScreen.tsx +210 -0
- package/src/presentation/screens/ReminderListScreen.tsx +138 -0
- package/src/types/global.d.ts +11 -8
- package/lib/index.d.ts +0 -16
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js +0 -23
- package/lib/index.js.map +0 -1
- package/lib/infrastructure/config/notificationsConfig.d.ts +0 -20
- package/lib/infrastructure/config/notificationsConfig.d.ts.map +0 -1
- package/lib/infrastructure/config/notificationsConfig.js +0 -81
- package/lib/infrastructure/config/notificationsConfig.js.map +0 -1
- package/lib/infrastructure/hooks/actions/useNotificationActions.d.ts +0 -7
- package/lib/infrastructure/hooks/actions/useNotificationActions.d.ts.map +0 -1
- package/lib/infrastructure/hooks/actions/useNotificationActions.js +0 -75
- package/lib/infrastructure/hooks/actions/useNotificationActions.js.map +0 -1
- package/lib/infrastructure/hooks/actions/useNotificationManagementActions.d.ts +0 -8
- package/lib/infrastructure/hooks/actions/useNotificationManagementActions.d.ts.map +0 -1
- package/lib/infrastructure/hooks/actions/useNotificationManagementActions.js +0 -78
- package/lib/infrastructure/hooks/actions/useNotificationManagementActions.js.map +0 -1
- package/lib/infrastructure/hooks/state/useNotificationsState.d.ts +0 -12
- package/lib/infrastructure/hooks/state/useNotificationsState.d.ts.map +0 -1
- package/lib/infrastructure/hooks/state/useNotificationsState.js +0 -30
- package/lib/infrastructure/hooks/state/useNotificationsState.js.map +0 -1
- package/lib/infrastructure/hooks/types.d.ts +0 -87
- package/lib/infrastructure/hooks/types.d.ts.map +0 -1
- package/lib/infrastructure/hooks/types.js +0 -8
- package/lib/infrastructure/hooks/types.js.map +0 -1
- package/lib/infrastructure/hooks/useNotificationSettings.d.ts +0 -10
- package/lib/infrastructure/hooks/useNotificationSettings.d.ts.map +0 -1
- package/lib/infrastructure/hooks/useNotificationSettings.js +0 -43
- package/lib/infrastructure/hooks/useNotificationSettings.js.map +0 -1
- package/lib/infrastructure/hooks/useNotifications.d.ts +0 -24
- package/lib/infrastructure/hooks/useNotifications.d.ts.map +0 -1
- package/lib/infrastructure/hooks/useNotifications.js +0 -72
- package/lib/infrastructure/hooks/useNotifications.js.map +0 -1
- package/lib/infrastructure/hooks/utils/useNotificationRefresh.d.ts +0 -8
- package/lib/infrastructure/hooks/utils/useNotificationRefresh.d.ts.map +0 -1
- package/lib/infrastructure/hooks/utils/useNotificationRefresh.js +0 -99
- package/lib/infrastructure/hooks/utils/useNotificationRefresh.js.map +0 -1
- package/lib/infrastructure/services/NotificationBadgeManager.d.ts +0 -5
- package/lib/infrastructure/services/NotificationBadgeManager.d.ts.map +0 -1
- package/lib/infrastructure/services/NotificationBadgeManager.js +0 -29
- package/lib/infrastructure/services/NotificationBadgeManager.js.map +0 -1
- package/lib/infrastructure/services/NotificationManager.d.ts +0 -59
- package/lib/infrastructure/services/NotificationManager.d.ts.map +0 -1
- package/lib/infrastructure/services/NotificationManager.js +0 -118
- package/lib/infrastructure/services/NotificationManager.js.map +0 -1
- package/lib/infrastructure/services/NotificationPermissions.d.ts +0 -6
- package/lib/infrastructure/services/NotificationPermissions.d.ts.map +0 -1
- package/lib/infrastructure/services/NotificationPermissions.js +0 -75
- package/lib/infrastructure/services/NotificationPermissions.js.map +0 -1
- package/lib/infrastructure/services/NotificationScheduler.d.ts +0 -8
- package/lib/infrastructure/services/NotificationScheduler.d.ts.map +0 -1
- package/lib/infrastructure/services/NotificationScheduler.js +0 -72
- package/lib/infrastructure/services/NotificationScheduler.js.map +0 -1
- package/lib/infrastructure/services/NotificationService.d.ts +0 -30
- package/lib/infrastructure/services/NotificationService.d.ts.map +0 -1
- package/lib/infrastructure/services/NotificationService.js +0 -41
- package/lib/infrastructure/services/NotificationService.js.map +0 -1
- package/lib/infrastructure/services/channels/ChannelManager.d.ts +0 -18
- package/lib/infrastructure/services/channels/ChannelManager.d.ts.map +0 -1
- package/lib/infrastructure/services/channels/ChannelManager.js +0 -87
- package/lib/infrastructure/services/channels/ChannelManager.js.map +0 -1
- package/lib/infrastructure/services/delivery/NotificationDelivery.d.ts +0 -10
- package/lib/infrastructure/services/delivery/NotificationDelivery.d.ts.map +0 -1
- package/lib/infrastructure/services/delivery/NotificationDelivery.js +0 -71
- package/lib/infrastructure/services/delivery/NotificationDelivery.js.map +0 -1
- package/lib/infrastructure/services/preferences/PreferencesManager.d.ts +0 -18
- package/lib/infrastructure/services/preferences/PreferencesManager.d.ts.map +0 -1
- package/lib/infrastructure/services/preferences/PreferencesManager.js +0 -65
- package/lib/infrastructure/services/preferences/PreferencesManager.js.map +0 -1
- package/lib/infrastructure/services/types.d.ts +0 -89
- package/lib/infrastructure/services/types.d.ts.map +0 -1
- package/lib/infrastructure/services/types.js +0 -7
- package/lib/infrastructure/services/types.js.map +0 -1
- package/lib/infrastructure/storage/NotificationsStore.d.ts +0 -23
- package/lib/infrastructure/storage/NotificationsStore.d.ts.map +0 -1
- package/lib/infrastructure/storage/NotificationsStore.js +0 -26
- package/lib/infrastructure/storage/NotificationsStore.js.map +0 -1
- package/lib/infrastructure/utils/dev.d.ts +0 -5
- package/lib/infrastructure/utils/dev.d.ts.map +0 -1
- package/lib/infrastructure/utils/dev.js +0 -24
- package/lib/infrastructure/utils/dev.js.map +0 -1
- package/lib/presentation/components/NotificationsSection.d.ts +0 -17
- package/lib/presentation/components/NotificationsSection.d.ts.map +0 -1
- package/lib/presentation/components/NotificationsSection.js +0 -133
- package/lib/presentation/components/NotificationsSection.js.map +0 -1
- package/lib/presentation/screens/NotificationsScreen.d.ts +0 -20
- package/lib/presentation/screens/NotificationsScreen.d.ts.map +0 -1
- package/lib/presentation/screens/NotificationsScreen.js +0 -74
- package/lib/presentation/screens/NotificationsScreen.js.map +0 -1
- package/src/__tests__/NotificationManager.test.ts +0 -215
- package/src/__tests__/useNotificationActions.test.ts +0 -189
- package/src/__tests__/useNotificationRefresh.test.ts +0 -213
- package/src/infrastructure/hooks/actions/useNotificationActions.ts +0 -131
- package/src/infrastructure/hooks/actions/useNotificationManagementActions.ts +0 -131
- package/src/infrastructure/hooks/state/useNotificationsState.ts +0 -46
- package/src/infrastructure/hooks/types.ts +0 -83
- package/src/infrastructure/hooks/useNotifications.ts +0 -96
- package/src/infrastructure/hooks/utils/useNotificationRefresh.ts +0 -131
- package/src/infrastructure/services/channels/ChannelManager.ts +0 -111
- package/src/infrastructure/services/delivery/NotificationDelivery.ts +0 -83
- package/src/infrastructure/services/preferences/PreferencesManager.ts +0 -77
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import { renderHook, act } from '@testing-library/react-hooks';
|
|
2
|
-
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
3
|
-
import { useNotificationRefresh } from '../src/infrastructure/hooks/utils/useNotificationRefresh';
|
|
4
|
-
|
|
5
|
-
// Mock AsyncStorage
|
|
6
|
-
jest.mock('@react-native-async-storage/async-storage', () => ({
|
|
7
|
-
getItem: jest.fn(),
|
|
8
|
-
setItem: jest.fn(),
|
|
9
|
-
}));
|
|
10
|
-
|
|
11
|
-
// Mock services
|
|
12
|
-
jest.mock('../src/infrastructure/services/channels/ChannelManager', () => ({
|
|
13
|
-
ChannelManager: jest.fn().mockImplementation(() => ({
|
|
14
|
-
getActiveChannels: jest.fn().mockResolvedValue([]),
|
|
15
|
-
})),
|
|
16
|
-
}));
|
|
17
|
-
|
|
18
|
-
jest.mock('../src/infrastructure/services/preferences/PreferencesManager', () => ({
|
|
19
|
-
PreferencesManager: jest.fn().mockImplementation(() => ({
|
|
20
|
-
get: jest.fn().mockResolvedValue(null),
|
|
21
|
-
})),
|
|
22
|
-
}));
|
|
23
|
-
|
|
24
|
-
describe('useNotificationRefresh', () => {
|
|
25
|
-
let mockSetters: any;
|
|
26
|
-
|
|
27
|
-
beforeEach(() => {
|
|
28
|
-
jest.clearAllMocks();
|
|
29
|
-
|
|
30
|
-
mockSetters = {
|
|
31
|
-
setNotifications: jest.fn(),
|
|
32
|
-
setChannels: jest.fn(),
|
|
33
|
-
setUnreadCount: jest.fn(),
|
|
34
|
-
setPreferences: jest.fn(),
|
|
35
|
-
setLoading: jest.fn(),
|
|
36
|
-
setError: jest.fn(),
|
|
37
|
-
setHasMore: jest.fn(),
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
(AsyncStorage.getItem as jest.Mock).mockResolvedValue('[]');
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
describe('refreshNotifications', () => {
|
|
44
|
-
it('should refresh notifications successfully', async () => {
|
|
45
|
-
const mockNotifications = [
|
|
46
|
-
{ id: 'notif-1', title: 'Test 1', read: false },
|
|
47
|
-
{ id: 'notif-2', title: 'Test 2', read: true },
|
|
48
|
-
{ id: 'notif-3', title: 'Test 3', read: false },
|
|
49
|
-
];
|
|
50
|
-
(AsyncStorage.getItem as jest.Mock).mockResolvedValue(JSON.stringify(mockNotifications));
|
|
51
|
-
|
|
52
|
-
const { result } = renderHook(() => useNotificationRefresh(2, mockSetters));
|
|
53
|
-
|
|
54
|
-
await act(async () => {
|
|
55
|
-
await result.current.refreshNotifications();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
expect(mockSetters.setLoading).toHaveBeenCalledWith(true);
|
|
59
|
-
expect(mockSetters.setError).toHaveBeenCalledWith(null);
|
|
60
|
-
expect(mockSetters.setNotifications).toHaveBeenCalledWith(mockNotifications.slice(0, 2));
|
|
61
|
-
expect(mockSetters.setUnreadCount).toHaveBeenCalledWith(2);
|
|
62
|
-
expect(mockSetters.setHasMore).toHaveBeenCalledWith(true);
|
|
63
|
-
expect(mockSetters.setLoading).toHaveBeenCalledWith(false);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('should handle empty notifications list', async () => {
|
|
67
|
-
(AsyncStorage.getItem as jest.Mock).mockResolvedValue('[]');
|
|
68
|
-
|
|
69
|
-
const { result } = renderHook(() => useNotificationRefresh(10, mockSetters));
|
|
70
|
-
|
|
71
|
-
await act(async () => {
|
|
72
|
-
await result.current.refreshNotifications();
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
expect(mockSetters.setNotifications).toHaveBeenCalledWith([]);
|
|
76
|
-
expect(mockSetters.setUnreadCount).toHaveBeenCalledWith(0);
|
|
77
|
-
expect(mockSetters.setHasMore).toHaveBeenCalledWith(false);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('should handle refresh errors', async () => {
|
|
81
|
-
(AsyncStorage.getItem as jest.Mock).mockRejectedValue(new Error('Refresh error'));
|
|
82
|
-
|
|
83
|
-
const { result } = renderHook(() => useNotificationRefresh(10, mockSetters));
|
|
84
|
-
|
|
85
|
-
await act(async () => {
|
|
86
|
-
await result.current.refreshNotifications();
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
expect(mockSetters.setError).toHaveBeenCalledWith('Refresh error');
|
|
90
|
-
expect(mockSetters.setLoading).toHaveBeenCalledWith(false);
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
describe('loadMoreNotifications', () => {
|
|
95
|
-
it('should load more notifications', async () => {
|
|
96
|
-
const mockNotifications = Array.from({ length: 15 }, (_, i) => ({
|
|
97
|
-
id: `notif-${i}`,
|
|
98
|
-
title: `Test ${i}`,
|
|
99
|
-
read: false,
|
|
100
|
-
}));
|
|
101
|
-
(AsyncStorage.getItem as jest.Mock).mockResolvedValue(JSON.stringify(mockNotifications));
|
|
102
|
-
|
|
103
|
-
const { result } = renderHook(() => useNotificationRefresh(5, mockSetters));
|
|
104
|
-
|
|
105
|
-
// First load
|
|
106
|
-
await act(async () => {
|
|
107
|
-
await result.current.refreshNotifications();
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// Load more
|
|
111
|
-
await act(async () => {
|
|
112
|
-
await result.current.loadMoreNotifications(5, true, false);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
expect(mockSetters.setNotifications).toHaveBeenCalledTimes(2);
|
|
116
|
-
expect(mockSetters.setHasMore).toHaveBeenCalledWith(true);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('should not load more if no more notifications', async () => {
|
|
120
|
-
const { result } = renderHook(() => useNotificationRefresh(10, mockSetters));
|
|
121
|
-
|
|
122
|
-
await act(async () => {
|
|
123
|
-
await result.current.loadMoreNotifications(5, false, false);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
expect(mockSetters.setNotifications).not.toHaveBeenCalled();
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('should not load more if already loading', async () => {
|
|
130
|
-
const { result } = renderHook(() => useNotificationRefresh(10, mockSetters));
|
|
131
|
-
|
|
132
|
-
await act(async () => {
|
|
133
|
-
await result.current.loadMoreNotifications(5, true, true);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
expect(mockSetters.setNotifications).not.toHaveBeenCalled();
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
describe('refreshChannels', () => {
|
|
141
|
-
it('should refresh channels successfully', async () => {
|
|
142
|
-
const mockChannels = [
|
|
143
|
-
{ id: 'channel-1', name: 'Channel 1' },
|
|
144
|
-
{ id: 'channel-2', name: 'Channel 2' },
|
|
145
|
-
];
|
|
146
|
-
|
|
147
|
-
const { result } = renderHook(() => useNotificationRefresh(10, mockSetters));
|
|
148
|
-
|
|
149
|
-
await act(async () => {
|
|
150
|
-
await result.current.refreshChannels();
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
expect(mockSetters.setChannels).toHaveBeenCalledWith(mockChannels);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('should handle channel refresh errors silently', async () => {
|
|
157
|
-
const ChannelManager = require('../src/infrastructure/services/channels/ChannelManager').ChannelManager;
|
|
158
|
-
ChannelManager.mockImplementation(() => ({
|
|
159
|
-
getActiveChannels: jest.fn().mockRejectedValue(new Error('Channel error')),
|
|
160
|
-
}));
|
|
161
|
-
|
|
162
|
-
const { result } = renderHook(() => useNotificationRefresh(10, mockSetters));
|
|
163
|
-
|
|
164
|
-
await act(async () => {
|
|
165
|
-
await result.current.refreshChannels();
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
expect(mockSetters.setChannels).not.toHaveBeenCalled();
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
describe('refreshPreferences', () => {
|
|
173
|
-
it('should refresh preferences successfully', async () => {
|
|
174
|
-
const mockPreferences = { enabled: true, sound: false };
|
|
175
|
-
|
|
176
|
-
const { result } = renderHook(() => useNotificationRefresh(10, mockSetters));
|
|
177
|
-
|
|
178
|
-
await act(async () => {
|
|
179
|
-
await result.current.refreshPreferences();
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
expect(mockSetters.setPreferences).toHaveBeenCalledWith(mockPreferences);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('should handle preferences refresh errors silently', async () => {
|
|
186
|
-
const PreferencesManager = require('../src/infrastructure/services/preferences/PreferencesManager').PreferencesManager;
|
|
187
|
-
PreferencesManager.mockImplementation(() => ({
|
|
188
|
-
get: jest.fn().mockRejectedValue(new Error('Preferences error')),
|
|
189
|
-
}));
|
|
190
|
-
|
|
191
|
-
const { result } = renderHook(() => useNotificationRefresh(10, mockSetters));
|
|
192
|
-
|
|
193
|
-
await act(async () => {
|
|
194
|
-
await result.current.refreshPreferences();
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
expect(mockSetters.setPreferences).not.toHaveBeenCalled();
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
describe('cleanup', () => {
|
|
202
|
-
it('should cleanup properly', async () => {
|
|
203
|
-
const { result } = renderHook(() => useNotificationRefresh(10, mockSetters));
|
|
204
|
-
|
|
205
|
-
await act(async () => {
|
|
206
|
-
result.current.cleanup();
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// Should not throw any errors
|
|
210
|
-
expect(true).toBe(true);
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
});
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { useCallback } from 'react';
|
|
2
|
-
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
3
|
-
import * as Notifications from 'expo-notifications';
|
|
4
|
-
import type {
|
|
5
|
-
SendNotificationOptions,
|
|
6
|
-
Notification,
|
|
7
|
-
NotificationChannel,
|
|
8
|
-
NotificationPreferences,
|
|
9
|
-
} from '../types';
|
|
10
|
-
import { NotificationDelivery } from '../../services/delivery/NotificationDelivery';
|
|
11
|
-
import { ChannelManager } from '../../services/channels/ChannelManager';
|
|
12
|
-
import { PreferencesManager } from '../../services/preferences/PreferencesManager';
|
|
13
|
-
import { devLog } from '../../utils/dev';
|
|
14
|
-
|
|
15
|
-
export const useNotificationActions = (state: any, setters: any) => {
|
|
16
|
-
const {
|
|
17
|
-
setNotifications,
|
|
18
|
-
setUnreadCount,
|
|
19
|
-
setChannels,
|
|
20
|
-
setPreferences,
|
|
21
|
-
setError,
|
|
22
|
-
} = setters;
|
|
23
|
-
|
|
24
|
-
const notificationDelivery = new NotificationDelivery();
|
|
25
|
-
const channelManager = new ChannelManager();
|
|
26
|
-
const preferencesManager = new PreferencesManager();
|
|
27
|
-
|
|
28
|
-
const sendNotification = useCallback(
|
|
29
|
-
async (options: SendNotificationOptions): Promise<Notification[]> => {
|
|
30
|
-
try {
|
|
31
|
-
setError(null);
|
|
32
|
-
|
|
33
|
-
const notification: Notification = {
|
|
34
|
-
id: `notif_${Date.now()}`,
|
|
35
|
-
title: options.title,
|
|
36
|
-
body: options.body,
|
|
37
|
-
data: options.data,
|
|
38
|
-
scheduled_for: options.scheduled_for?.toISOString(),
|
|
39
|
-
created_at: new Date().toISOString(),
|
|
40
|
-
read: false,
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const data = await AsyncStorage.getItem('@notifications:list');
|
|
44
|
-
const notifications: Notification[] = data ? JSON.parse(data) : [];
|
|
45
|
-
notifications.unshift(notification);
|
|
46
|
-
await AsyncStorage.setItem(
|
|
47
|
-
'@notifications:list',
|
|
48
|
-
JSON.stringify(notifications)
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
await notificationDelivery.deliver(notification);
|
|
52
|
-
|
|
53
|
-
devLog('[useNotificationActions] Notification sent:', notification.id);
|
|
54
|
-
|
|
55
|
-
return [notification];
|
|
56
|
-
} catch (err) {
|
|
57
|
-
setError(
|
|
58
|
-
err instanceof Error ? err.message : 'Failed to send notification'
|
|
59
|
-
);
|
|
60
|
-
return [];
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
[setError]
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
const markAsRead = useCallback(
|
|
67
|
-
async (notificationId: string): Promise<boolean> => {
|
|
68
|
-
try {
|
|
69
|
-
const data = await AsyncStorage.getItem('@notifications:list');
|
|
70
|
-
const notifications: Notification[] = data ? JSON.parse(data) : [];
|
|
71
|
-
|
|
72
|
-
const updated = notifications.map((n) =>
|
|
73
|
-
n.id === notificationId ? { ...n, read: true } : n
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
await AsyncStorage.setItem(
|
|
77
|
-
'@notifications:list',
|
|
78
|
-
JSON.stringify(updated)
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
setNotifications((prev: Notification[]) =>
|
|
82
|
-
prev.map((n) =>
|
|
83
|
-
n.id === notificationId ? { ...n, read: true } : n
|
|
84
|
-
)
|
|
85
|
-
);
|
|
86
|
-
setUnreadCount((prev: number) => Math.max(0, prev - 1));
|
|
87
|
-
|
|
88
|
-
devLog('[useNotificationActions] Marked as read:', notificationId);
|
|
89
|
-
|
|
90
|
-
return true;
|
|
91
|
-
} catch (err) {
|
|
92
|
-
setError(
|
|
93
|
-
err instanceof Error ? err.message : 'Failed to mark as read'
|
|
94
|
-
);
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
[setNotifications, setUnreadCount, setError]
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
const markAllAsRead = useCallback(async (): Promise<boolean> => {
|
|
102
|
-
try {
|
|
103
|
-
const data = await AsyncStorage.getItem('@notifications:list');
|
|
104
|
-
const notifications: Notification[] = data ? JSON.parse(data) : [];
|
|
105
|
-
|
|
106
|
-
const updated = notifications.map((n) => ({ ...n, read: true }));
|
|
107
|
-
|
|
108
|
-
await AsyncStorage.setItem('@notifications:list', JSON.stringify(updated));
|
|
109
|
-
|
|
110
|
-
setNotifications((prev: Notification[]) =>
|
|
111
|
-
prev.map((n) => ({ ...n, read: true }))
|
|
112
|
-
);
|
|
113
|
-
setUnreadCount(0);
|
|
114
|
-
|
|
115
|
-
devLog('[useNotificationActions] All notifications marked as read');
|
|
116
|
-
|
|
117
|
-
return true;
|
|
118
|
-
} catch (err) {
|
|
119
|
-
setError(
|
|
120
|
-
err instanceof Error ? err.message : 'Failed to mark all as read'
|
|
121
|
-
);
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
|
-
}, [setNotifications, setUnreadCount, setError]);
|
|
125
|
-
|
|
126
|
-
return {
|
|
127
|
-
sendNotification,
|
|
128
|
-
markAsRead,
|
|
129
|
-
markAllAsRead,
|
|
130
|
-
};
|
|
131
|
-
};
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { useCallback } from 'react';
|
|
2
|
-
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
3
|
-
import type {
|
|
4
|
-
Notification,
|
|
5
|
-
NotificationChannel,
|
|
6
|
-
NotificationPreferences,
|
|
7
|
-
} from '../types';
|
|
8
|
-
import { ChannelManager } from '../../services/channels/ChannelManager';
|
|
9
|
-
import { PreferencesManager } from '../../services/preferences/PreferencesManager';
|
|
10
|
-
import { devLog } from '../../utils/dev';
|
|
11
|
-
|
|
12
|
-
export const useNotificationManagementActions = (state: any, setters: any) => {
|
|
13
|
-
const { setNotifications, setUnreadCount, setChannels, setPreferences, setError } = setters;
|
|
14
|
-
|
|
15
|
-
const channelManager = new ChannelManager();
|
|
16
|
-
const preferencesManager = new PreferencesManager();
|
|
17
|
-
|
|
18
|
-
const deleteNotification = useCallback(
|
|
19
|
-
async (notificationId: string): Promise<boolean> => {
|
|
20
|
-
try {
|
|
21
|
-
const data = await AsyncStorage.getItem('@notifications:list');
|
|
22
|
-
const notifications: Notification[] = data ? JSON.parse(data) : [];
|
|
23
|
-
|
|
24
|
-
const deleted = notifications.find((n) => n.id === notificationId);
|
|
25
|
-
const filtered = notifications.filter((n) => n.id !== notificationId);
|
|
26
|
-
|
|
27
|
-
await AsyncStorage.setItem(
|
|
28
|
-
'@notifications:list',
|
|
29
|
-
JSON.stringify(filtered)
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
setNotifications((prev: Notification[]) =>
|
|
33
|
-
prev.filter((n) => n.id !== notificationId)
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
if (deleted && !deleted.read) {
|
|
37
|
-
setUnreadCount((prev: number) => Math.max(0, prev - 1));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
devLog('[useNotificationManagementActions] Deleted notification:', notificationId);
|
|
41
|
-
|
|
42
|
-
return true;
|
|
43
|
-
} catch (err) {
|
|
44
|
-
setError(
|
|
45
|
-
err instanceof Error ? err.message : 'Failed to delete notification'
|
|
46
|
-
);
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
},
|
|
50
|
-
[setNotifications, setUnreadCount, setError]
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
const registerChannel = useCallback(
|
|
54
|
-
async (
|
|
55
|
-
channelType: 'push' | 'in_app',
|
|
56
|
-
preferences: Record<string, any> = {}
|
|
57
|
-
): Promise<NotificationChannel | null> => {
|
|
58
|
-
try {
|
|
59
|
-
const channel = await channelManager.register(channelType, preferences);
|
|
60
|
-
if (channel) {
|
|
61
|
-
setChannels((prev: NotificationChannel[]) => [...prev, channel]);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
devLog('[useNotificationManagementActions] Channel registered:', channel?.id);
|
|
65
|
-
|
|
66
|
-
return channel;
|
|
67
|
-
} catch (err) {
|
|
68
|
-
setError(
|
|
69
|
-
err instanceof Error ? err.message : 'Failed to register channel'
|
|
70
|
-
);
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
[setChannels, setError]
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
const verifyChannel = useCallback(
|
|
78
|
-
async (channelId: string): Promise<boolean> => {
|
|
79
|
-
try {
|
|
80
|
-
const success = await channelManager.verify(channelId);
|
|
81
|
-
if (success) {
|
|
82
|
-
setChannels((prev: NotificationChannel[]) =>
|
|
83
|
-
prev.map((c) =>
|
|
84
|
-
c.id === channelId ? { ...c, is_verified: true } : c
|
|
85
|
-
)
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
devLog('[useNotificationManagementActions] Channel verified:', channelId, success);
|
|
90
|
-
|
|
91
|
-
return success;
|
|
92
|
-
} catch (err) {
|
|
93
|
-
setError(
|
|
94
|
-
err instanceof Error ? err.message : 'Failed to verify channel'
|
|
95
|
-
);
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
|
-
},
|
|
99
|
-
[setChannels, setError]
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
const updatePreferences = useCallback(
|
|
103
|
-
async (newPreferences: Partial<NotificationPreferences>): Promise<boolean> => {
|
|
104
|
-
try {
|
|
105
|
-
const success = await preferencesManager.update(newPreferences);
|
|
106
|
-
if (success) {
|
|
107
|
-
setPreferences((prev: NotificationPreferences | null) =>
|
|
108
|
-
prev ? { ...prev, ...newPreferences } : null
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
devLog('[useNotificationManagementActions] Preferences updated:', newPreferences);
|
|
113
|
-
|
|
114
|
-
return success;
|
|
115
|
-
} catch (err) {
|
|
116
|
-
setError(
|
|
117
|
-
err instanceof Error ? err.message : 'Failed to update preferences'
|
|
118
|
-
);
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
},
|
|
122
|
-
[setPreferences, setError]
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
return {
|
|
126
|
-
deleteNotification,
|
|
127
|
-
registerChannel,
|
|
128
|
-
verifyChannel,
|
|
129
|
-
updatePreferences,
|
|
130
|
-
};
|
|
131
|
-
};
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
import type { NotificationsState } from '../types';
|
|
3
|
-
|
|
4
|
-
export const useNotificationsState = () => {
|
|
5
|
-
const [state, setState] = useState<NotificationsState>({
|
|
6
|
-
notifications: [],
|
|
7
|
-
channels: [],
|
|
8
|
-
unreadCount: 0,
|
|
9
|
-
preferences: null,
|
|
10
|
-
loading: false,
|
|
11
|
-
error: null,
|
|
12
|
-
hasMore: true,
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
const setNotifications = (notifications: NotificationsState['notifications']) =>
|
|
16
|
-
setState(prev => ({ ...prev, notifications }));
|
|
17
|
-
|
|
18
|
-
const setChannels = (channels: NotificationsState['channels']) =>
|
|
19
|
-
setState(prev => ({ ...prev, channels }));
|
|
20
|
-
|
|
21
|
-
const setUnreadCount = (unreadCount: number) =>
|
|
22
|
-
setState(prev => ({ ...prev, unreadCount }));
|
|
23
|
-
|
|
24
|
-
const setPreferences = (preferences: NotificationsState['preferences']) =>
|
|
25
|
-
setState(prev => ({ ...prev, preferences }));
|
|
26
|
-
|
|
27
|
-
const setLoading = (loading: boolean) =>
|
|
28
|
-
setState(prev => ({ ...prev, loading }));
|
|
29
|
-
|
|
30
|
-
const setError = (error: string | null) =>
|
|
31
|
-
setState(prev => ({ ...prev, error }));
|
|
32
|
-
|
|
33
|
-
const setHasMore = (hasMore: boolean) =>
|
|
34
|
-
setState(prev => ({ ...prev, hasMore }));
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
state,
|
|
38
|
-
setNotifications,
|
|
39
|
-
setChannels,
|
|
40
|
-
setUnreadCount,
|
|
41
|
-
setPreferences,
|
|
42
|
-
setLoading,
|
|
43
|
-
setError,
|
|
44
|
-
setHasMore,
|
|
45
|
-
};
|
|
46
|
-
};
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Notification Hook Types - Offline-First
|
|
3
|
-
*
|
|
4
|
-
* All types for offline local notifications.
|
|
5
|
-
* NO backend dependencies.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Local notification entity
|
|
10
|
-
*/
|
|
11
|
-
export interface Notification {
|
|
12
|
-
id: string;
|
|
13
|
-
title: string;
|
|
14
|
-
body: string;
|
|
15
|
-
data?: Record<string, any>;
|
|
16
|
-
scheduled_for?: string;
|
|
17
|
-
created_at: string;
|
|
18
|
-
read: boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Notification channel (push or in-app only)
|
|
23
|
-
*/
|
|
24
|
-
export interface NotificationChannel {
|
|
25
|
-
id: string;
|
|
26
|
-
channel_type: 'push' | 'in_app';
|
|
27
|
-
channel_address: string;
|
|
28
|
-
preferences: Record<string, any>;
|
|
29
|
-
is_verified: boolean;
|
|
30
|
-
is_active: boolean;
|
|
31
|
-
created_at: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Notification preferences
|
|
36
|
-
*/
|
|
37
|
-
export interface NotificationPreferences {
|
|
38
|
-
push_enabled: boolean;
|
|
39
|
-
quiet_hours: {
|
|
40
|
-
enabled: boolean;
|
|
41
|
-
start_time: string;
|
|
42
|
-
end_time: string;
|
|
43
|
-
timezone: string;
|
|
44
|
-
};
|
|
45
|
-
categories: {
|
|
46
|
-
reminders: { push: boolean; in_app: boolean };
|
|
47
|
-
updates: { push: boolean; in_app: boolean };
|
|
48
|
-
alerts: { push: boolean; in_app: boolean };
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Options for sending notification
|
|
54
|
-
*/
|
|
55
|
-
export interface SendNotificationOptions {
|
|
56
|
-
title: string;
|
|
57
|
-
body: string;
|
|
58
|
-
data?: Record<string, any>;
|
|
59
|
-
scheduled_for?: Date;
|
|
60
|
-
category?: 'reminders' | 'updates' | 'alerts';
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* State for useNotifications hook
|
|
65
|
-
*/
|
|
66
|
-
export interface NotificationsState {
|
|
67
|
-
notifications: Notification[];
|
|
68
|
-
channels: NotificationChannel[];
|
|
69
|
-
unreadCount: number;
|
|
70
|
-
preferences: NotificationPreferences | null;
|
|
71
|
-
loading: boolean;
|
|
72
|
-
error: string | null;
|
|
73
|
-
hasMore: boolean;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Options for useNotifications hook
|
|
78
|
-
*/
|
|
79
|
-
export interface UseNotificationsOptions {
|
|
80
|
-
autoRefresh?: boolean;
|
|
81
|
-
refreshInterval?: number;
|
|
82
|
-
pageSize?: number;
|
|
83
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { useEffect, useCallback } from 'react';
|
|
2
|
-
import { useNotificationsState } from './state/useNotificationsState';
|
|
3
|
-
import { useNotificationActions } from './actions/useNotificationActions';
|
|
4
|
-
import { useNotificationManagementActions } from './actions/useNotificationManagementActions';
|
|
5
|
-
import { useNotificationRefresh } from './utils/useNotificationRefresh';
|
|
6
|
-
import type { UseNotificationsOptions } from './types';
|
|
7
|
-
import { devLog } from '../utils/dev';
|
|
8
|
-
|
|
9
|
-
export * from './types';
|
|
10
|
-
|
|
11
|
-
export function useNotifications(userId: string, options: UseNotificationsOptions = {}) {
|
|
12
|
-
const { autoRefresh = false, refreshInterval = 30000, pageSize = 20 } = options;
|
|
13
|
-
|
|
14
|
-
const {
|
|
15
|
-
state,
|
|
16
|
-
setNotifications,
|
|
17
|
-
setChannels,
|
|
18
|
-
setUnreadCount,
|
|
19
|
-
setPreferences,
|
|
20
|
-
setLoading,
|
|
21
|
-
setError,
|
|
22
|
-
setHasMore,
|
|
23
|
-
} = useNotificationsState();
|
|
24
|
-
|
|
25
|
-
const setters = {
|
|
26
|
-
setNotifications,
|
|
27
|
-
setChannels,
|
|
28
|
-
setUnreadCount,
|
|
29
|
-
setPreferences,
|
|
30
|
-
setLoading,
|
|
31
|
-
setError,
|
|
32
|
-
setHasMore,
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const actions = useNotificationActions(state, setters);
|
|
36
|
-
const managementActions = useNotificationManagementActions(state, setters);
|
|
37
|
-
const refresh = useNotificationRefresh(pageSize, setters);
|
|
38
|
-
|
|
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]);
|
|
55
|
-
|
|
56
|
-
// Load initial data
|
|
57
|
-
useEffect(() => {
|
|
58
|
-
if (userId) {
|
|
59
|
-
refresh.refreshNotifications();
|
|
60
|
-
refresh.refreshChannels();
|
|
61
|
-
refresh.refreshPreferences();
|
|
62
|
-
} else {
|
|
63
|
-
cleanup();
|
|
64
|
-
}
|
|
65
|
-
}, [userId, cleanup]);
|
|
66
|
-
|
|
67
|
-
// Auto-refresh setup with proper cleanup
|
|
68
|
-
useEffect(() => {
|
|
69
|
-
if (!autoRefresh || !userId) return;
|
|
70
|
-
|
|
71
|
-
const interval = setInterval(() => {
|
|
72
|
-
refresh.refreshNotifications();
|
|
73
|
-
}, refreshInterval);
|
|
74
|
-
|
|
75
|
-
return () => {
|
|
76
|
-
clearInterval(interval);
|
|
77
|
-
devLog('[useNotifications] Auto-refresh interval cleared');
|
|
78
|
-
};
|
|
79
|
-
}, [autoRefresh, userId, refreshInterval]);
|
|
80
|
-
|
|
81
|
-
// Cleanup on unmount
|
|
82
|
-
useEffect(() => {
|
|
83
|
-
return () => {
|
|
84
|
-
devLog('[useNotifications] Component unmounted, cleaning up');
|
|
85
|
-
};
|
|
86
|
-
}, []);
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
...state,
|
|
90
|
-
...actions,
|
|
91
|
-
...managementActions,
|
|
92
|
-
...refresh,
|
|
93
|
-
loadMoreNotifications,
|
|
94
|
-
cleanup,
|
|
95
|
-
};
|
|
96
|
-
}
|