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
|
@@ -20,13 +20,13 @@ struct OpenAlarmAppIntent: LiveActivityIntent {
|
|
|
20
20
|
static let description = IntentDescription("Opens the ringing alarm's screen")
|
|
21
21
|
static let openAppWhenRun: Bool = true
|
|
22
22
|
static let isDiscoverable: Bool = false
|
|
23
|
-
|
|
23
|
+
|
|
24
24
|
@Parameter(title: "alarmID")
|
|
25
25
|
var alarmID: String
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
init() {}
|
|
28
28
|
init(alarmID: String) { self.alarmID = alarmID }
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
func perform() async throws -> some IntentResult {
|
|
31
31
|
AlarmKitBackend.shared.openFromAlert(alarmID)
|
|
32
32
|
return .result()
|
|
@@ -39,11 +39,11 @@ struct OpenAlarmAppIntent: LiveActivityIntent {
|
|
|
39
39
|
@available(iOS 26.0, *)
|
|
40
40
|
final class AlarmKitBackend {
|
|
41
41
|
static let shared = AlarmKitBackend()
|
|
42
|
-
|
|
42
|
+
|
|
43
43
|
private let manager = AlarmManager.shared
|
|
44
44
|
/// Wired by `AlarmModule` so native events reach JS.
|
|
45
45
|
var onEvent: ((String, AlarmData) -> Void)?
|
|
46
|
-
|
|
46
|
+
|
|
47
47
|
private var observer: Task<Void, Never>?
|
|
48
48
|
private var lastStates: [UUID: Alarm.State] = [:]
|
|
49
49
|
/// Alarms we are stopping as part of a programmatic snooze (so the observer
|
|
@@ -53,15 +53,15 @@ final class AlarmKitBackend {
|
|
|
53
53
|
/// (with its own TTS) is showing. The ring session stays alive until JS calls
|
|
54
54
|
/// stop/snooze, so the observer must not report these as dismissed.
|
|
55
55
|
private var silenced: [UUID: AlarmData] = [:]
|
|
56
|
-
|
|
56
|
+
|
|
57
57
|
private static let allWeekdays: [Locale.Weekday] = [
|
|
58
58
|
.sunday, .monday, .tuesday, .wednesday, .thursday, .friday, .saturday,
|
|
59
59
|
]
|
|
60
|
-
|
|
60
|
+
|
|
61
61
|
// MARK: - Authorization
|
|
62
|
-
|
|
62
|
+
|
|
63
63
|
var isAuthorized: Bool { manager.authorizationState == .authorized }
|
|
64
|
-
|
|
64
|
+
|
|
65
65
|
/// 'granted' | 'denied' | 'notDetermined', same vocabulary as Android.
|
|
66
66
|
var status: String {
|
|
67
67
|
switch manager.authorizationState {
|
|
@@ -70,13 +70,13 @@ final class AlarmKitBackend {
|
|
|
70
70
|
default: return "notDetermined"
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
-
|
|
73
|
+
|
|
74
74
|
func requestAuthorization() async -> Bool {
|
|
75
75
|
(try? await manager.requestAuthorization()) == .authorized
|
|
76
76
|
}
|
|
77
|
-
|
|
77
|
+
|
|
78
78
|
// MARK: - Ids
|
|
79
|
-
|
|
79
|
+
|
|
80
80
|
/// Android ids are free-form strings; AlarmKit wants UUIDs. Real UUIDs pass through,
|
|
81
81
|
/// anything else is hashed deterministically so cancel/stop can find it again.
|
|
82
82
|
static func uuid(for id: String, snooze: Bool = false) -> UUID {
|
|
@@ -85,20 +85,20 @@ final class AlarmKitBackend {
|
|
|
85
85
|
let bytes = Array(SHA256.hash(data: Data(key.utf8)).prefix(16))
|
|
86
86
|
return bytes.withUnsafeBufferPointer { NSUUID(uuidBytes: $0.baseAddress!) as UUID }
|
|
87
87
|
}
|
|
88
|
-
|
|
88
|
+
|
|
89
89
|
private func stored(for uuid: UUID) -> AlarmData? {
|
|
90
90
|
AlarmStore.all().first {
|
|
91
91
|
Self.uuid(for: $0.id) == uuid || Self.uuid(for: $0.id, snooze: true) == uuid
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
-
|
|
94
|
+
|
|
95
95
|
// MARK: - Scheduling
|
|
96
|
-
|
|
96
|
+
|
|
97
97
|
func schedule(_ a: AlarmData) async throws {
|
|
98
98
|
cancel(a.id)
|
|
99
99
|
let next = a.nextTrigger()
|
|
100
100
|
guard next > 0 else { return } // nothing in the future; Android skips too
|
|
101
|
-
|
|
101
|
+
|
|
102
102
|
let hm = Calendar.current.dateComponents([.hour, .minute], from: a.date)
|
|
103
103
|
let time = Alarm.Schedule.Relative.Time(hour: hm.hour ?? 0, minute: hm.minute ?? 0)
|
|
104
104
|
let schedule: Alarm.Schedule
|
|
@@ -107,21 +107,21 @@ final class AlarmKitBackend {
|
|
|
107
107
|
schedule = .relative(.init(time: time, repeats: .weekly(Self.allWeekdays)))
|
|
108
108
|
case "weekly":
|
|
109
109
|
let days = a.weekdays.isEmpty
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
? Self.allWeekdays
|
|
111
|
+
: a.weekdays.filter { (1...7).contains($0) }.map { Self.allWeekdays[$0 - 1] }
|
|
112
112
|
schedule = .relative(.init(time: time, repeats: .weekly(days)))
|
|
113
113
|
default:
|
|
114
114
|
// none: one exact ring. interval: one exact ring, re-armed by the observer
|
|
115
115
|
// after it is dismissed (only while the app process is alive).
|
|
116
116
|
schedule = .fixed(Date(timeIntervalSince1970: next / 1000))
|
|
117
117
|
}
|
|
118
|
-
|
|
118
|
+
|
|
119
119
|
_ = try await manager.schedule(id: Self.uuid(for: a.id), configuration: configuration(for: a, schedule: schedule))
|
|
120
120
|
startObserving()
|
|
121
121
|
}
|
|
122
|
-
|
|
122
|
+
|
|
123
123
|
private func configuration(for a: AlarmData, schedule: Alarm.Schedule)
|
|
124
|
-
|
|
124
|
+
-> AlarmManager.AlarmConfiguration<RNAlarmMetadata>
|
|
125
125
|
{
|
|
126
126
|
let title = LocalizedStringResource(stringLiteral: a.title.isEmpty ? "Alarm" : a.title)
|
|
127
127
|
let stop = AlarmButton(text: "Stop", textColor: .white, systemImageName: "stop.fill")
|
|
@@ -132,15 +132,15 @@ final class AlarmKitBackend {
|
|
|
132
132
|
let alert = AlarmPresentation.Alert(
|
|
133
133
|
title: title, stopButton: stop, secondaryButton: open, secondaryButtonBehavior: .custom
|
|
134
134
|
)
|
|
135
|
-
|
|
135
|
+
|
|
136
136
|
let attributes = AlarmAttributes<RNAlarmMetadata>(
|
|
137
137
|
presentation: AlarmPresentation(alert: alert),
|
|
138
138
|
metadata: RNAlarmMetadata(),
|
|
139
139
|
tintColor: .blue
|
|
140
140
|
)
|
|
141
141
|
let sound: AlertConfiguration.AlertSound =
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
(a.sound.isEmpty || a.sound == "default") ? .default : .named(a.sound)
|
|
143
|
+
|
|
144
144
|
return AlarmManager.AlarmConfiguration(
|
|
145
145
|
schedule: schedule,
|
|
146
146
|
attributes: attributes,
|
|
@@ -148,18 +148,18 @@ final class AlarmKitBackend {
|
|
|
148
148
|
sound: sound
|
|
149
149
|
)
|
|
150
150
|
}
|
|
151
|
-
|
|
151
|
+
|
|
152
152
|
func cancel(_ id: String) {
|
|
153
153
|
for uuid in [Self.uuid(for: id), Self.uuid(for: id, snooze: true)] {
|
|
154
154
|
try? manager.cancel(id: uuid)
|
|
155
155
|
silenced.removeValue(forKey: uuid)
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
|
-
|
|
158
|
+
|
|
159
159
|
func cancelAll() {
|
|
160
160
|
for alarm in (try? manager.alarms) ?? [] { try? manager.cancel(id: alarm.id) }
|
|
161
161
|
}
|
|
162
|
-
|
|
162
|
+
|
|
163
163
|
func stop(_ id: String) {
|
|
164
164
|
for uuid in [Self.uuid(for: id), Self.uuid(for: id, snooze: true)] {
|
|
165
165
|
try? manager.stop(id: uuid)
|
|
@@ -171,7 +171,7 @@ final class AlarmKitBackend {
|
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
|
-
|
|
174
|
+
|
|
175
175
|
/// "Open" tapped on the system alert (OpenAlarmAppIntent). The tap stops the
|
|
176
176
|
/// AlarmKit alarm, so adopt the session as `silenced` — still ringing as far as
|
|
177
177
|
/// JS is concerned — and show the React ring screen once the app is frontmost.
|
|
@@ -179,7 +179,7 @@ final class AlarmKitBackend {
|
|
|
179
179
|
guard let a = AlarmStore.get(id) else { return }
|
|
180
180
|
let current = (try? manager.alarms) ?? []
|
|
181
181
|
for uuid in [Self.uuid(for: a.id), Self.uuid(for: a.id, snooze: true)]
|
|
182
|
-
|
|
182
|
+
where current.contains(where: { $0.id == uuid && $0.state == .alerting }) {
|
|
183
183
|
silenced[uuid] = a
|
|
184
184
|
try? manager.stop(id: uuid)
|
|
185
185
|
}
|
|
@@ -187,20 +187,23 @@ final class AlarmKitBackend {
|
|
|
187
187
|
if !silenced.values.contains(where: { $0.id == a.id }) {
|
|
188
188
|
silenced[Self.uuid(for: a.id)] = a
|
|
189
189
|
}
|
|
190
|
-
DispatchQueue.main.async { AlarmRingPresenter.present(a, bridge: nil
|
|
190
|
+
DispatchQueue.main.async { AlarmRingPresenter.present(a, bridge: nil,
|
|
191
|
+
componentName: a.componentName,
|
|
192
|
+
componentProps: a.componentProps
|
|
193
|
+
) }
|
|
191
194
|
}
|
|
192
|
-
|
|
195
|
+
|
|
193
196
|
/// Stops the system alert's sound/UI while the in-app ring screen takes over.
|
|
194
197
|
/// The session stays "ringing" (see `silenced`) until JS stops or snoozes it.
|
|
195
198
|
func silence(_ id: String) {
|
|
196
199
|
let current = (try? manager.alarms) ?? []
|
|
197
200
|
for uuid in [Self.uuid(for: id), Self.uuid(for: id, snooze: true)]
|
|
198
|
-
|
|
201
|
+
where current.contains(where: { $0.id == uuid && $0.state == .alerting }) {
|
|
199
202
|
silenced[uuid] = stored(for: uuid)
|
|
200
203
|
try? manager.stop(id: uuid)
|
|
201
204
|
}
|
|
202
205
|
}
|
|
203
|
-
|
|
206
|
+
|
|
204
207
|
/// Silences the ringing alarm (if any) and re-rings once in `minutes`,
|
|
205
208
|
/// equivalent to Android's `AlarmScheduler.scheduleSnooze`.
|
|
206
209
|
func snooze(_ a: AlarmData, minutes: Int) async throws {
|
|
@@ -222,7 +225,7 @@ final class AlarmKitBackend {
|
|
|
222
225
|
)
|
|
223
226
|
startObserving()
|
|
224
227
|
}
|
|
225
|
-
|
|
228
|
+
|
|
226
229
|
func ringingAlarm() -> AlarmData? {
|
|
227
230
|
if let alerting = ((try? manager.alarms) ?? []).first(where: { $0.state == .alerting }) {
|
|
228
231
|
return stored(for: alerting.id)
|
|
@@ -230,9 +233,9 @@ final class AlarmKitBackend {
|
|
|
230
233
|
// Sound silenced for the in-app ring screen, but the session is still live.
|
|
231
234
|
return silenced.values.first
|
|
232
235
|
}
|
|
233
|
-
|
|
236
|
+
|
|
234
237
|
// MARK: - Events
|
|
235
|
-
|
|
238
|
+
|
|
236
239
|
/// Translates AlarmKit state changes into the Android-style events.
|
|
237
240
|
func startObserving() {
|
|
238
241
|
guard observer == nil else { return }
|
|
@@ -243,11 +246,11 @@ final class AlarmKitBackend {
|
|
|
243
246
|
}
|
|
244
247
|
}
|
|
245
248
|
}
|
|
246
|
-
|
|
249
|
+
|
|
247
250
|
private func handle(_ alarms: [Alarm]) {
|
|
248
251
|
var states: [UUID: Alarm.State] = [:]
|
|
249
252
|
for alarm in alarms { states[alarm.id] = alarm.state }
|
|
250
|
-
|
|
253
|
+
|
|
251
254
|
for (uuid, state) in states where lastStates[uuid] != state {
|
|
252
255
|
guard let a = stored(for: uuid) else { continue }
|
|
253
256
|
if state == .alerting {
|
|
@@ -258,7 +261,7 @@ final class AlarmKitBackend {
|
|
|
258
261
|
onEvent?(state == .countdown ? AlarmModule.eventSnoozed : AlarmModule.eventDismissed, a)
|
|
259
262
|
}
|
|
260
263
|
}
|
|
261
|
-
|
|
264
|
+
|
|
262
265
|
// One-off (`.fixed`) alarms disappear once stopped.
|
|
263
266
|
for (uuid, previous) in lastStates where states[uuid] == nil && previous == .alerting {
|
|
264
267
|
if silenced[uuid] != nil { continue } // silenced for the ring screen, not over yet
|
|
@@ -272,7 +275,7 @@ final class AlarmKitBackend {
|
|
|
272
275
|
}
|
|
273
276
|
}
|
|
274
277
|
}
|
|
275
|
-
|
|
278
|
+
|
|
276
279
|
lastStates = states
|
|
277
280
|
}
|
|
278
281
|
}
|
|
@@ -14,20 +14,20 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
14
14
|
static let eventFired = "onAlarmFired"
|
|
15
15
|
static let eventDismissed = "onAlarmDismissed"
|
|
16
16
|
static let eventSnoozed = "onAlarmSnoozed"
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
private var hasListeners = false
|
|
19
19
|
private var activeObserver: NSObjectProtocol?
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
override init() {
|
|
22
22
|
super.init()
|
|
23
23
|
NotificationBackend.install()
|
|
24
24
|
NotificationBackend.shared.onEvent = { [weak self] name, alarm in self?.handle(name, alarm) }
|
|
25
|
-
|
|
25
|
+
#if canImport(AlarmKit)
|
|
26
26
|
if #available(iOS 26.0, *) {
|
|
27
27
|
AlarmKitBackend.shared.onEvent = { [weak self] name, alarm in self?.handle(name, alarm) }
|
|
28
28
|
AlarmKitBackend.shared.startObserving()
|
|
29
29
|
}
|
|
30
|
-
|
|
30
|
+
#endif
|
|
31
31
|
// App launched by tapping the alarm notification/alert before this module existed.
|
|
32
32
|
DispatchQueue.main.async { [weak self] in self?.presentIfRinging() }
|
|
33
33
|
// Tapping an AlarmKit alert (and some notification taps on a warm start) only
|
|
@@ -36,40 +36,44 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
36
36
|
forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main
|
|
37
37
|
) { [weak self] _ in self?.presentIfRinging() }
|
|
38
38
|
}
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
deinit {
|
|
41
41
|
if let activeObserver { NotificationCenter.default.removeObserver(activeObserver) }
|
|
42
42
|
}
|
|
43
|
-
|
|
43
|
+
|
|
44
44
|
/// Shows the ring screen when a backend says an alarm is ringing right now.
|
|
45
45
|
private func presentIfRinging() {
|
|
46
46
|
var ringing = NotificationBackend.shared.ringing
|
|
47
|
-
|
|
47
|
+
#if canImport(AlarmKit)
|
|
48
48
|
if #available(iOS 26.0, *), ringing == nil { ringing = AlarmKitBackend.shared.ringingAlarm() }
|
|
49
|
-
|
|
50
|
-
if let ringing { AlarmRingPresenter.present(ringing, bridge: bridge
|
|
49
|
+
#endif
|
|
50
|
+
if let ringing { AlarmRingPresenter.present(ringing, bridge: bridge,
|
|
51
|
+
componentName: ringing.componentName ?? "AlarmRing",
|
|
52
|
+
componentProps: ringing.componentProps ?? [:]) }
|
|
51
53
|
}
|
|
52
|
-
|
|
54
|
+
|
|
53
55
|
private func handle(_ name: String, _ alarm: AlarmData) {
|
|
54
56
|
if name == Self.eventFired {
|
|
55
|
-
AlarmRingPresenter.present(alarm, bridge: bridge
|
|
57
|
+
AlarmRingPresenter.present(alarm, bridge: bridge,
|
|
58
|
+
componentName: alarm.componentName ?? "AlarmRing",
|
|
59
|
+
componentProps: alarm.componentProps ?? [:])
|
|
56
60
|
} else {
|
|
57
61
|
AlarmRingPresenter.dismiss(alarm.id)
|
|
58
62
|
}
|
|
59
63
|
emit(name, alarm)
|
|
60
64
|
}
|
|
61
|
-
|
|
65
|
+
|
|
62
66
|
@objc override static func requiresMainQueueSetup() -> Bool { false }
|
|
63
|
-
|
|
67
|
+
|
|
64
68
|
// MARK: - Events
|
|
65
|
-
|
|
69
|
+
|
|
66
70
|
override func supportedEvents() -> [String]! {
|
|
67
71
|
[Self.eventFired, Self.eventDismissed, Self.eventSnoozed]
|
|
68
72
|
}
|
|
69
|
-
|
|
73
|
+
|
|
70
74
|
override func startObserving() { hasListeners = true }
|
|
71
75
|
override func stopObserving() { hasListeners = false }
|
|
72
|
-
|
|
76
|
+
|
|
73
77
|
private func emit(_ name: String, _ a: AlarmData) {
|
|
74
78
|
guard hasListeners else { return }
|
|
75
79
|
sendEvent(withName: name, body: [
|
|
@@ -80,38 +84,38 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
80
84
|
"data": a.data,
|
|
81
85
|
])
|
|
82
86
|
}
|
|
83
|
-
|
|
87
|
+
|
|
84
88
|
// MARK: - Backend selection
|
|
85
|
-
|
|
89
|
+
|
|
86
90
|
private var useAlarmKit: Bool {
|
|
87
|
-
|
|
91
|
+
#if canImport(AlarmKit)
|
|
88
92
|
if #available(iOS 26.0, *) { return AlarmKitBackend.shared.isAuthorized }
|
|
89
|
-
|
|
93
|
+
#endif
|
|
90
94
|
return false
|
|
91
95
|
}
|
|
92
|
-
|
|
96
|
+
|
|
93
97
|
private func cancelNative(_ id: String) {
|
|
94
98
|
AlarmRingPresenter.dismiss(id)
|
|
95
99
|
NotificationBackend.shared.cancel(id)
|
|
96
|
-
|
|
100
|
+
#if canImport(AlarmKit)
|
|
97
101
|
if #available(iOS 26.0, *) { AlarmKitBackend.shared.cancel(id) }
|
|
98
|
-
|
|
102
|
+
#endif
|
|
99
103
|
}
|
|
100
|
-
|
|
104
|
+
|
|
101
105
|
// MARK: - Permissions
|
|
102
|
-
|
|
106
|
+
|
|
103
107
|
/// Same keys as Android's `permissionMap()`. The Android-only flags are always
|
|
104
108
|
/// true here so shared UI code can treat them as granted.
|
|
105
109
|
private func permissionMap(_ done: @escaping ([String: Any]) -> Void) {
|
|
106
110
|
NotificationBackend.shared.status { notifications in
|
|
107
111
|
var status = notifications
|
|
108
112
|
var backend = "notifications"
|
|
109
|
-
|
|
113
|
+
#if canImport(AlarmKit)
|
|
110
114
|
if #available(iOS 26.0, *) {
|
|
111
115
|
status = AlarmKitBackend.shared.status
|
|
112
116
|
backend = "alarmkit"
|
|
113
117
|
}
|
|
114
|
-
|
|
118
|
+
#endif
|
|
115
119
|
done([
|
|
116
120
|
"status": status,
|
|
117
121
|
"notifications": status == "granted" || notifications == "granted",
|
|
@@ -122,15 +126,15 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
122
126
|
])
|
|
123
127
|
}
|
|
124
128
|
}
|
|
125
|
-
|
|
129
|
+
|
|
126
130
|
private func notificationStatus() async -> String {
|
|
127
131
|
await withCheckedContinuation { c in NotificationBackend.shared.status { c.resume(returning: $0) } }
|
|
128
132
|
}
|
|
129
|
-
|
|
133
|
+
|
|
130
134
|
private func requestNotifications() async {
|
|
131
135
|
_ = await withCheckedContinuation { c in NotificationBackend.shared.request { c.resume(returning: $0) } }
|
|
132
136
|
}
|
|
133
|
-
|
|
137
|
+
|
|
134
138
|
/// iOS never re-prompts after a denial, so (like Android for its special
|
|
135
139
|
/// permissions) send the user to the app's Settings page.
|
|
136
140
|
@MainActor private func openSettings() {
|
|
@@ -138,16 +142,16 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
138
142
|
UIApplication.shared.open(url)
|
|
139
143
|
}
|
|
140
144
|
}
|
|
141
|
-
|
|
145
|
+
|
|
142
146
|
@objc(checkPermission:reject:)
|
|
143
147
|
func checkPermission(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
144
148
|
permissionMap { resolve($0) }
|
|
145
149
|
}
|
|
146
|
-
|
|
150
|
+
|
|
147
151
|
@objc(requestPermission:reject:)
|
|
148
152
|
func requestPermission(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
149
153
|
Task {
|
|
150
|
-
|
|
154
|
+
#if canImport(AlarmKit)
|
|
151
155
|
if #available(iOS 26.0, *) {
|
|
152
156
|
let kit = AlarmKitBackend.shared
|
|
153
157
|
if kit.status == "notDetermined" { _ = await kit.requestAuthorization() }
|
|
@@ -157,7 +161,7 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
157
161
|
}
|
|
158
162
|
await self.openSettings()
|
|
159
163
|
}
|
|
160
|
-
|
|
164
|
+
#endif
|
|
161
165
|
// Notifications back the fallback path.
|
|
162
166
|
let notifications = await self.notificationStatus()
|
|
163
167
|
if notifications == "notDetermined" {
|
|
@@ -168,42 +172,42 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
168
172
|
self.permissionMap { resolve($0) }
|
|
169
173
|
}
|
|
170
174
|
}
|
|
171
|
-
|
|
175
|
+
|
|
172
176
|
@objc(canScheduleExactAlarms:reject:)
|
|
173
177
|
func canScheduleExactAlarms(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
174
178
|
permissionMap { (map: [String: Any]) in
|
|
175
179
|
resolve((map["canScheduleExact"] as? Bool) ?? false)
|
|
176
180
|
}
|
|
177
181
|
}
|
|
178
|
-
|
|
182
|
+
|
|
179
183
|
/// Android OEM concept; iOS has no such screen.
|
|
180
184
|
@objc(openAutoStartSettings:reject:)
|
|
181
185
|
func openAutoStartSettings(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
182
186
|
resolve(false)
|
|
183
187
|
}
|
|
184
|
-
|
|
188
|
+
|
|
185
189
|
// MARK: - Scheduling
|
|
186
|
-
|
|
190
|
+
|
|
187
191
|
@objc(scheduleAlarm:resolve:reject:)
|
|
188
192
|
func scheduleAlarm(_ config: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
189
193
|
let dict = config as? [String: Any] ?? [:]
|
|
190
194
|
let id = (dict["alarmId"] as? String) ?? UUID().uuidString
|
|
191
195
|
arm(AlarmData.from(config: dict, id: id), code: "E_SCHEDULE", reject: reject) { resolve(id) }
|
|
192
196
|
}
|
|
193
|
-
|
|
197
|
+
|
|
194
198
|
@objc(updateAlarm:config:resolve:reject:)
|
|
195
199
|
func updateAlarm(_ alarmId: String, config: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
196
200
|
guard AlarmStore.get(alarmId) != nil else { return resolve(false) }
|
|
197
201
|
let dict = config as? [String: Any] ?? [:]
|
|
198
202
|
arm(AlarmData.from(config: dict, id: alarmId), code: "E_UPDATE", reject: reject) { resolve(true) }
|
|
199
203
|
}
|
|
200
|
-
|
|
204
|
+
|
|
201
205
|
/// Cancel → persist → schedule, the same sequence as Android.
|
|
202
206
|
private func arm(_ alarm: AlarmData, code: String, reject: @escaping RCTPromiseRejectBlock, done: @escaping () -> Void) {
|
|
203
207
|
cancelNative(alarm.id)
|
|
204
208
|
AlarmStore.save(alarm)
|
|
205
|
-
|
|
206
|
-
|
|
209
|
+
|
|
210
|
+
#if canImport(AlarmKit)
|
|
207
211
|
if #available(iOS 26.0, *), useAlarmKit {
|
|
208
212
|
Task {
|
|
209
213
|
do {
|
|
@@ -215,8 +219,8 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
215
219
|
}
|
|
216
220
|
return
|
|
217
221
|
}
|
|
218
|
-
|
|
219
|
-
|
|
222
|
+
#endif
|
|
223
|
+
|
|
220
224
|
NotificationBackend.shared.status { status in
|
|
221
225
|
guard status == "granted" else {
|
|
222
226
|
// Mirrors Android's SecurityException when the exact-alarm permission is missing.
|
|
@@ -227,7 +231,7 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
227
231
|
}
|
|
228
232
|
}
|
|
229
233
|
}
|
|
230
|
-
|
|
234
|
+
|
|
231
235
|
@objc(cancelAlarm:resolve:reject:)
|
|
232
236
|
func cancelAlarm(_ alarmId: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
233
237
|
let existed = AlarmStore.get(alarmId) != nil
|
|
@@ -235,31 +239,31 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
235
239
|
AlarmStore.remove(alarmId)
|
|
236
240
|
resolve(existed)
|
|
237
241
|
}
|
|
238
|
-
|
|
242
|
+
|
|
239
243
|
@objc(cancelAllAlarms:reject:)
|
|
240
244
|
func cancelAllAlarms(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
241
245
|
AlarmRingPresenter.dismiss()
|
|
242
246
|
AlarmStore.all().forEach { cancelNative($0.id) }
|
|
243
247
|
NotificationBackend.shared.cancelAll()
|
|
244
|
-
|
|
248
|
+
#if canImport(AlarmKit)
|
|
245
249
|
if #available(iOS 26.0, *) { AlarmKitBackend.shared.cancelAll() }
|
|
246
|
-
|
|
250
|
+
#endif
|
|
247
251
|
AlarmStore.clear()
|
|
248
252
|
resolve(nil)
|
|
249
253
|
}
|
|
250
|
-
|
|
254
|
+
|
|
251
255
|
// MARK: - Control (while ringing)
|
|
252
|
-
|
|
256
|
+
|
|
253
257
|
@objc(stopAlarm:resolve:reject:)
|
|
254
258
|
func stopAlarm(_ alarmId: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
255
259
|
AlarmRingPresenter.dismiss(alarmId)
|
|
256
260
|
NotificationBackend.shared.stop(alarmId)
|
|
257
|
-
|
|
261
|
+
#if canImport(AlarmKit)
|
|
258
262
|
if #available(iOS 26.0, *) { AlarmKitBackend.shared.stop(alarmId) }
|
|
259
|
-
|
|
263
|
+
#endif
|
|
260
264
|
resolve(nil)
|
|
261
265
|
}
|
|
262
|
-
|
|
266
|
+
|
|
263
267
|
@objc(snoozeAlarm:minutes:resolve:reject:)
|
|
264
268
|
func snoozeAlarm(_ alarmId: String, minutes: Double, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
265
269
|
guard let alarm = AlarmStore.get(alarmId) else {
|
|
@@ -267,8 +271,8 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
267
271
|
}
|
|
268
272
|
let mins = minutes > 0 ? Int(minutes) : alarm.snoozeMinutes
|
|
269
273
|
AlarmRingPresenter.dismiss(alarmId)
|
|
270
|
-
|
|
271
|
-
|
|
274
|
+
|
|
275
|
+
#if canImport(AlarmKit)
|
|
272
276
|
if #available(iOS 26.0, *), useAlarmKit {
|
|
273
277
|
Task {
|
|
274
278
|
do {
|
|
@@ -280,36 +284,36 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
280
284
|
}
|
|
281
285
|
return
|
|
282
286
|
}
|
|
283
|
-
|
|
284
|
-
|
|
287
|
+
#endif
|
|
288
|
+
|
|
285
289
|
let wasRinging = NotificationBackend.shared.ringing?.id == alarmId
|
|
286
290
|
NotificationBackend.shared.snooze(alarm, minutes: mins)
|
|
287
291
|
if wasRinging { emit(Self.eventSnoozed, alarm) }
|
|
288
292
|
resolve(nil)
|
|
289
293
|
}
|
|
290
|
-
|
|
294
|
+
|
|
291
295
|
/// The alarm currently ringing (if any), so JS can show the ring UI on launch.
|
|
292
296
|
@objc(getRingingAlarm:reject:)
|
|
293
297
|
func getRingingAlarm(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
294
298
|
var ringing = NotificationBackend.shared.ringing
|
|
295
|
-
|
|
299
|
+
#if canImport(AlarmKit)
|
|
296
300
|
if #available(iOS 26.0, *), ringing == nil { ringing = AlarmKitBackend.shared.ringingAlarm() }
|
|
297
|
-
|
|
301
|
+
#endif
|
|
298
302
|
resolve(ringing?.toDictionary())
|
|
299
303
|
}
|
|
300
|
-
|
|
304
|
+
|
|
301
305
|
// MARK: - Queries
|
|
302
|
-
|
|
306
|
+
|
|
303
307
|
@objc(getAlarm:resolve:reject:)
|
|
304
308
|
func getAlarm(_ alarmId: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
305
309
|
resolve(AlarmStore.get(alarmId)?.toDictionary())
|
|
306
310
|
}
|
|
307
|
-
|
|
311
|
+
|
|
308
312
|
@objc(getAllAlarms:reject:)
|
|
309
313
|
func getAllAlarms(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
310
314
|
resolve(AlarmStore.all().sorted { $0.timestamp < $1.timestamp }.map { $0.toDictionary() })
|
|
311
315
|
}
|
|
312
|
-
|
|
316
|
+
|
|
313
317
|
@objc(isScheduled:resolve:reject:)
|
|
314
318
|
func isScheduled(_ alarmId: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
315
319
|
let a = AlarmStore.get(alarmId)
|