@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.
Files changed (78) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +93 -0
  3. package/lib/index.d.ts +13 -0
  4. package/lib/index.d.ts.map +1 -0
  5. package/lib/index.js +15 -0
  6. package/lib/index.js.map +1 -0
  7. package/lib/infrastructure/config/notificationsConfig.d.ts +20 -0
  8. package/lib/infrastructure/config/notificationsConfig.d.ts.map +1 -0
  9. package/lib/infrastructure/config/notificationsConfig.js +81 -0
  10. package/lib/infrastructure/config/notificationsConfig.js.map +1 -0
  11. package/lib/infrastructure/hooks/actions/useNotificationActions.d.ts +17 -0
  12. package/lib/infrastructure/hooks/actions/useNotificationActions.d.ts.map +1 -0
  13. package/lib/infrastructure/hooks/actions/useNotificationActions.js +141 -0
  14. package/lib/infrastructure/hooks/actions/useNotificationActions.js.map +1 -0
  15. package/lib/infrastructure/hooks/state/useNotificationsState.d.ts +12 -0
  16. package/lib/infrastructure/hooks/state/useNotificationsState.d.ts.map +1 -0
  17. package/lib/infrastructure/hooks/state/useNotificationsState.js +30 -0
  18. package/lib/infrastructure/hooks/state/useNotificationsState.js.map +1 -0
  19. package/lib/infrastructure/hooks/types.d.ts +87 -0
  20. package/lib/infrastructure/hooks/types.d.ts.map +1 -0
  21. package/lib/infrastructure/hooks/types.js +8 -0
  22. package/lib/infrastructure/hooks/types.js.map +1 -0
  23. package/lib/infrastructure/hooks/useNotificationSettings.d.ts +10 -0
  24. package/lib/infrastructure/hooks/useNotificationSettings.d.ts.map +1 -0
  25. package/lib/infrastructure/hooks/useNotificationSettings.js +43 -0
  26. package/lib/infrastructure/hooks/useNotificationSettings.js.map +1 -0
  27. package/lib/infrastructure/hooks/useNotifications.d.ts +23 -0
  28. package/lib/infrastructure/hooks/useNotifications.d.ts.map +1 -0
  29. package/lib/infrastructure/hooks/useNotifications.js +51 -0
  30. package/lib/infrastructure/hooks/useNotifications.js.map +1 -0
  31. package/lib/infrastructure/hooks/utils/useNotificationRefresh.d.ts +13 -0
  32. package/lib/infrastructure/hooks/utils/useNotificationRefresh.d.ts.map +1 -0
  33. package/lib/infrastructure/hooks/utils/useNotificationRefresh.js +82 -0
  34. package/lib/infrastructure/hooks/utils/useNotificationRefresh.js.map +1 -0
  35. package/lib/infrastructure/services/NotificationManager.d.ts +138 -0
  36. package/lib/infrastructure/services/NotificationManager.d.ts.map +1 -0
  37. package/lib/infrastructure/services/NotificationManager.js +284 -0
  38. package/lib/infrastructure/services/NotificationManager.js.map +1 -0
  39. package/lib/infrastructure/services/NotificationService.d.ts +30 -0
  40. package/lib/infrastructure/services/NotificationService.d.ts.map +1 -0
  41. package/lib/infrastructure/services/NotificationService.js +41 -0
  42. package/lib/infrastructure/services/NotificationService.js.map +1 -0
  43. package/lib/infrastructure/services/channels/ChannelManager.d.ts +18 -0
  44. package/lib/infrastructure/services/channels/ChannelManager.d.ts.map +1 -0
  45. package/lib/infrastructure/services/channels/ChannelManager.js +87 -0
  46. package/lib/infrastructure/services/channels/ChannelManager.js.map +1 -0
  47. package/lib/infrastructure/services/delivery/NotificationDelivery.d.ts +16 -0
  48. package/lib/infrastructure/services/delivery/NotificationDelivery.d.ts.map +1 -0
  49. package/lib/infrastructure/services/delivery/NotificationDelivery.js +57 -0
  50. package/lib/infrastructure/services/delivery/NotificationDelivery.js.map +1 -0
  51. package/lib/infrastructure/services/preferences/PreferencesManager.d.ts +18 -0
  52. package/lib/infrastructure/services/preferences/PreferencesManager.d.ts.map +1 -0
  53. package/lib/infrastructure/services/preferences/PreferencesManager.js +65 -0
  54. package/lib/infrastructure/services/preferences/PreferencesManager.js.map +1 -0
  55. package/lib/infrastructure/services/types.d.ts +89 -0
  56. package/lib/infrastructure/services/types.d.ts.map +1 -0
  57. package/lib/infrastructure/services/types.js +7 -0
  58. package/lib/infrastructure/services/types.js.map +1 -0
  59. package/lib/infrastructure/storage/NotificationsStore.d.ts +23 -0
  60. package/lib/infrastructure/storage/NotificationsStore.d.ts.map +1 -0
  61. package/lib/infrastructure/storage/NotificationsStore.js +25 -0
  62. package/lib/infrastructure/storage/NotificationsStore.js.map +1 -0
  63. package/package.json +62 -0
  64. package/src/index.ts +34 -0
  65. package/src/infrastructure/config/notificationsConfig.ts +98 -0
  66. package/src/infrastructure/hooks/actions/useNotificationActions.ts +233 -0
  67. package/src/infrastructure/hooks/state/useNotificationsState.ts +46 -0
  68. package/src/infrastructure/hooks/types.ts +83 -0
  69. package/src/infrastructure/hooks/useNotificationSettings.ts +45 -0
  70. package/src/infrastructure/hooks/useNotifications.ts +70 -0
  71. package/src/infrastructure/hooks/utils/useNotificationRefresh.ts +107 -0
  72. package/src/infrastructure/services/NotificationManager.ts +326 -0
  73. package/src/infrastructure/services/NotificationService.ts +50 -0
  74. package/src/infrastructure/services/channels/ChannelManager.ts +111 -0
  75. package/src/infrastructure/services/delivery/NotificationDelivery.ts +65 -0
  76. package/src/infrastructure/services/preferences/PreferencesManager.ts +77 -0
  77. package/src/infrastructure/services/types.ts +81 -0
  78. 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
+ }