react-native-alarms 0.0.6 → 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.
- package/AlarmService.d.ts +137 -33
- package/AlarmService.ts +224 -37
- package/README.md +143 -9
- package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmData.kt +154 -8
- package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmModule.kt +2 -2
- package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmRingActivity.kt +3 -2
- package/android/local.properties +8 -0
- package/index.d.ts +3 -3
- package/index.ts +5 -5
- package/ios/alarm/AlarmData.swift +20 -6
- package/ios/alarm/AlarmKitBackend.swift +43 -40
- package/ios/alarm/AlarmModule.swift +68 -64
- package/ios/alarm/AlarmRingPresenter.swift +26 -19
- package/ios/alarm/AlarmStore.swift +9 -9
- package/ios/alarm/NotificationBackend.swift +33 -33
- package/package.json +1 -1
- package/specs/NativeAlarm.ts +120 -71
- package/src/types/index.ts +246 -0
- package/src/utils/AlarmRegistry.ts +57 -0
- package/src/utils/index.ts +1 -0
|
@@ -8,12 +8,15 @@ import UIKit
|
|
|
8
8
|
/// so this shows when the alarm fires while the app is in the foreground, or when the
|
|
9
9
|
/// user opens the app by tapping the alarm notification.
|
|
10
10
|
final class AlarmRingPresenter {
|
|
11
|
-
static let
|
|
12
|
-
|
|
11
|
+
private static let defaultComponentName = "AlarmRing"
|
|
12
|
+
|
|
13
13
|
private static weak var controller: UIViewController?
|
|
14
14
|
private static var alarmId: String?
|
|
15
|
-
|
|
16
|
-
static func present(_ alarm: AlarmData, bridge: RCTBridge?,
|
|
15
|
+
|
|
16
|
+
static func present(_ alarm: AlarmData, bridge: RCTBridge?,
|
|
17
|
+
componentName: String = defaultComponentName,
|
|
18
|
+
componentProps: [String: Any] = [:],
|
|
19
|
+
attempt: Int = 0) {
|
|
17
20
|
DispatchQueue.main.async {
|
|
18
21
|
if controller != nil, alarmId == alarm.id {
|
|
19
22
|
// Already showing (e.g. presented while backgrounded, now foregrounded):
|
|
@@ -23,7 +26,9 @@ final class AlarmRingPresenter {
|
|
|
23
26
|
}
|
|
24
27
|
// Cold start from a notification tap or the AlarmKit "Open" button: the root
|
|
25
28
|
// surface or window may not be ready yet, so retry for up to ~15 s.
|
|
26
|
-
guard let surface = makeSurface(bridge,
|
|
29
|
+
guard let surface = makeSurface(bridge,
|
|
30
|
+
componentName: componentName,
|
|
31
|
+
props: alarm.toDictionary()),
|
|
27
32
|
let top = topViewController() else {
|
|
28
33
|
if attempt < 60 {
|
|
29
34
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
|
@@ -34,7 +39,7 @@ final class AlarmRingPresenter {
|
|
|
34
39
|
}
|
|
35
40
|
return
|
|
36
41
|
}
|
|
37
|
-
|
|
42
|
+
|
|
38
43
|
let vc = UIViewController()
|
|
39
44
|
let root = RCTSurfaceHostingProxyRootView(surface: surface)
|
|
40
45
|
root.backgroundColor = .systemBackground
|
|
@@ -42,7 +47,7 @@ final class AlarmRingPresenter {
|
|
|
42
47
|
surface.start()
|
|
43
48
|
vc.modalPresentationStyle = .fullScreen
|
|
44
49
|
vc.isModalInPresentation = true
|
|
45
|
-
|
|
50
|
+
|
|
46
51
|
let show = {
|
|
47
52
|
top.present(vc, animated: true)
|
|
48
53
|
controller = vc
|
|
@@ -56,7 +61,7 @@ final class AlarmRingPresenter {
|
|
|
56
61
|
}
|
|
57
62
|
}
|
|
58
63
|
}
|
|
59
|
-
|
|
64
|
+
|
|
60
65
|
/// The ring screen speaks the task via TTS, and iOS won't play it over the
|
|
61
66
|
/// notification / AlarmKit sound, so stop that sound once the screen is up.
|
|
62
67
|
/// Only while the app is frontmost: in the background the system alert is the
|
|
@@ -64,11 +69,11 @@ final class AlarmRingPresenter {
|
|
|
64
69
|
private static func silenceIfActive(_ id: String) {
|
|
65
70
|
guard UIApplication.shared.applicationState == .active else { return }
|
|
66
71
|
NotificationBackend.shared.silence(id)
|
|
67
|
-
|
|
72
|
+
#if canImport(AlarmKit)
|
|
68
73
|
if #available(iOS 26.0, *) { AlarmKitBackend.shared.silence(id) }
|
|
69
|
-
|
|
74
|
+
#endif
|
|
70
75
|
}
|
|
71
|
-
|
|
76
|
+
|
|
72
77
|
/// Called when ringing ends for any reason (stop, snooze, cancel).
|
|
73
78
|
static func dismiss(_ id: String? = nil) {
|
|
74
79
|
DispatchQueue.main.async {
|
|
@@ -78,18 +83,20 @@ final class AlarmRingPresenter {
|
|
|
78
83
|
vc.dismiss(animated: true)
|
|
79
84
|
}
|
|
80
85
|
}
|
|
81
|
-
|
|
86
|
+
|
|
82
87
|
private static let createSurface = NSSelectorFromString("createFabricSurfaceForModuleName:initialProperties:")
|
|
83
|
-
|
|
88
|
+
|
|
84
89
|
/// `RCTSurfacePresenter` implements `createFabricSurfaceForModuleName:initialProperties:`
|
|
85
90
|
/// without formally adopting `RCTSurfacePresenterStub`, and its header is ObjC++ (not
|
|
86
91
|
/// importable from Swift), so call it by selector.
|
|
87
|
-
private static func makeSurface(_ bridge: RCTBridge?,
|
|
92
|
+
private static func makeSurface(_ bridge: RCTBridge?,
|
|
93
|
+
componentName: String,
|
|
94
|
+
props: [String: Any]) -> RCTSurfaceProtocol? {
|
|
88
95
|
guard let presenter = surfacePresenter(bridge), presenter.responds(to: createSurface) else { return nil }
|
|
89
96
|
return presenter.perform(createSurface, with: componentName, with: props)?
|
|
90
97
|
.takeUnretainedValue() as? RCTSurfaceProtocol
|
|
91
98
|
}
|
|
92
|
-
|
|
99
|
+
|
|
93
100
|
/// Bridgeless mode skips `bridge.surfacePresenter` (and injection) for modules
|
|
94
101
|
/// registered with `RCT_EXTERN_MODULE`, so fall back to the presenter of the app's
|
|
95
102
|
/// root React surface, which `RCTFabricSurface` keeps in a weak `_surfacePresenter` ivar.
|
|
@@ -99,10 +106,10 @@ final class AlarmRingPresenter {
|
|
|
99
106
|
let hosting = findHostingView(in: root),
|
|
100
107
|
let surface = hosting.surface as AnyObject?,
|
|
101
108
|
let ivar = class_getInstanceVariable(type(of: surface), "_surfacePresenter")
|
|
102
|
-
|
|
109
|
+
else { return nil }
|
|
103
110
|
return object_getIvar(surface, ivar) as? NSObject
|
|
104
111
|
}
|
|
105
|
-
|
|
112
|
+
|
|
106
113
|
private static func findHostingView(in view: UIView) -> RCTSurfaceHostingView? {
|
|
107
114
|
if let hosting = view as? RCTSurfaceHostingView { return hosting }
|
|
108
115
|
for sub in view.subviews {
|
|
@@ -110,14 +117,14 @@ final class AlarmRingPresenter {
|
|
|
110
117
|
}
|
|
111
118
|
return nil
|
|
112
119
|
}
|
|
113
|
-
|
|
120
|
+
|
|
114
121
|
private static func keyWindow() -> UIWindow? {
|
|
115
122
|
UIApplication.shared.connectedScenes
|
|
116
123
|
.compactMap { $0 as? UIWindowScene }
|
|
117
124
|
.flatMap { $0.windows }
|
|
118
125
|
.first { $0.isKeyWindow }
|
|
119
126
|
}
|
|
120
|
-
|
|
127
|
+
|
|
121
128
|
private static func topViewController() -> UIViewController? {
|
|
122
129
|
var top = keyWindow()?.rootViewController
|
|
123
130
|
while let presented = top?.presentedViewController { top = presented }
|
|
@@ -4,45 +4,45 @@ import Foundation
|
|
|
4
4
|
enum AlarmStore {
|
|
5
5
|
private static let key = "RNAlarm.alarms"
|
|
6
6
|
private static let lock = NSLock()
|
|
7
|
-
|
|
7
|
+
|
|
8
8
|
static func all() -> [AlarmData] {
|
|
9
9
|
lock.lock(); defer { lock.unlock() }
|
|
10
10
|
return Array(load().values)
|
|
11
11
|
}
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
static func get(_ id: String) -> AlarmData? {
|
|
14
14
|
lock.lock(); defer { lock.unlock() }
|
|
15
15
|
return load()[id]
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
static func save(_ alarm: AlarmData) {
|
|
19
19
|
lock.lock(); defer { lock.unlock() }
|
|
20
20
|
var map = load()
|
|
21
21
|
map[alarm.id] = alarm
|
|
22
22
|
persist(map)
|
|
23
23
|
}
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
static func remove(_ id: String) {
|
|
26
26
|
lock.lock(); defer { lock.unlock() }
|
|
27
27
|
var map = load()
|
|
28
28
|
map.removeValue(forKey: id)
|
|
29
29
|
persist(map)
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
static func clear() {
|
|
33
33
|
lock.lock(); defer { lock.unlock() }
|
|
34
34
|
persist([:])
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
// MARK: - Private
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
private static func load() -> [String: AlarmData] {
|
|
40
40
|
guard let data = UserDefaults.standard.data(forKey: key),
|
|
41
41
|
let map = try? JSONDecoder().decode([String: AlarmData].self, from: data)
|
|
42
|
-
|
|
42
|
+
else { return [:] }
|
|
43
43
|
return map
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
private static func persist(_ map: [String: AlarmData]) {
|
|
47
47
|
if let data = try? JSONEncoder().encode(map) {
|
|
48
48
|
UserDefaults.standard.set(data, forKey: key)
|
|
@@ -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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
package/specs/NativeAlarm.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
103
|
-
|
|
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 ??
|