@umituz/react-native-notifications 1.0.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/LICENSE +22 -0
- package/README.md +93 -0
- package/lib/index.d.ts +13 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +15 -0
- package/lib/index.js.map +1 -0
- package/lib/infrastructure/config/notificationsConfig.d.ts +20 -0
- package/lib/infrastructure/config/notificationsConfig.d.ts.map +1 -0
- package/lib/infrastructure/config/notificationsConfig.js +81 -0
- package/lib/infrastructure/config/notificationsConfig.js.map +1 -0
- package/lib/infrastructure/hooks/actions/useNotificationActions.d.ts +17 -0
- package/lib/infrastructure/hooks/actions/useNotificationActions.d.ts.map +1 -0
- package/lib/infrastructure/hooks/actions/useNotificationActions.js +141 -0
- package/lib/infrastructure/hooks/actions/useNotificationActions.js.map +1 -0
- package/lib/infrastructure/hooks/state/useNotificationsState.d.ts +12 -0
- package/lib/infrastructure/hooks/state/useNotificationsState.d.ts.map +1 -0
- package/lib/infrastructure/hooks/state/useNotificationsState.js +30 -0
- package/lib/infrastructure/hooks/state/useNotificationsState.js.map +1 -0
- package/lib/infrastructure/hooks/types.d.ts +87 -0
- package/lib/infrastructure/hooks/types.d.ts.map +1 -0
- package/lib/infrastructure/hooks/types.js +8 -0
- package/lib/infrastructure/hooks/types.js.map +1 -0
- package/lib/infrastructure/hooks/useNotificationSettings.d.ts +10 -0
- package/lib/infrastructure/hooks/useNotificationSettings.d.ts.map +1 -0
- package/lib/infrastructure/hooks/useNotificationSettings.js +43 -0
- package/lib/infrastructure/hooks/useNotificationSettings.js.map +1 -0
- package/lib/infrastructure/hooks/useNotifications.d.ts +23 -0
- package/lib/infrastructure/hooks/useNotifications.d.ts.map +1 -0
- package/lib/infrastructure/hooks/useNotifications.js +51 -0
- package/lib/infrastructure/hooks/useNotifications.js.map +1 -0
- package/lib/infrastructure/hooks/utils/useNotificationRefresh.d.ts +13 -0
- package/lib/infrastructure/hooks/utils/useNotificationRefresh.d.ts.map +1 -0
- package/lib/infrastructure/hooks/utils/useNotificationRefresh.js +82 -0
- package/lib/infrastructure/hooks/utils/useNotificationRefresh.js.map +1 -0
- package/lib/infrastructure/services/NotificationManager.d.ts +138 -0
- package/lib/infrastructure/services/NotificationManager.d.ts.map +1 -0
- package/lib/infrastructure/services/NotificationManager.js +284 -0
- package/lib/infrastructure/services/NotificationManager.js.map +1 -0
- package/lib/infrastructure/services/NotificationService.d.ts +30 -0
- package/lib/infrastructure/services/NotificationService.d.ts.map +1 -0
- package/lib/infrastructure/services/NotificationService.js +41 -0
- package/lib/infrastructure/services/NotificationService.js.map +1 -0
- package/lib/infrastructure/services/channels/ChannelManager.d.ts +18 -0
- package/lib/infrastructure/services/channels/ChannelManager.d.ts.map +1 -0
- package/lib/infrastructure/services/channels/ChannelManager.js +87 -0
- package/lib/infrastructure/services/channels/ChannelManager.js.map +1 -0
- package/lib/infrastructure/services/delivery/NotificationDelivery.d.ts +16 -0
- package/lib/infrastructure/services/delivery/NotificationDelivery.d.ts.map +1 -0
- package/lib/infrastructure/services/delivery/NotificationDelivery.js +57 -0
- package/lib/infrastructure/services/delivery/NotificationDelivery.js.map +1 -0
- package/lib/infrastructure/services/preferences/PreferencesManager.d.ts +18 -0
- package/lib/infrastructure/services/preferences/PreferencesManager.d.ts.map +1 -0
- package/lib/infrastructure/services/preferences/PreferencesManager.js +65 -0
- package/lib/infrastructure/services/preferences/PreferencesManager.js.map +1 -0
- package/lib/infrastructure/services/types.d.ts +89 -0
- package/lib/infrastructure/services/types.d.ts.map +1 -0
- package/lib/infrastructure/services/types.js +7 -0
- package/lib/infrastructure/services/types.js.map +1 -0
- package/lib/infrastructure/storage/NotificationsStore.d.ts +23 -0
- package/lib/infrastructure/storage/NotificationsStore.d.ts.map +1 -0
- package/lib/infrastructure/storage/NotificationsStore.js +25 -0
- package/lib/infrastructure/storage/NotificationsStore.js.map +1 -0
- package/package.json +62 -0
- package/src/index.ts +34 -0
- package/src/infrastructure/config/notificationsConfig.ts +98 -0
- package/src/infrastructure/hooks/actions/useNotificationActions.ts +233 -0
- package/src/infrastructure/hooks/state/useNotificationsState.ts +46 -0
- package/src/infrastructure/hooks/types.ts +83 -0
- package/src/infrastructure/hooks/useNotificationSettings.ts +45 -0
- package/src/infrastructure/hooks/useNotifications.ts +70 -0
- package/src/infrastructure/hooks/utils/useNotificationRefresh.ts +107 -0
- package/src/infrastructure/services/NotificationManager.ts +326 -0
- package/src/infrastructure/services/NotificationService.ts +50 -0
- package/src/infrastructure/services/channels/ChannelManager.ts +111 -0
- package/src/infrastructure/services/delivery/NotificationDelivery.ts +65 -0
- package/src/infrastructure/services/preferences/PreferencesManager.ts +77 -0
- package/src/infrastructure/services/types.ts +81 -0
- package/src/infrastructure/storage/NotificationsStore.ts +39 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NotificationManager
|
|
3
|
+
*
|
|
4
|
+
* Offline-first notification system using expo-notifications.
|
|
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
|
+
*/
|
|
23
|
+
|
|
24
|
+
import * as Notifications from 'expo-notifications';
|
|
25
|
+
import * as Device from 'expo-device';
|
|
26
|
+
import { Platform } from 'react-native';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Trigger types for notifications
|
|
30
|
+
*/
|
|
31
|
+
export type NotificationTrigger =
|
|
32
|
+
| { type: 'date'; date: Date }
|
|
33
|
+
| { type: 'daily'; hour: number; minute: number }
|
|
34
|
+
| { type: 'weekly'; weekday: number; hour: number; minute: number }
|
|
35
|
+
| { type: 'monthly'; day: number; hour: number; minute: number };
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Options for scheduling a notification
|
|
39
|
+
*/
|
|
40
|
+
export interface ScheduleNotificationOptions {
|
|
41
|
+
title: string;
|
|
42
|
+
body: string;
|
|
43
|
+
data?: Record<string, any>;
|
|
44
|
+
trigger: NotificationTrigger;
|
|
45
|
+
sound?: boolean | string;
|
|
46
|
+
badge?: number;
|
|
47
|
+
categoryIdentifier?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Scheduled notification details
|
|
52
|
+
*/
|
|
53
|
+
export interface ScheduledNotification {
|
|
54
|
+
identifier: string;
|
|
55
|
+
content: {
|
|
56
|
+
title: string;
|
|
57
|
+
body: string;
|
|
58
|
+
data: Record<string, any>;
|
|
59
|
+
};
|
|
60
|
+
trigger: any;
|
|
61
|
+
}
|
|
62
|
+
|
|
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
|
+
export class NotificationManager {
|
|
70
|
+
/**
|
|
71
|
+
* Configure notification handler (how notifications appear when app is in foreground)
|
|
72
|
+
*/
|
|
73
|
+
static configure() {
|
|
74
|
+
Notifications.setNotificationHandler({
|
|
75
|
+
handleNotification: async () => ({
|
|
76
|
+
shouldShowAlert: true,
|
|
77
|
+
shouldPlaySound: true,
|
|
78
|
+
shouldSetBadge: true,
|
|
79
|
+
}),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Request notification permissions
|
|
85
|
+
* iOS: Shows system permission dialog
|
|
86
|
+
* Android: Permissions granted by default (Android 13+ requires runtime permission)
|
|
87
|
+
*/
|
|
88
|
+
async requestPermissions(): Promise<boolean> {
|
|
89
|
+
try {
|
|
90
|
+
if (!Device.isDevice) {
|
|
91
|
+
console.warn('[NotificationManager] Notifications only work on physical devices');
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const { status: existingStatus } = await Notifications.getPermissionsAsync();
|
|
96
|
+
let finalStatus = existingStatus;
|
|
97
|
+
|
|
98
|
+
if (existingStatus !== 'granted') {
|
|
99
|
+
const { status } = await Notifications.requestPermissionsAsync();
|
|
100
|
+
finalStatus = status;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (Platform.OS === 'android') {
|
|
104
|
+
await this.createAndroidChannels();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return finalStatus === 'granted';
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error('[NotificationManager] Permission request failed:', error);
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check if notification permissions are granted
|
|
116
|
+
*/
|
|
117
|
+
async hasPermissions(): Promise<boolean> {
|
|
118
|
+
try {
|
|
119
|
+
if (!Device.isDevice) return false;
|
|
120
|
+
const { status } = await Notifications.getPermissionsAsync();
|
|
121
|
+
return status === 'granted';
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error('[NotificationManager] Permission check failed:', error);
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Create Android notification channels (required for Android 8+)
|
|
130
|
+
*/
|
|
131
|
+
private async createAndroidChannels(): Promise<void> {
|
|
132
|
+
if (Platform.OS !== 'android') return;
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
await Notifications.setNotificationChannelAsync('default', {
|
|
136
|
+
name: 'Default',
|
|
137
|
+
importance: Notifications.AndroidImportance.DEFAULT,
|
|
138
|
+
vibrationPattern: [0, 250, 250, 250],
|
|
139
|
+
sound: 'default',
|
|
140
|
+
lightColor: '#3B82F6',
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await Notifications.setNotificationChannelAsync('reminders', {
|
|
144
|
+
name: 'Reminders',
|
|
145
|
+
importance: Notifications.AndroidImportance.HIGH,
|
|
146
|
+
vibrationPattern: [0, 250, 250, 250],
|
|
147
|
+
sound: 'default',
|
|
148
|
+
lightColor: '#3B82F6',
|
|
149
|
+
enableVibrate: true,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await Notifications.setNotificationChannelAsync('urgent', {
|
|
153
|
+
name: 'Urgent',
|
|
154
|
+
importance: Notifications.AndroidImportance.MAX,
|
|
155
|
+
vibrationPattern: [0, 500, 250, 500],
|
|
156
|
+
sound: 'default',
|
|
157
|
+
lightColor: '#EF4444',
|
|
158
|
+
enableVibrate: true,
|
|
159
|
+
});
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.error('[NotificationManager] Android channel creation failed:', error);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Schedule a notification
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* // Specific date
|
|
170
|
+
* const id = await manager.scheduleNotification({
|
|
171
|
+
* title: 'Bill Reminder',
|
|
172
|
+
* body: 'Electricity bill due today',
|
|
173
|
+
* trigger: { type: 'date', date: new Date('2025-01-15T09:00:00') }
|
|
174
|
+
* });
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* // Daily reminder
|
|
178
|
+
* const id = await manager.scheduleNotification({
|
|
179
|
+
* title: 'Daily Workout',
|
|
180
|
+
* body: 'Time for your morning workout!',
|
|
181
|
+
* trigger: { type: 'daily', hour: 7, minute: 0 }
|
|
182
|
+
* });
|
|
183
|
+
*/
|
|
184
|
+
async scheduleNotification(options: ScheduleNotificationOptions): Promise<string> {
|
|
185
|
+
try {
|
|
186
|
+
const { title, body, data = {}, trigger, sound = true, badge, categoryIdentifier } = options;
|
|
187
|
+
|
|
188
|
+
let notificationTrigger: any;
|
|
189
|
+
|
|
190
|
+
if (trigger.type === 'date') {
|
|
191
|
+
notificationTrigger = {
|
|
192
|
+
date: trigger.date,
|
|
193
|
+
channelId: categoryIdentifier || 'default',
|
|
194
|
+
};
|
|
195
|
+
} else if (trigger.type === 'daily') {
|
|
196
|
+
notificationTrigger = {
|
|
197
|
+
hour: trigger.hour,
|
|
198
|
+
minute: trigger.minute,
|
|
199
|
+
repeats: true,
|
|
200
|
+
channelId: categoryIdentifier || 'reminders',
|
|
201
|
+
};
|
|
202
|
+
} else if (trigger.type === 'weekly') {
|
|
203
|
+
notificationTrigger = {
|
|
204
|
+
weekday: trigger.weekday,
|
|
205
|
+
hour: trigger.hour,
|
|
206
|
+
minute: trigger.minute,
|
|
207
|
+
repeats: true,
|
|
208
|
+
channelId: categoryIdentifier || 'reminders',
|
|
209
|
+
};
|
|
210
|
+
} else if (trigger.type === 'monthly') {
|
|
211
|
+
notificationTrigger = {
|
|
212
|
+
day: trigger.day,
|
|
213
|
+
hour: trigger.hour,
|
|
214
|
+
minute: trigger.minute,
|
|
215
|
+
repeats: true,
|
|
216
|
+
channelId: categoryIdentifier || 'reminders',
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const notificationId = await Notifications.scheduleNotificationAsync({
|
|
221
|
+
content: {
|
|
222
|
+
title,
|
|
223
|
+
body,
|
|
224
|
+
data,
|
|
225
|
+
sound: sound === true ? 'default' : sound || undefined,
|
|
226
|
+
badge,
|
|
227
|
+
categoryIdentifier,
|
|
228
|
+
priority: Notifications.AndroidNotificationPriority.HIGH,
|
|
229
|
+
vibrate: [0, 250, 250, 250],
|
|
230
|
+
},
|
|
231
|
+
trigger: notificationTrigger,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
return notificationId;
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.error('[NotificationManager] Schedule notification failed:', error);
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Cancel a scheduled notification
|
|
243
|
+
*/
|
|
244
|
+
async cancelNotification(notificationId: string): Promise<void> {
|
|
245
|
+
try {
|
|
246
|
+
await Notifications.cancelScheduledNotificationAsync(notificationId);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
console.error('[NotificationManager] Cancel notification failed:', error);
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Cancel all scheduled notifications
|
|
255
|
+
*/
|
|
256
|
+
async cancelAllNotifications(): Promise<void> {
|
|
257
|
+
try {
|
|
258
|
+
await Notifications.cancelAllScheduledNotificationsAsync();
|
|
259
|
+
} catch (error) {
|
|
260
|
+
console.error('[NotificationManager] Cancel all notifications failed:', error);
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Get all scheduled notifications
|
|
267
|
+
*/
|
|
268
|
+
async getScheduledNotifications(): Promise<ScheduledNotification[]> {
|
|
269
|
+
try {
|
|
270
|
+
const notifications = await Notifications.getAllScheduledNotificationsAsync();
|
|
271
|
+
return notifications.map(notification => ({
|
|
272
|
+
identifier: notification.identifier,
|
|
273
|
+
content: {
|
|
274
|
+
title: notification.content.title || '',
|
|
275
|
+
body: notification.content.body || '',
|
|
276
|
+
data: notification.content.data as Record<string, any>,
|
|
277
|
+
},
|
|
278
|
+
trigger: notification.trigger,
|
|
279
|
+
}));
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.error('[NotificationManager] Get scheduled notifications failed:', error);
|
|
282
|
+
return [];
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Dismiss all delivered notifications (clear from notification center)
|
|
288
|
+
*/
|
|
289
|
+
async dismissAllNotifications(): Promise<void> {
|
|
290
|
+
try {
|
|
291
|
+
await Notifications.dismissAllNotificationsAsync();
|
|
292
|
+
} catch (error) {
|
|
293
|
+
console.error('[NotificationManager] Dismiss all notifications failed:', error);
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get notification badge count (iOS only)
|
|
300
|
+
*/
|
|
301
|
+
async getBadgeCount(): Promise<number> {
|
|
302
|
+
try {
|
|
303
|
+
if (Platform.OS === 'ios') {
|
|
304
|
+
return await Notifications.getBadgeCountAsync();
|
|
305
|
+
}
|
|
306
|
+
return 0;
|
|
307
|
+
} catch (error) {
|
|
308
|
+
console.error('[NotificationManager] Get badge count failed:', error);
|
|
309
|
+
return 0;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Set notification badge count (iOS only)
|
|
315
|
+
*/
|
|
316
|
+
async setBadgeCount(count: number): Promise<void> {
|
|
317
|
+
try {
|
|
318
|
+
if (Platform.OS === 'ios') {
|
|
319
|
+
await Notifications.setBadgeCountAsync(count);
|
|
320
|
+
}
|
|
321
|
+
} catch (error) {
|
|
322
|
+
console.error('[NotificationManager] Set badge count failed:', error);
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NotificationService
|
|
3
|
+
*
|
|
4
|
+
* Simple facade for offline notification system.
|
|
5
|
+
* Works in ALL apps - offline, online, hybrid - no backend required.
|
|
6
|
+
*
|
|
7
|
+
* @module NotificationService
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { NotificationManager } from './NotificationManager';
|
|
11
|
+
|
|
12
|
+
export * from './types';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Notification service singleton
|
|
16
|
+
* Provides simple access to notification manager
|
|
17
|
+
*/
|
|
18
|
+
export class NotificationService {
|
|
19
|
+
private static instance: NotificationService;
|
|
20
|
+
|
|
21
|
+
readonly notifications = new NotificationManager();
|
|
22
|
+
|
|
23
|
+
private constructor() {
|
|
24
|
+
// Configure notification handler on initialization
|
|
25
|
+
NotificationManager.configure();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static getInstance(): NotificationService {
|
|
29
|
+
if (!NotificationService.instance) {
|
|
30
|
+
NotificationService.instance = new NotificationService();
|
|
31
|
+
}
|
|
32
|
+
return NotificationService.instance;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Request notification permissions
|
|
37
|
+
*/
|
|
38
|
+
async requestPermissions(): Promise<boolean> {
|
|
39
|
+
return await this.notifications.requestPermissions();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if permissions are granted
|
|
44
|
+
*/
|
|
45
|
+
async hasPermissions(): Promise<boolean> {
|
|
46
|
+
return await this.notifications.hasPermissions();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const notificationService = NotificationService.getInstance();
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
2
|
+
import * as Notifications from 'expo-notifications';
|
|
3
|
+
import type { NotificationChannel } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ChannelManager - Offline Notification Channel Management
|
|
7
|
+
*
|
|
8
|
+
* Manages push notification tokens and preferences in AsyncStorage.
|
|
9
|
+
* Only supports local push and in-app notifications.
|
|
10
|
+
*
|
|
11
|
+
* NO backend, NO email/SMS - pure offline.
|
|
12
|
+
*/
|
|
13
|
+
export class ChannelManager {
|
|
14
|
+
private static STORAGE_KEY = '@notifications:channels';
|
|
15
|
+
|
|
16
|
+
async register(
|
|
17
|
+
channelType: 'push' | 'in_app',
|
|
18
|
+
preferences: Record<string, any> = {}
|
|
19
|
+
): Promise<NotificationChannel | null> {
|
|
20
|
+
try {
|
|
21
|
+
// Get push token for local notifications
|
|
22
|
+
const token =
|
|
23
|
+
channelType === 'push'
|
|
24
|
+
? (await Notifications.getExpoPushTokenAsync()).data
|
|
25
|
+
: 'in_app';
|
|
26
|
+
|
|
27
|
+
const channel: NotificationChannel = {
|
|
28
|
+
id: `${channelType}_${Date.now()}`,
|
|
29
|
+
channel_type: channelType,
|
|
30
|
+
channel_address: token,
|
|
31
|
+
preferences,
|
|
32
|
+
is_verified: true, // Local channels always verified
|
|
33
|
+
is_active: true,
|
|
34
|
+
created_at: new Date().toISOString(),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Store in AsyncStorage
|
|
38
|
+
const channels = await this.getAllChannels();
|
|
39
|
+
channels.push(channel);
|
|
40
|
+
await AsyncStorage.setItem(
|
|
41
|
+
ChannelManager.STORAGE_KEY,
|
|
42
|
+
JSON.stringify(channels)
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return channel;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async verify(channelId: string): Promise<boolean> {
|
|
52
|
+
try {
|
|
53
|
+
const channels = await this.getAllChannels();
|
|
54
|
+
const index = channels.findIndex((c) => c.id === channelId);
|
|
55
|
+
|
|
56
|
+
if (index !== -1) {
|
|
57
|
+
channels[index].is_verified = true;
|
|
58
|
+
await AsyncStorage.setItem(
|
|
59
|
+
ChannelManager.STORAGE_KEY,
|
|
60
|
+
JSON.stringify(channels)
|
|
61
|
+
);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return false;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async getActiveChannels(): Promise<NotificationChannel[]> {
|
|
72
|
+
try {
|
|
73
|
+
const channels = await this.getAllChannels();
|
|
74
|
+
return channels.filter((c) => c.is_active && c.is_verified);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private async getAllChannels(): Promise<NotificationChannel[]> {
|
|
81
|
+
try {
|
|
82
|
+
const data = await AsyncStorage.getItem(ChannelManager.STORAGE_KEY);
|
|
83
|
+
return data ? JSON.parse(data) : [];
|
|
84
|
+
} catch (error) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private async update(
|
|
90
|
+
channelId: string,
|
|
91
|
+
updates: Partial<NotificationChannel>
|
|
92
|
+
): Promise<NotificationChannel | null> {
|
|
93
|
+
try {
|
|
94
|
+
const channels = await this.getAllChannels();
|
|
95
|
+
const index = channels.findIndex((c) => c.id === channelId);
|
|
96
|
+
|
|
97
|
+
if (index !== -1) {
|
|
98
|
+
channels[index] = { ...channels[index], ...updates };
|
|
99
|
+
await AsyncStorage.setItem(
|
|
100
|
+
ChannelManager.STORAGE_KEY,
|
|
101
|
+
JSON.stringify(channels)
|
|
102
|
+
);
|
|
103
|
+
return channels[index];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return null;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import * as Notifications from 'expo-notifications';
|
|
2
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
3
|
+
import type { Notification } from '../types';
|
|
4
|
+
|
|
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
|
+
export class NotificationDelivery {
|
|
14
|
+
private static STORAGE_KEY = '@notifications:delivered';
|
|
15
|
+
|
|
16
|
+
async deliver(notification: Notification): Promise<void> {
|
|
17
|
+
try {
|
|
18
|
+
// Schedule local notification using expo-notifications
|
|
19
|
+
await Notifications.scheduleNotificationAsync({
|
|
20
|
+
content: {
|
|
21
|
+
title: notification.title,
|
|
22
|
+
body: notification.body,
|
|
23
|
+
data: { notificationId: notification.id },
|
|
24
|
+
},
|
|
25
|
+
trigger: notification.scheduled_for
|
|
26
|
+
? { date: new Date(notification.scheduled_for) }
|
|
27
|
+
: null, // null = immediate delivery
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Update status in AsyncStorage (offline storage)
|
|
31
|
+
await this.updateStatus(notification.id, 'delivered');
|
|
32
|
+
} catch (error) {
|
|
33
|
+
// Silent failure - update status to failed
|
|
34
|
+
await this.updateStatus(notification.id, 'failed');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private async updateStatus(
|
|
39
|
+
notificationId: string,
|
|
40
|
+
status: 'delivered' | 'failed'
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
try {
|
|
43
|
+
const delivered = await this.getDelivered();
|
|
44
|
+
delivered[notificationId] = {
|
|
45
|
+
status,
|
|
46
|
+
delivered_at: new Date().toISOString(),
|
|
47
|
+
};
|
|
48
|
+
await AsyncStorage.setItem(
|
|
49
|
+
NotificationDelivery.STORAGE_KEY,
|
|
50
|
+
JSON.stringify(delivered)
|
|
51
|
+
);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
// Silent failure
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private async getDelivered(): Promise<Record<string, any>> {
|
|
58
|
+
try {
|
|
59
|
+
const data = await AsyncStorage.getItem(NotificationDelivery.STORAGE_KEY);
|
|
60
|
+
return data ? JSON.parse(data) : {};
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
2
|
+
import type { NotificationPreferences } from '../types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* PreferencesManager - Offline Notification Preferences
|
|
6
|
+
*
|
|
7
|
+
* Stores preferences in AsyncStorage (offline-capable).
|
|
8
|
+
* Only supports local push and in-app notifications.
|
|
9
|
+
*
|
|
10
|
+
* NO backend, NO email/SMS - pure offline.
|
|
11
|
+
*/
|
|
12
|
+
export class PreferencesManager {
|
|
13
|
+
private static STORAGE_KEY = '@notifications:preferences';
|
|
14
|
+
|
|
15
|
+
async get(): Promise<NotificationPreferences> {
|
|
16
|
+
try {
|
|
17
|
+
const data = await AsyncStorage.getItem(PreferencesManager.STORAGE_KEY);
|
|
18
|
+
return data ? JSON.parse(data) : this.getDefaults();
|
|
19
|
+
} catch (error) {
|
|
20
|
+
return this.getDefaults();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async update(preferences: Partial<NotificationPreferences>): Promise<boolean> {
|
|
25
|
+
try {
|
|
26
|
+
const current = await this.get();
|
|
27
|
+
const updated = {
|
|
28
|
+
...current,
|
|
29
|
+
...preferences,
|
|
30
|
+
categories: { ...current.categories, ...preferences.categories },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
await AsyncStorage.setItem(
|
|
34
|
+
PreferencesManager.STORAGE_KEY,
|
|
35
|
+
JSON.stringify(updated)
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return true;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
isChannelEnabled(
|
|
45
|
+
channelType: 'push' | 'in_app',
|
|
46
|
+
prefs: NotificationPreferences
|
|
47
|
+
): boolean {
|
|
48
|
+
if (channelType === 'in_app') return true;
|
|
49
|
+
return prefs.push_enabled ?? true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
isInQuietHours(quietHours: NotificationPreferences['quiet_hours']): boolean {
|
|
53
|
+
if (!quietHours.enabled) return false;
|
|
54
|
+
const currentTime = new Date().toTimeString().slice(0, 5);
|
|
55
|
+
return (
|
|
56
|
+
currentTime >= quietHours.start_time ||
|
|
57
|
+
currentTime <= quietHours.end_time
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private getDefaults(): NotificationPreferences {
|
|
62
|
+
return {
|
|
63
|
+
push_enabled: true,
|
|
64
|
+
quiet_hours: {
|
|
65
|
+
enabled: false,
|
|
66
|
+
start_time: '22:00',
|
|
67
|
+
end_time: '08:00',
|
|
68
|
+
timezone: 'UTC',
|
|
69
|
+
},
|
|
70
|
+
categories: {
|
|
71
|
+
reminders: { push: true, in_app: true },
|
|
72
|
+
updates: { push: true, in_app: true },
|
|
73
|
+
alerts: { push: true, in_app: true },
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline-First Notification Types
|
|
3
|
+
* Uses expo-notifications for local device notifications
|
|
4
|
+
* NO backend, NO user IDs, NO push notifications
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Notification channel for managing notification delivery
|
|
9
|
+
*/
|
|
10
|
+
export interface NotificationChannel {
|
|
11
|
+
id: string;
|
|
12
|
+
channel_type: 'push' | 'in_app';
|
|
13
|
+
channel_address: string;
|
|
14
|
+
preferences: Record<string, any>;
|
|
15
|
+
is_verified: boolean;
|
|
16
|
+
is_active: boolean;
|
|
17
|
+
created_at: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Notification data structure
|
|
22
|
+
*/
|
|
23
|
+
export interface Notification {
|
|
24
|
+
id: string;
|
|
25
|
+
title: string;
|
|
26
|
+
body: string;
|
|
27
|
+
scheduled_for?: string;
|
|
28
|
+
data?: Record<string, any>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* User notification preferences
|
|
33
|
+
*/
|
|
34
|
+
export interface NotificationPreferences {
|
|
35
|
+
push_enabled: boolean;
|
|
36
|
+
quiet_hours: {
|
|
37
|
+
enabled: boolean;
|
|
38
|
+
start_time: string;
|
|
39
|
+
end_time: string;
|
|
40
|
+
timezone: string;
|
|
41
|
+
};
|
|
42
|
+
categories: Record<string, {
|
|
43
|
+
push: boolean;
|
|
44
|
+
in_app: boolean;
|
|
45
|
+
}>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Trigger types for scheduling notifications
|
|
50
|
+
*/
|
|
51
|
+
export type NotificationTrigger =
|
|
52
|
+
| { type: 'date'; date: Date }
|
|
53
|
+
| { type: 'daily'; hour: number; minute: number }
|
|
54
|
+
| { type: 'weekly'; weekday: number; hour: number; minute: number }
|
|
55
|
+
| { type: 'monthly'; day: number; hour: number; minute: number };
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Options for scheduling a notification
|
|
59
|
+
*/
|
|
60
|
+
export interface ScheduleNotificationOptions {
|
|
61
|
+
title: string;
|
|
62
|
+
body: string;
|
|
63
|
+
data?: Record<string, any>;
|
|
64
|
+
trigger: NotificationTrigger;
|
|
65
|
+
sound?: boolean | string;
|
|
66
|
+
badge?: number;
|
|
67
|
+
categoryIdentifier?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Scheduled notification details
|
|
72
|
+
*/
|
|
73
|
+
export interface ScheduledNotification {
|
|
74
|
+
identifier: string;
|
|
75
|
+
content: {
|
|
76
|
+
title: string;
|
|
77
|
+
body: string;
|
|
78
|
+
data: Record<string, any>;
|
|
79
|
+
};
|
|
80
|
+
trigger: any;
|
|
81
|
+
}
|