@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.
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +5 -0
- package/lib/index.js.map +1 -1
- 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 +2 -2
- 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/index.ts +7 -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,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
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as Notifications from 'expo-notifications';
|
|
2
|
+
import * as Device from 'expo-device';
|
|
3
|
+
import { Platform } from 'react-native';
|
|
4
|
+
import { devWarn, devError, devLog } from '../utils/dev';
|
|
5
|
+
|
|
6
|
+
export class NotificationPermissions {
|
|
7
|
+
async requestPermissions(): Promise<boolean> {
|
|
8
|
+
try {
|
|
9
|
+
if (!Device.isDevice) {
|
|
10
|
+
devWarn('[NotificationPermissions] Notifications only work on physical devices');
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const permissionsResponse = await Notifications.getPermissionsAsync();
|
|
15
|
+
const existingStatus = (permissionsResponse as any).status || ((permissionsResponse as any).granted ? 'granted' : 'denied');
|
|
16
|
+
let finalStatus = existingStatus;
|
|
17
|
+
|
|
18
|
+
if (existingStatus !== 'granted') {
|
|
19
|
+
const requestResponse = await Notifications.requestPermissionsAsync();
|
|
20
|
+
finalStatus = (requestResponse as any).status || ((requestResponse as any).granted ? 'granted' : 'denied');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (Platform.OS === 'android') {
|
|
24
|
+
await this.createAndroidChannels();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return finalStatus === 'granted';
|
|
28
|
+
} catch (error) {
|
|
29
|
+
devError('[NotificationPermissions] Permission request failed:', error);
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async hasPermissions(): Promise<boolean> {
|
|
35
|
+
try {
|
|
36
|
+
if (!Device.isDevice) return false;
|
|
37
|
+
const permissionsResponse = await Notifications.getPermissionsAsync();
|
|
38
|
+
return (permissionsResponse as any).status === 'granted' || (permissionsResponse as any).granted === true;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
devError('[NotificationPermissions] Permission check failed:', error);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private async createAndroidChannels(): Promise<void> {
|
|
46
|
+
if (Platform.OS !== 'android') return;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
await Notifications.setNotificationChannelAsync('default', {
|
|
50
|
+
name: 'Default',
|
|
51
|
+
importance: Notifications.AndroidImportance.DEFAULT,
|
|
52
|
+
vibrationPattern: [0, 250, 250, 250],
|
|
53
|
+
sound: 'default',
|
|
54
|
+
lightColor: '#3B82F6',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await Notifications.setNotificationChannelAsync('reminders', {
|
|
58
|
+
name: 'Reminders',
|
|
59
|
+
importance: Notifications.AndroidImportance.HIGH,
|
|
60
|
+
vibrationPattern: [0, 250, 250, 250],
|
|
61
|
+
sound: 'default',
|
|
62
|
+
lightColor: '#3B82F6',
|
|
63
|
+
enableVibrate: true,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await Notifications.setNotificationChannelAsync('urgent', {
|
|
67
|
+
name: 'Urgent',
|
|
68
|
+
importance: Notifications.AndroidImportance.MAX,
|
|
69
|
+
vibrationPattern: [0, 500, 250, 500],
|
|
70
|
+
sound: 'default',
|
|
71
|
+
lightColor: '#EF4444',
|
|
72
|
+
enableVibrate: true,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
devLog('[NotificationPermissions] Android channels created');
|
|
76
|
+
} catch (error) {
|
|
77
|
+
devError('[NotificationPermissions] Android channel creation failed:', error);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as Notifications from 'expo-notifications';
|
|
2
|
+
import type { NotificationTrigger, ScheduleNotificationOptions, ScheduledNotification } from './NotificationManager';
|
|
3
|
+
|
|
4
|
+
export class NotificationScheduler {
|
|
5
|
+
async scheduleNotification(options: ScheduleNotificationOptions): Promise<string> {
|
|
6
|
+
const { title, body, data = {}, trigger, sound = true, badge, categoryIdentifier } = options;
|
|
7
|
+
|
|
8
|
+
let notificationTrigger: any;
|
|
9
|
+
|
|
10
|
+
if (trigger.type === 'date') {
|
|
11
|
+
notificationTrigger = {
|
|
12
|
+
date: trigger.date,
|
|
13
|
+
channelId: categoryIdentifier || 'default',
|
|
14
|
+
};
|
|
15
|
+
} else if (trigger.type === 'daily') {
|
|
16
|
+
notificationTrigger = {
|
|
17
|
+
hour: trigger.hour,
|
|
18
|
+
minute: trigger.minute,
|
|
19
|
+
repeats: true,
|
|
20
|
+
channelId: categoryIdentifier || 'reminders',
|
|
21
|
+
};
|
|
22
|
+
} else if (trigger.type === 'weekly') {
|
|
23
|
+
notificationTrigger = {
|
|
24
|
+
weekday: trigger.weekday,
|
|
25
|
+
hour: trigger.hour,
|
|
26
|
+
minute: trigger.minute,
|
|
27
|
+
repeats: true,
|
|
28
|
+
channelId: categoryIdentifier || 'reminders',
|
|
29
|
+
};
|
|
30
|
+
} else if (trigger.type === 'monthly') {
|
|
31
|
+
notificationTrigger = {
|
|
32
|
+
day: trigger.day,
|
|
33
|
+
hour: trigger.hour,
|
|
34
|
+
minute: trigger.minute,
|
|
35
|
+
repeats: true,
|
|
36
|
+
channelId: categoryIdentifier || 'reminders',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const notificationId = await Notifications.scheduleNotificationAsync({
|
|
41
|
+
content: {
|
|
42
|
+
title,
|
|
43
|
+
body,
|
|
44
|
+
data,
|
|
45
|
+
sound: sound === true ? 'default' : sound || undefined,
|
|
46
|
+
badge,
|
|
47
|
+
categoryIdentifier,
|
|
48
|
+
priority: Notifications.AndroidNotificationPriority.HIGH,
|
|
49
|
+
vibrate: [0, 250, 250, 250],
|
|
50
|
+
},
|
|
51
|
+
trigger: notificationTrigger,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return notificationId;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async cancelNotification(notificationId: string): Promise<void> {
|
|
58
|
+
await Notifications.cancelScheduledNotificationAsync(notificationId);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async cancelAllNotifications(): Promise<void> {
|
|
62
|
+
await Notifications.cancelAllScheduledNotificationsAsync();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async getScheduledNotifications(): Promise<ScheduledNotification[]> {
|
|
66
|
+
const notifications = await Notifications.getAllScheduledNotificationsAsync();
|
|
67
|
+
return notifications.map(notification => ({
|
|
68
|
+
identifier: notification.identifier,
|
|
69
|
+
content: {
|
|
70
|
+
title: notification.content.title || '',
|
|
71
|
+
body: notification.content.body || '',
|
|
72
|
+
data: notification.content.data as Record<string, any>,
|
|
73
|
+
},
|
|
74
|
+
trigger: notification.trigger,
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
import * as Notifications from 'expo-notifications';
|
|
2
2
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
3
3
|
import type { Notification } from '../types';
|
|
4
|
+
import { devLog, devError } from '../../utils/dev';
|
|
4
5
|
|
|
5
|
-
/**
|
|
6
|
-
* NotificationDelivery - Offline Local Notification Delivery
|
|
7
|
-
*
|
|
8
|
-
* Uses expo-notifications for local push notifications only.
|
|
9
|
-
* All data stored in AsyncStorage (offline-capable).
|
|
10
|
-
*
|
|
11
|
-
* NO backend, NO email/SMS - pure offline local notifications.
|
|
12
|
-
*/
|
|
13
6
|
export class NotificationDelivery {
|
|
14
7
|
private static STORAGE_KEY = '@notifications:delivered';
|
|
15
8
|
|
|
16
9
|
async deliver(notification: Notification): Promise<void> {
|
|
17
10
|
try {
|
|
18
|
-
// Schedule local notification using expo-notifications
|
|
19
11
|
await Notifications.scheduleNotificationAsync({
|
|
20
12
|
content: {
|
|
21
13
|
title: notification.title,
|
|
@@ -24,14 +16,17 @@ export class NotificationDelivery {
|
|
|
24
16
|
},
|
|
25
17
|
trigger: notification.scheduled_for
|
|
26
18
|
? { date: new Date(notification.scheduled_for) }
|
|
27
|
-
: null,
|
|
19
|
+
: null,
|
|
28
20
|
});
|
|
29
21
|
|
|
30
|
-
// Update status in AsyncStorage (offline storage)
|
|
31
22
|
await this.updateStatus(notification.id, 'delivered');
|
|
23
|
+
|
|
24
|
+
devLog('[NotificationDelivery] Notification delivered:', notification.id);
|
|
32
25
|
} catch (error) {
|
|
33
|
-
// Silent failure - update status to failed
|
|
34
26
|
await this.updateStatus(notification.id, 'failed');
|
|
27
|
+
|
|
28
|
+
devError('[NotificationDelivery] Delivery failed:', notification.id, error);
|
|
29
|
+
throw error;
|
|
35
30
|
}
|
|
36
31
|
}
|
|
37
32
|
|
|
@@ -49,8 +44,10 @@ export class NotificationDelivery {
|
|
|
49
44
|
NotificationDelivery.STORAGE_KEY,
|
|
50
45
|
JSON.stringify(delivered)
|
|
51
46
|
);
|
|
47
|
+
|
|
48
|
+
devLog('[NotificationDelivery] Status updated:', notificationId, status);
|
|
52
49
|
} catch (error) {
|
|
53
|
-
|
|
50
|
+
devError('[NotificationDelivery] Status update failed:', notificationId, error);
|
|
54
51
|
}
|
|
55
52
|
}
|
|
56
53
|
|
|
@@ -59,7 +56,28 @@ export class NotificationDelivery {
|
|
|
59
56
|
const data = await AsyncStorage.getItem(NotificationDelivery.STORAGE_KEY);
|
|
60
57
|
return data ? JSON.parse(data) : {};
|
|
61
58
|
} catch (error) {
|
|
59
|
+
devError('[NotificationDelivery] Failed to get delivered notifications:', error);
|
|
62
60
|
return {};
|
|
63
61
|
}
|
|
64
62
|
}
|
|
65
|
-
|
|
63
|
+
|
|
64
|
+
async getDeliveryStatus(notificationId: string): Promise<string | null> {
|
|
65
|
+
try {
|
|
66
|
+
const delivered = await this.getDelivered();
|
|
67
|
+
return delivered[notificationId]?.status || null;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
devError('[NotificationDelivery] Failed to get delivery status:', notificationId, error);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async clearDeliveryHistory(): Promise<void> {
|
|
75
|
+
try {
|
|
76
|
+
await AsyncStorage.removeItem(NotificationDelivery.STORAGE_KEY);
|
|
77
|
+
|
|
78
|
+
devLog('[NotificationDelivery] Delivery history cleared');
|
|
79
|
+
} catch (error) {
|
|
80
|
+
devError('[NotificationDelivery] Failed to clear delivery history:', error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -28,7 +28,8 @@ export const useNotificationsStore = create<NotificationsStore>((set) => ({
|
|
|
28
28
|
* Hook for accessing notifications state
|
|
29
29
|
*/
|
|
30
30
|
export const useNotifications = () => {
|
|
31
|
-
const
|
|
31
|
+
const store = useNotificationsStore();
|
|
32
|
+
const { hasPermissions, isInitialized, setPermissions, setInitialized } = store;
|
|
32
33
|
|
|
33
34
|
return {
|
|
34
35
|
hasPermissions,
|
|
@@ -36,4 +37,4 @@ export const useNotifications = () => {
|
|
|
36
37
|
setPermissions,
|
|
37
38
|
setInitialized,
|
|
38
39
|
};
|
|
39
|
-
};
|
|
40
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const isDev = () => {
|
|
2
|
+
try {
|
|
3
|
+
return typeof __DEV__ !== 'undefined' && __DEV__;
|
|
4
|
+
} catch {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const devLog = (message: string, ...args: any[]) => {
|
|
10
|
+
if (isDev()) {
|
|
11
|
+
console.log(message, ...args);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const devError = (message: string, ...args: any[]) => {
|
|
16
|
+
if (isDev()) {
|
|
17
|
+
console.error(message, ...args);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const devWarn = (message: string, ...args: any[]) => {
|
|
22
|
+
if (isDev()) {
|
|
23
|
+
console.warn(message, ...args);
|
|
24
|
+
}
|
|
25
|
+
};
|