@umituz/react-native-notifications 1.0.6 → 1.1.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/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
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { renderHook, act } from '@testing-library/react-hooks';
|
|
2
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
3
|
+
import { useNotificationActions } from '../src/infrastructure/hooks/actions/useNotificationActions';
|
|
4
|
+
import { useNotificationsState } from '../src/infrastructure/hooks/state/useNotificationsState';
|
|
5
|
+
|
|
6
|
+
// Mock AsyncStorage
|
|
7
|
+
jest.mock('@react-native-async-storage/async-storage', () => ({
|
|
8
|
+
getItem: jest.fn(),
|
|
9
|
+
setItem: jest.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
// Mock expo-notifications
|
|
13
|
+
jest.mock('expo-notifications', () => ({
|
|
14
|
+
scheduleNotificationAsync: jest.fn(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// Mock services
|
|
18
|
+
jest.mock('../src/infrastructure/services/delivery/NotificationDelivery', () => ({
|
|
19
|
+
NotificationDelivery: jest.fn().mockImplementation(() => ({
|
|
20
|
+
deliver: jest.fn().mockResolvedValue(undefined),
|
|
21
|
+
})),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
jest.mock('../src/infrastructure/services/channels/ChannelManager', () => ({
|
|
25
|
+
ChannelManager: jest.fn().mockImplementation(() => ({
|
|
26
|
+
register: jest.fn(),
|
|
27
|
+
verify: jest.fn(),
|
|
28
|
+
})),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
jest.mock('../src/infrastructure/services/preferences/PreferencesManager', () => ({
|
|
32
|
+
PreferencesManager: jest.fn().mockImplementation(() => ({
|
|
33
|
+
update: jest.fn().mockResolvedValue(true),
|
|
34
|
+
})),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
describe('useNotificationActions', () => {
|
|
38
|
+
let mockState: any;
|
|
39
|
+
let mockSetters: any;
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
jest.clearAllMocks();
|
|
43
|
+
|
|
44
|
+
mockState = {
|
|
45
|
+
notifications: [],
|
|
46
|
+
channels: [],
|
|
47
|
+
unreadCount: 0,
|
|
48
|
+
preferences: null,
|
|
49
|
+
loading: false,
|
|
50
|
+
error: null,
|
|
51
|
+
hasMore: true,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
mockSetters = {
|
|
55
|
+
setNotifications: jest.fn(),
|
|
56
|
+
setChannels: jest.fn(),
|
|
57
|
+
setUnreadCount: jest.fn(),
|
|
58
|
+
setPreferences: jest.fn(),
|
|
59
|
+
setLoading: jest.fn(),
|
|
60
|
+
setError: jest.fn(),
|
|
61
|
+
setHasMore: jest.fn(),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
(AsyncStorage.getItem as jest.Mock).mockResolvedValue('[]');
|
|
65
|
+
(AsyncStorage.setItem as jest.Mock).mockResolvedValue(undefined);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('sendNotification', () => {
|
|
69
|
+
it('should send a notification successfully', async () => {
|
|
70
|
+
const { result } = renderHook(() => useNotificationActions(mockState, mockSetters));
|
|
71
|
+
|
|
72
|
+
const options = {
|
|
73
|
+
title: 'Test Notification',
|
|
74
|
+
body: 'Test body',
|
|
75
|
+
data: { key: 'value' },
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
let resultNotifications: any[] = [];
|
|
79
|
+
await act(async () => {
|
|
80
|
+
resultNotifications = await result.current.sendNotification(options);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(resultNotifications).toHaveLength(1);
|
|
84
|
+
expect(resultNotifications[0]).toMatchObject({
|
|
85
|
+
title: 'Test Notification',
|
|
86
|
+
body: 'Test body',
|
|
87
|
+
data: { key: 'value' },
|
|
88
|
+
read: false,
|
|
89
|
+
});
|
|
90
|
+
expect(AsyncStorage.setItem).toHaveBeenCalled();
|
|
91
|
+
expect(mockSetters.setError).toHaveBeenCalledWith(null);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should handle send notification errors', async () => {
|
|
95
|
+
(AsyncStorage.setItem as jest.Mock).mockRejectedValue(new Error('Storage error'));
|
|
96
|
+
const { result } = renderHook(() => useNotificationActions(mockState, mockSetters));
|
|
97
|
+
|
|
98
|
+
const options = {
|
|
99
|
+
title: 'Test',
|
|
100
|
+
body: 'Test',
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
let resultNotifications: any[] = [];
|
|
104
|
+
await act(async () => {
|
|
105
|
+
resultNotifications = await result.current.sendNotification(options);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(resultNotifications).toHaveLength(0);
|
|
109
|
+
expect(mockSetters.setError).toHaveBeenCalledWith('Storage error');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should schedule notification with date', async () => {
|
|
113
|
+
const { result } = renderHook(() => useNotificationActions(mockState, mockSetters));
|
|
114
|
+
|
|
115
|
+
const scheduledDate = new Date('2025-01-15T09:00:00');
|
|
116
|
+
const options = {
|
|
117
|
+
title: 'Scheduled Notification',
|
|
118
|
+
body: 'Test body',
|
|
119
|
+
scheduled_for: scheduledDate,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
await act(async () => {
|
|
123
|
+
await result.current.sendNotification(options);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const notificationData = JSON.parse((AsyncStorage.setItem as jest.Mock).mock.calls[0][1]);
|
|
127
|
+
expect(notificationData[0]).toMatchObject({
|
|
128
|
+
scheduled_for: scheduledDate.toISOString(),
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('markAsRead', () => {
|
|
134
|
+
it('should mark notification as read successfully', async () => {
|
|
135
|
+
const mockNotifications = [
|
|
136
|
+
{ id: 'notif-1', title: 'Test 1', read: false },
|
|
137
|
+
{ id: 'notif-2', title: 'Test 2', read: false },
|
|
138
|
+
];
|
|
139
|
+
(AsyncStorage.getItem as jest.Mock).mockResolvedValue(JSON.stringify(mockNotifications));
|
|
140
|
+
|
|
141
|
+
const { result } = renderHook(() => useNotificationActions(mockState, mockSetters));
|
|
142
|
+
|
|
143
|
+
let success = false;
|
|
144
|
+
await act(async () => {
|
|
145
|
+
success = await result.current.markAsRead('notif-1');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(success).toBe(true);
|
|
149
|
+
expect(AsyncStorage.setItem).toHaveBeenCalled();
|
|
150
|
+
expect(mockSetters.setUnreadCount).toHaveBeenCalledWith(0);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should handle mark as read errors', async () => {
|
|
154
|
+
(AsyncStorage.getItem as jest.Mock).mockRejectedValue(new Error('Read error'));
|
|
155
|
+
const { result } = renderHook(() => useNotificationActions(mockState, mockSetters));
|
|
156
|
+
|
|
157
|
+
let success = false;
|
|
158
|
+
await act(async () => {
|
|
159
|
+
success = await result.current.markAsRead('notif-1');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(success).toBe(false);
|
|
163
|
+
expect(mockSetters.setError).toHaveBeenCalledWith('Read error');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('markAllAsRead', () => {
|
|
168
|
+
it('should mark all notifications as read', async () => {
|
|
169
|
+
const mockNotifications = [
|
|
170
|
+
{ id: 'notif-1', title: 'Test 1', read: false },
|
|
171
|
+
{ id: 'notif-2', title: 'Test 2', read: false },
|
|
172
|
+
];
|
|
173
|
+
(AsyncStorage.getItem as jest.Mock).mockResolvedValue(JSON.stringify(mockNotifications));
|
|
174
|
+
|
|
175
|
+
const { result } = renderHook(() => useNotificationActions(mockState, mockSetters));
|
|
176
|
+
|
|
177
|
+
let success = false;
|
|
178
|
+
await act(async () => {
|
|
179
|
+
success = await result.current.markAllAsRead();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(success).toBe(true);
|
|
183
|
+
expect(mockSetters.setUnreadCount).toHaveBeenCalledWith(0);
|
|
184
|
+
|
|
185
|
+
const savedData = JSON.parse((AsyncStorage.setItem as jest.Mock).mock.calls[0][1]);
|
|
186
|
+
expect(savedData.every((n: any) => n.read)).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
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
|
+
});
|
|
@@ -10,13 +10,8 @@ import type {
|
|
|
10
10
|
import { NotificationDelivery } from '../../services/delivery/NotificationDelivery';
|
|
11
11
|
import { ChannelManager } from '../../services/channels/ChannelManager';
|
|
12
12
|
import { PreferencesManager } from '../../services/preferences/PreferencesManager';
|
|
13
|
+
import { devLog } from '../../utils/dev';
|
|
13
14
|
|
|
14
|
-
/**
|
|
15
|
-
* useNotificationActions - Offline Notification Actions
|
|
16
|
-
*
|
|
17
|
-
* All actions use AsyncStorage and expo-notifications.
|
|
18
|
-
* NO backend - pure offline.
|
|
19
|
-
*/
|
|
20
15
|
export const useNotificationActions = (state: any, setters: any) => {
|
|
21
16
|
const {
|
|
22
17
|
setNotifications,
|
|
@@ -35,7 +30,6 @@ export const useNotificationActions = (state: any, setters: any) => {
|
|
|
35
30
|
try {
|
|
36
31
|
setError(null);
|
|
37
32
|
|
|
38
|
-
// Create notification
|
|
39
33
|
const notification: Notification = {
|
|
40
34
|
id: `notif_${Date.now()}`,
|
|
41
35
|
title: options.title,
|
|
@@ -46,7 +40,6 @@ export const useNotificationActions = (state: any, setters: any) => {
|
|
|
46
40
|
read: false,
|
|
47
41
|
};
|
|
48
42
|
|
|
49
|
-
// Save to AsyncStorage
|
|
50
43
|
const data = await AsyncStorage.getItem('@notifications:list');
|
|
51
44
|
const notifications: Notification[] = data ? JSON.parse(data) : [];
|
|
52
45
|
notifications.unshift(notification);
|
|
@@ -55,9 +48,10 @@ export const useNotificationActions = (state: any, setters: any) => {
|
|
|
55
48
|
JSON.stringify(notifications)
|
|
56
49
|
);
|
|
57
50
|
|
|
58
|
-
// Deliver using expo-notifications
|
|
59
51
|
await notificationDelivery.deliver(notification);
|
|
60
52
|
|
|
53
|
+
devLog('[useNotificationActions] Notification sent:', notification.id);
|
|
54
|
+
|
|
61
55
|
return [notification];
|
|
62
56
|
} catch (err) {
|
|
63
57
|
setError(
|
|
@@ -91,6 +85,8 @@ export const useNotificationActions = (state: any, setters: any) => {
|
|
|
91
85
|
);
|
|
92
86
|
setUnreadCount((prev: number) => Math.max(0, prev - 1));
|
|
93
87
|
|
|
88
|
+
devLog('[useNotificationActions] Marked as read:', notificationId);
|
|
89
|
+
|
|
94
90
|
return true;
|
|
95
91
|
} catch (err) {
|
|
96
92
|
setError(
|
|
@@ -116,6 +112,8 @@ export const useNotificationActions = (state: any, setters: any) => {
|
|
|
116
112
|
);
|
|
117
113
|
setUnreadCount(0);
|
|
118
114
|
|
|
115
|
+
devLog('[useNotificationActions] All notifications marked as read');
|
|
116
|
+
|
|
119
117
|
return true;
|
|
120
118
|
} catch (err) {
|
|
121
119
|
setError(
|
|
@@ -125,109 +123,9 @@ export const useNotificationActions = (state: any, setters: any) => {
|
|
|
125
123
|
}
|
|
126
124
|
}, [setNotifications, setUnreadCount, setError]);
|
|
127
125
|
|
|
128
|
-
const deleteNotification = useCallback(
|
|
129
|
-
async (notificationId: string): Promise<boolean> => {
|
|
130
|
-
try {
|
|
131
|
-
const data = await AsyncStorage.getItem('@notifications:list');
|
|
132
|
-
const notifications: Notification[] = data ? JSON.parse(data) : [];
|
|
133
|
-
|
|
134
|
-
const deleted = notifications.find((n) => n.id === notificationId);
|
|
135
|
-
const filtered = notifications.filter((n) => n.id !== notificationId);
|
|
136
|
-
|
|
137
|
-
await AsyncStorage.setItem(
|
|
138
|
-
'@notifications:list',
|
|
139
|
-
JSON.stringify(filtered)
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
setNotifications((prev: Notification[]) =>
|
|
143
|
-
prev.filter((n) => n.id !== notificationId)
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
if (deleted && !deleted.read) {
|
|
147
|
-
setUnreadCount((prev: number) => Math.max(0, prev - 1));
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return true;
|
|
151
|
-
} catch (err) {
|
|
152
|
-
setError(
|
|
153
|
-
err instanceof Error ? err.message : 'Failed to delete notification'
|
|
154
|
-
);
|
|
155
|
-
return false;
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
[setNotifications, setUnreadCount, setError]
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
const registerChannel = useCallback(
|
|
162
|
-
async (
|
|
163
|
-
channelType: 'push' | 'in_app',
|
|
164
|
-
preferences: Record<string, any> = {}
|
|
165
|
-
): Promise<NotificationChannel | null> => {
|
|
166
|
-
try {
|
|
167
|
-
const channel = await channelManager.register(channelType, preferences);
|
|
168
|
-
if (channel) {
|
|
169
|
-
setChannels((prev: NotificationChannel[]) => [...prev, channel]);
|
|
170
|
-
}
|
|
171
|
-
return channel;
|
|
172
|
-
} catch (err) {
|
|
173
|
-
setError(
|
|
174
|
-
err instanceof Error ? err.message : 'Failed to register channel'
|
|
175
|
-
);
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
},
|
|
179
|
-
[setChannels, setError]
|
|
180
|
-
);
|
|
181
|
-
|
|
182
|
-
const verifyChannel = useCallback(
|
|
183
|
-
async (channelId: string): Promise<boolean> => {
|
|
184
|
-
try {
|
|
185
|
-
const success = await channelManager.verify(channelId);
|
|
186
|
-
if (success) {
|
|
187
|
-
setChannels((prev: NotificationChannel[]) =>
|
|
188
|
-
prev.map((c) =>
|
|
189
|
-
c.id === channelId ? { ...c, is_verified: true } : c
|
|
190
|
-
)
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
return success;
|
|
194
|
-
} catch (err) {
|
|
195
|
-
setError(
|
|
196
|
-
err instanceof Error ? err.message : 'Failed to verify channel'
|
|
197
|
-
);
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
},
|
|
201
|
-
[setChannels, setError]
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
const updatePreferences = useCallback(
|
|
205
|
-
async (newPreferences: Partial<NotificationPreferences>): Promise<boolean> => {
|
|
206
|
-
try {
|
|
207
|
-
const success = await preferencesManager.update(newPreferences);
|
|
208
|
-
if (success) {
|
|
209
|
-
setPreferences((prev: NotificationPreferences | null) =>
|
|
210
|
-
prev ? { ...prev, ...newPreferences } : null
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
return success;
|
|
214
|
-
} catch (err) {
|
|
215
|
-
setError(
|
|
216
|
-
err instanceof Error ? err.message : 'Failed to update preferences'
|
|
217
|
-
);
|
|
218
|
-
return false;
|
|
219
|
-
}
|
|
220
|
-
},
|
|
221
|
-
[setPreferences, setError]
|
|
222
|
-
);
|
|
223
|
-
|
|
224
126
|
return {
|
|
225
127
|
sendNotification,
|
|
226
128
|
markAsRead,
|
|
227
129
|
markAllAsRead,
|
|
228
|
-
deleteNotification,
|
|
229
|
-
registerChannel,
|
|
230
|
-
verifyChannel,
|
|
231
|
-
updatePreferences,
|
|
232
130
|
};
|
|
233
|
-
};
|
|
131
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
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
|
+
};
|