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.
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +0 -0
- package/AGENTS.md +291 -0
- package/AlarmService.d.ts +141 -32
- package/AlarmService.ts +238 -39
- package/LICENSE.md +21 -0
- package/README.md +159 -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/commitlint.config.mjs +1 -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 +5 -2
- package/specs/NativeAlarm.ts +120 -71
- package/src/assets/recording.mp4 +0 -0
- package/src/assets/sample-1.png +0 -0
- package/src/assets/sample-2.png +0 -0
- package/src/assets/sample-3.png +0 -0
- package/src/screens/index.ts +0 -0
- package/src/types/index.ts +246 -0
- package/src/utils/AlarmRegistry.ts +57 -0
- package/src/utils/index.ts +1 -0
|
@@ -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)
|
|
@@ -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)
|