react-native-alarms 0.0.5 → 0.0.7

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.
@@ -10,25 +10,25 @@ import UserNotifications
10
10
  @objc(RNAlarmNotificationBackend)
11
11
  final class NotificationBackend: NSObject, UNUserNotificationCenterDelegate {
12
12
  static let shared = NotificationBackend()
13
-
13
+
14
14
  static let category = "RN_ALARM"
15
15
  static let actionSnooze = "RN_ALARM_SNOOZE"
16
16
  static let actionStop = "RN_ALARM_STOP"
17
17
  private static let prefix = "rnalarm:"
18
18
  /// How many `interval` occurrences to pre-schedule (iOS caps pending requests at 64).
19
19
  private static let intervalOccurrences = 12
20
-
20
+
21
21
  private let center = UNUserNotificationCenter.current()
22
-
22
+
23
23
  /// The alarm whose notification is currently being shown (foreground) or was tapped.
24
24
  private(set) var ringing: AlarmData?
25
25
  /// Wired by `AlarmModule` so native events reach JS.
26
26
  var onEvent: ((String, AlarmData) -> Void)?
27
-
27
+
28
28
  /// Delegate that was installed before us (e.g. Notifee); non-alarm notifications go to it.
29
29
  private weak var previousDelegate: UNUserNotificationCenterDelegate?
30
30
  private static var installed = false
31
-
31
+
32
32
  /// Installs the delegate and actions. Call once at launch so a tap on a
33
33
  /// notification that starts the app is delivered to the module.
34
34
  @objc static func install() {
@@ -51,9 +51,9 @@ final class NotificationBackend: NSObject, UNUserNotificationCenterDelegate {
51
51
  ),
52
52
  ])
53
53
  }
54
-
54
+
55
55
  // MARK: - Permission
56
-
56
+
57
57
  /// 'granted' | 'denied' | 'notDetermined', same vocabulary as Android.
58
58
  func status(_ completion: @escaping (String) -> Void) {
59
59
  center.getNotificationSettings { settings in
@@ -64,20 +64,20 @@ final class NotificationBackend: NSObject, UNUserNotificationCenterDelegate {
64
64
  }
65
65
  }
66
66
  }
67
-
67
+
68
68
  func request(_ completion: @escaping (Bool) -> Void) {
69
69
  center.requestAuthorization(options: [.alert, .sound, .badge]) { ok, _ in completion(ok) }
70
70
  }
71
-
71
+
72
72
  // MARK: - Scheduling
73
-
73
+
74
74
  private func ids(for alarmId: String) -> [String] {
75
75
  let base = Self.prefix + alarmId
76
76
  return [base, base + ":snooze"]
77
- + (1...7).map { base + ":wd\($0)" }
78
- + (0..<Self.intervalOccurrences).map { base + ":i\($0)" }
77
+ + (1...7).map { base + ":wd\($0)" }
78
+ + (0..<Self.intervalOccurrences).map { base + ":i\($0)" }
79
79
  }
80
-
80
+
81
81
  private func content(for a: AlarmData) -> UNMutableNotificationContent {
82
82
  let c = UNMutableNotificationContent()
83
83
  c.title = a.title.isEmpty ? "Alarm" : a.title
@@ -85,32 +85,32 @@ final class NotificationBackend: NSObject, UNUserNotificationCenterDelegate {
85
85
  c.categoryIdentifier = Self.category
86
86
  c.userInfo = ["alarmId": a.id]
87
87
  c.sound = (a.sound.isEmpty || a.sound == "default")
88
- ? .default
89
- : UNNotificationSound(named: UNNotificationSoundName(rawValue: a.sound))
88
+ ? .default
89
+ : UNNotificationSound(named: UNNotificationSoundName(rawValue: a.sound))
90
90
  // Breaks through Focus when the Time Sensitive Notifications capability is enabled;
91
91
  // silently downgraded to a normal notification otherwise.
92
92
  c.interruptionLevel = .timeSensitive
93
93
  return c
94
94
  }
95
-
95
+
96
96
  private func request(_ id: String, _ a: AlarmData, _ trigger: UNNotificationTrigger) -> UNNotificationRequest {
97
97
  UNNotificationRequest(identifier: id, content: content(for: a), trigger: trigger)
98
98
  }
99
-
99
+
100
100
  func schedule(_ a: AlarmData, completion: @escaping (Error?) -> Void) {
101
101
  cancel(a.id)
102
102
  let cal = Calendar.current
103
103
  let base = Self.prefix + a.id
104
104
  let hm = cal.dateComponents([.hour, .minute], from: a.date)
105
105
  var requests: [UNNotificationRequest] = []
106
-
106
+
107
107
  switch a.repeatType {
108
108
  case "daily":
109
109
  var dc = DateComponents()
110
110
  dc.hour = hm.hour
111
111
  dc.minute = hm.minute
112
112
  requests.append(request(base, a, UNCalendarNotificationTrigger(dateMatching: dc, repeats: true)))
113
-
113
+
114
114
  case "weekly":
115
115
  let days = a.weekdays.isEmpty ? Array(1...7) : a.weekdays.filter { (1...7).contains($0) }
116
116
  for d in days {
@@ -120,7 +120,7 @@ final class NotificationBackend: NSObject, UNUserNotificationCenterDelegate {
120
120
  dc.minute = hm.minute
121
121
  requests.append(request(base + ":wd\(d)", a, UNCalendarNotificationTrigger(dateMatching: dc, repeats: true)))
122
122
  }
123
-
123
+
124
124
  case "interval":
125
125
  // iOS repeating interval triggers count from "now", not from `timestamp`, so
126
126
  // pre-schedule the next N exact occurrences instead.
@@ -130,16 +130,16 @@ final class NotificationBackend: NSObject, UNUserNotificationCenterDelegate {
130
130
  requests.append(request(base + ":i\(k)", a, exactTrigger(ms: t)))
131
131
  t += a.intervalMs
132
132
  }
133
-
133
+
134
134
  default:
135
135
  let next = a.nextTrigger()
136
136
  guard next > 0 else { return completion(nil) } // nothing in the future, same as Android
137
137
  requests.append(request(base, a, exactTrigger(ms: next)))
138
138
  }
139
-
139
+
140
140
  add(requests, completion: completion)
141
141
  }
142
-
142
+
143
143
  private func exactTrigger(ms: Double) -> UNCalendarNotificationTrigger {
144
144
  let dc = Calendar.current.dateComponents(
145
145
  [.year, .month, .day, .hour, .minute, .second],
@@ -147,7 +147,7 @@ final class NotificationBackend: NSObject, UNUserNotificationCenterDelegate {
147
147
  )
148
148
  return UNCalendarNotificationTrigger(dateMatching: dc, repeats: false)
149
149
  }
150
-
150
+
151
151
  private func add(_ requests: [UNNotificationRequest], completion: @escaping (Error?) -> Void) {
152
152
  let group = DispatchGroup()
153
153
  var firstError: Error?
@@ -160,7 +160,7 @@ final class NotificationBackend: NSObject, UNUserNotificationCenterDelegate {
160
160
  }
161
161
  group.notify(queue: .main) { completion(firstError) }
162
162
  }
163
-
163
+
164
164
  /// One-off re-ring in `minutes`, equivalent to Android's `AlarmScheduler.scheduleSnooze`.
165
165
  func snooze(_ a: AlarmData, minutes: Int) {
166
166
  center.removeDeliveredNotifications(withIdentifiers: ids(for: a.id))
@@ -168,19 +168,19 @@ final class NotificationBackend: NSObject, UNUserNotificationCenterDelegate {
168
168
  center.add(request(Self.prefix + a.id + ":snooze", a, trigger))
169
169
  if ringing?.id == a.id { ringing = nil }
170
170
  }
171
-
171
+
172
172
  /// Stops the notification's sound (by clearing the delivered notification) while the
173
173
  /// in-app ring screen takes over; `ringing` stays set so the session is still live.
174
174
  func silence(_ id: String) {
175
175
  center.removeDeliveredNotifications(withIdentifiers: ids(for: id))
176
176
  }
177
-
177
+
178
178
  func cancel(_ id: String) {
179
179
  center.removePendingNotificationRequests(withIdentifiers: ids(for: id))
180
180
  center.removeDeliveredNotifications(withIdentifiers: ids(for: id))
181
181
  if ringing?.id == id { ringing = nil }
182
182
  }
183
-
183
+
184
184
  func cancelAll() {
185
185
  center.getPendingNotificationRequests { (requests: [UNNotificationRequest]) in
186
186
  let ours = requests.map { $0.identifier }.filter { $0.hasPrefix(Self.prefix) }
@@ -192,7 +192,7 @@ final class NotificationBackend: NSObject, UNUserNotificationCenterDelegate {
192
192
  }
193
193
  ringing = nil
194
194
  }
195
-
195
+
196
196
  /// Dismiss: clear the shown notification and, like Android, drop a pending snooze.
197
197
  func stop(_ id: String) {
198
198
  center.removeDeliveredNotifications(withIdentifiers: ids(for: id))
@@ -202,14 +202,14 @@ final class NotificationBackend: NSObject, UNUserNotificationCenterDelegate {
202
202
  onEvent?(AlarmModule.eventDismissed, a)
203
203
  }
204
204
  }
205
-
205
+
206
206
  // MARK: - UNUserNotificationCenterDelegate
207
-
207
+
208
208
  private func alarm(from notification: UNNotification) -> AlarmData? {
209
209
  guard let id = notification.request.content.userInfo["alarmId"] as? String else { return nil }
210
210
  return AlarmStore.get(id)
211
211
  }
212
-
212
+
213
213
  func userNotificationCenter(
214
214
  _ center: UNUserNotificationCenter,
215
215
  willPresent notification: UNNotification,
@@ -229,7 +229,7 @@ final class NotificationBackend: NSObject, UNUserNotificationCenterDelegate {
229
229
  // The ring screen takes over; keep the sound but skip the banner when it can show.
230
230
  completionHandler(onEvent == nil ? [.banner, .list, .sound] : [.list, .sound])
231
231
  }
232
-
232
+
233
233
  func userNotificationCenter(
234
234
  _ center: UNUserNotificationCenter,
235
235
  didReceive response: UNNotificationResponse,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-alarms",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "A powerful, cross-platform alarm management library for React Native",
5
5
  "private": false,
6
6
  "main": "index.ts",
@@ -22,7 +22,7 @@
22
22
  "lint": "eslint .",
23
23
  "start": "react-native start",
24
24
  "test": "jest",
25
- "prepare": "npm run build",
25
+ "prepare": "husky",
26
26
  "build": "tsc",
27
27
  "clean": "rm -rf dist types *.d.ts"
28
28
  },
@@ -56,6 +56,8 @@
56
56
  "@babel/core": "^7.25.2",
57
57
  "@babel/preset-env": "^7.25.3",
58
58
  "@babel/runtime": "^7.25.0",
59
+ "@commitlint/cli": "^21.2.4",
60
+ "@commitlint/config-conventional": "^21.2.4",
59
61
  "@react-native-community/cli": "20.2.0",
60
62
  "@react-native-community/cli-platform-android": "20.2.0",
61
63
  "@react-native-community/cli-platform-ios": "20.2.0",
@@ -68,6 +70,7 @@
68
70
  "@types/react": "^19.2.0",
69
71
  "@types/react-test-renderer": "^19.1.0",
70
72
  "eslint": "^8.19.0",
73
+ "husky": "^9.1.7",
71
74
  "jest": "^29.6.3",
72
75
  "prettier": "2.8.8",
73
76
  "react": "19.3.0",
@@ -1,97 +1,130 @@
1
1
  import type { TurboModule } from 'react-native';
2
2
  import { NativeModules, Platform, TurboModuleRegistry } from 'react-native';
3
+ import { AlarmPermission, AlarmConfig, AlarmProp } from '../src/types';
3
4
 
4
5
  /**
5
- * Shared contract of the native `Alarm` module.
6
- *
7
- * Both platforms register a legacy-style module named `Alarm` with these exact
8
- * method names (Android: AlarmModule.kt, iOS: ios/alarm/AlarmModule.swift) and
9
- * emit the same events through `NativeEventEmitter`:
10
- * onAlarmFired | onAlarmDismissed | onAlarmSnoozed -> AlarmEvent
6
+ * Native Alarm module interface.
11
7
  */
12
-
13
- /** type: 'none' | 'daily' | 'weekly' | 'interval' */
14
- export type AlarmRepeat = {
15
- type: string;
16
- weekdays?: number[]; // 1 = Sunday ... 7 = Saturday (weekly)
17
- intervalMs?: number; // interval
18
- };
19
-
20
- export type AlarmConfig = {
21
- alarmId?: string;
22
- timestamp: number; // ms since epoch
23
- title: string;
24
- body?: string;
25
- sound?: string; // 'default' or a bundled sound file name
26
- vibrate?: boolean; // Android only (iOS alarms always vibrate per system settings)
27
- snoozeEnabled?: boolean;
28
- snoozeMinutes?: number;
29
- repeat?: AlarmRepeat;
30
- data?: string; // JSON string payload
31
- };
32
-
33
- export type Alarm = {
34
- alarmId: string;
35
- timestamp: number;
36
- title: string;
37
- body: string;
38
- sound: string;
39
- vibrate: boolean;
40
- snoozeEnabled: boolean;
41
- snoozeMinutes: number;
42
- repeat: AlarmRepeat;
43
- data: string;
44
- enabled: boolean;
45
- nextTriggerAt: number; // 0 if none
46
- };
47
-
48
- /** status: 'granted' | 'denied' | 'notDetermined' */
49
- export type AlarmPermission = {
50
- status: string;
51
- notifications: boolean;
52
- canScheduleExact: boolean;
53
- canDrawOverlays: boolean; // Android only; always true on iOS
54
- canUseFullScreenIntent: boolean; // Android only; always true on iOS
55
- backend?: string; // iOS only: 'alarmkit' (iOS 26+) | 'notifications'
56
- };
57
-
58
- export type AlarmEvent = {
59
- alarmId: string;
60
- timestamp: number;
61
- title: string;
62
- body: string;
63
- data: string;
64
- };
65
-
66
8
  export interface Spec extends TurboModule {
67
- // ---------- Permissions ----------
9
+ // ---------------------------------------------------------------------------
10
+ // Permissions
11
+ // ---------------------------------------------------------------------------
12
+
13
+ /**
14
+ * Returns the current permission status.
15
+ */
68
16
  checkPermission(): Promise<AlarmPermission>;
17
+
18
+ /**
19
+ * Requests any required permissions.
20
+ */
69
21
  requestPermission(): Promise<AlarmPermission>;
22
+
23
+ /**
24
+ * Checks whether exact alarms can be scheduled.
25
+ *
26
+ * Android only.
27
+ */
70
28
  canScheduleExactAlarms(): Promise<boolean>;
71
- /** Opens the vendor Autostart screen on Android; resolves false on iOS. */
29
+
30
+ /**
31
+ * Opens the device's Autostart settings screen.
32
+ *
33
+ * Android:
34
+ * Opens the vendor-specific Autostart page when available.
35
+ *
36
+ * iOS:
37
+ * Resolves `false`.
38
+ */
72
39
  openAutoStartSettings(): Promise<boolean>;
73
40
 
74
- // ---------- Scheduling ----------
41
+ // ---------------------------------------------------------------------------
42
+ // Scheduling
43
+ // ---------------------------------------------------------------------------
44
+
45
+ /**
46
+ * Schedules a new alarm.
47
+ *
48
+ * Returns the generated or provided alarm ID.
49
+ */
75
50
  scheduleAlarm(config: AlarmConfig): Promise<string>;
51
+
52
+ /**
53
+ * Updates an existing alarm.
54
+ *
55
+ * Returns `true` if the update succeeds.
56
+ */
76
57
  updateAlarm(alarmId: string, config: AlarmConfig): Promise<boolean>;
58
+
59
+ /**
60
+ * Cancels a specific alarm.
61
+ */
77
62
  cancelAlarm(alarmId: string): Promise<boolean>;
63
+
64
+ /**
65
+ * Cancels every scheduled alarm.
66
+ */
78
67
  cancelAllAlarms(): Promise<void>;
79
68
 
80
- // ---------- Control (while ringing) ----------
69
+ // ---------------------------------------------------------------------------
70
+ // Ringing Controls
71
+ // ---------------------------------------------------------------------------
72
+
73
+ /**
74
+ * Stops the currently ringing alarm.
75
+ */
81
76
  stopAlarm(alarmId: string): Promise<void>;
77
+
78
+ /**
79
+ * Snoozes the currently ringing alarm.
80
+ *
81
+ * @param alarmId Alarm identifier.
82
+ * @param minutes Snooze duration in minutes.
83
+ */
82
84
  snoozeAlarm(alarmId: string, minutes: number): Promise<void>;
83
- getRingingAlarm(): Promise<Alarm | null>;
84
85
 
85
- // ---------- Queries ----------
86
- getAlarm(alarmId: string): Promise<Alarm | null>;
87
- getAllAlarms(): Promise<Alarm[]>;
86
+ /**
87
+ * Returns the currently ringing alarm, if one exists.
88
+ */
89
+ getRingingAlarm(): Promise<AlarmProp | null>;
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Queries
93
+ // ---------------------------------------------------------------------------
94
+
95
+ /**
96
+ * Returns a scheduled alarm by ID.
97
+ */
98
+ getAlarm(alarmId: string): Promise<AlarmProp | null>;
99
+
100
+ /**
101
+ * Returns every scheduled alarm.
102
+ */
103
+ getAllAlarms(): Promise<AlarmProp[]>;
104
+
105
+ /**
106
+ * Checks whether an alarm is scheduled.
107
+ */
88
108
  isScheduled(alarmId: string): Promise<boolean>;
89
109
 
90
- // ---------- Events (used by NativeEventEmitter) ----------
110
+ // ---------------------------------------------------------------------------
111
+ // NativeEventEmitter Support
112
+ // ---------------------------------------------------------------------------
113
+
114
+ /**
115
+ * Internal listener registration.
116
+ */
91
117
  addListener(eventName: string): void;
118
+
119
+ /**
120
+ * Internal listener cleanup.
121
+ */
92
122
  removeListeners(count: number): void;
93
123
  }
94
124
 
125
+ /**
126
+ * Error shown when the native module is unavailable.
127
+ */
95
128
  const LINKING_ERROR =
96
129
  "react-native-alarms: the native 'Alarm' module is not linked. " +
97
130
  Platform.select({
@@ -99,8 +132,24 @@ const LINKING_ERROR =
99
132
  default: 'Rebuild the app after installing the package.',
100
133
  });
101
134
 
102
- // Both platforms register a legacy module, reachable through either registry
103
- // depending on the architecture. Throw on first use, not on import.
135
+ /**
136
+ * Cross-platform native Alarm module.
137
+ *
138
+ * Supports both the legacy Native Module system and the New Architecture
139
+ * (TurboModules). The module is resolved lazily so importing it does not
140
+ * immediately throw if native linking is missing.
141
+ *
142
+ * @example
143
+ * ```ts
144
+ * import Alarm from 'react-native-alarms';
145
+ *
146
+ * const id = await Alarm.scheduleAlarm({
147
+ * timestamp: Date.now() + 60000,
148
+ * title: 'Meeting',
149
+ * body: 'Standup starts now',
150
+ * });
151
+ * ```
152
+ */
104
153
  const Alarm: Spec =
105
154
  TurboModuleRegistry.get<Spec>('Alarm') ??
106
155
  NativeModules.Alarm ??
Binary file
Binary file
Binary file
Binary file
File without changes