@umituz/react-native-notifications 1.0.5 → 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.
Files changed (72) hide show
  1. package/lib/index.d.ts +1 -0
  2. package/lib/index.d.ts.map +1 -1
  3. package/lib/index.js +5 -0
  4. package/lib/index.js.map +1 -1
  5. package/lib/infrastructure/hooks/actions/useNotificationActions.d.ts +4 -13
  6. package/lib/infrastructure/hooks/actions/useNotificationActions.d.ts.map +1 -1
  7. package/lib/infrastructure/hooks/actions/useNotificationActions.js +4 -70
  8. package/lib/infrastructure/hooks/actions/useNotificationActions.js.map +1 -1
  9. package/lib/infrastructure/hooks/actions/useNotificationManagementActions.d.ts +8 -0
  10. package/lib/infrastructure/hooks/actions/useNotificationManagementActions.d.ts.map +1 -0
  11. package/lib/infrastructure/hooks/actions/useNotificationManagementActions.js +78 -0
  12. package/lib/infrastructure/hooks/actions/useNotificationManagementActions.js.map +1 -0
  13. package/lib/infrastructure/hooks/state/useNotificationsState.d.ts +8 -8
  14. package/lib/infrastructure/hooks/useNotificationSettings.d.ts +2 -2
  15. package/lib/infrastructure/hooks/useNotifications.d.ts +21 -1
  16. package/lib/infrastructure/hooks/useNotifications.d.ts.map +1 -1
  17. package/lib/infrastructure/hooks/useNotifications.js +30 -9
  18. package/lib/infrastructure/hooks/useNotifications.js.map +1 -1
  19. package/lib/infrastructure/hooks/utils/useNotificationRefresh.d.ts +5 -10
  20. package/lib/infrastructure/hooks/utils/useNotificationRefresh.d.ts.map +1 -1
  21. package/lib/infrastructure/hooks/utils/useNotificationRefresh.js +32 -15
  22. package/lib/infrastructure/hooks/utils/useNotificationRefresh.js.map +1 -1
  23. package/lib/infrastructure/services/NotificationBadgeManager.d.ts +5 -0
  24. package/lib/infrastructure/services/NotificationBadgeManager.d.ts.map +1 -0
  25. package/lib/infrastructure/services/NotificationBadgeManager.js +29 -0
  26. package/lib/infrastructure/services/NotificationBadgeManager.js.map +1 -0
  27. package/lib/infrastructure/services/NotificationManager.d.ts +5 -84
  28. package/lib/infrastructure/services/NotificationManager.d.ts.map +1 -1
  29. package/lib/infrastructure/services/NotificationManager.js +36 -203
  30. package/lib/infrastructure/services/NotificationManager.js.map +1 -1
  31. package/lib/infrastructure/services/NotificationPermissions.d.ts +6 -0
  32. package/lib/infrastructure/services/NotificationPermissions.d.ts.map +1 -0
  33. package/lib/infrastructure/services/NotificationPermissions.js +75 -0
  34. package/lib/infrastructure/services/NotificationPermissions.js.map +1 -0
  35. package/lib/infrastructure/services/NotificationScheduler.d.ts +8 -0
  36. package/lib/infrastructure/services/NotificationScheduler.d.ts.map +1 -0
  37. package/lib/infrastructure/services/NotificationScheduler.js +72 -0
  38. package/lib/infrastructure/services/NotificationScheduler.js.map +1 -0
  39. package/lib/infrastructure/services/delivery/NotificationDelivery.d.ts +2 -8
  40. package/lib/infrastructure/services/delivery/NotificationDelivery.d.ts.map +1 -1
  41. package/lib/infrastructure/services/delivery/NotificationDelivery.js +27 -13
  42. package/lib/infrastructure/services/delivery/NotificationDelivery.js.map +1 -1
  43. package/lib/infrastructure/storage/NotificationsStore.d.ts +8 -1
  44. package/lib/infrastructure/storage/NotificationsStore.d.ts.map +1 -1
  45. package/lib/infrastructure/storage/NotificationsStore.js +2 -1
  46. package/lib/infrastructure/storage/NotificationsStore.js.map +1 -1
  47. package/lib/infrastructure/utils/dev.d.ts +5 -0
  48. package/lib/infrastructure/utils/dev.d.ts.map +1 -0
  49. package/lib/infrastructure/utils/dev.js +24 -0
  50. package/lib/infrastructure/utils/dev.js.map +1 -0
  51. package/lib/presentation/screens/NotificationsScreen.d.ts +14 -4
  52. package/lib/presentation/screens/NotificationsScreen.d.ts.map +1 -1
  53. package/lib/presentation/screens/NotificationsScreen.js +12 -15
  54. package/lib/presentation/screens/NotificationsScreen.js.map +1 -1
  55. package/package.json +2 -2
  56. package/src/__tests__/NotificationManager.test.ts +215 -0
  57. package/src/__tests__/useNotificationActions.test.ts +189 -0
  58. package/src/__tests__/useNotificationRefresh.test.ts +213 -0
  59. package/src/index.ts +7 -0
  60. package/src/infrastructure/hooks/actions/useNotificationActions.ts +8 -110
  61. package/src/infrastructure/hooks/actions/useNotificationManagementActions.ts +131 -0
  62. package/src/infrastructure/hooks/useNotifications.ts +37 -11
  63. package/src/infrastructure/hooks/utils/useNotificationRefresh.ts +40 -16
  64. package/src/infrastructure/services/NotificationBadgeManager.ts +28 -0
  65. package/src/infrastructure/services/NotificationManager.ts +51 -217
  66. package/src/infrastructure/services/NotificationPermissions.ts +80 -0
  67. package/src/infrastructure/services/NotificationScheduler.ts +77 -0
  68. package/src/infrastructure/services/delivery/NotificationDelivery.ts +32 -14
  69. package/src/infrastructure/storage/NotificationsStore.ts +3 -2
  70. package/src/infrastructure/utils/dev.ts +25 -0
  71. package/src/presentation/screens/NotificationsScreen.tsx +31 -18
  72. package/src/types/global.d.ts +255 -0
@@ -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
+ };
@@ -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
- setNotifications([]);
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 () => clearInterval(interval);
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
- // Silent failure
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
- // Silent failure
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
+ }