react-native-alarms 0.0.6 → 0.0.8
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.ts +289 -89
- package/README.md +186 -12
- package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmData.kt +160 -8
- package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmModule.kt +3 -2
- package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmRingActivity.kt +3 -2
- package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmService.kt +55 -1
- package/android/local.properties +8 -0
- package/android-lib/src/main/AndroidManifest.xml +1 -0
- package/index.ts +7 -5
- package/ios/.xcode.env.local +1 -1
- package/ios/ReactNativeAlarm/Info.plist +2 -2
- package/ios/ReactNativeAlarm.xcodeproj/project.pbxproj +8 -0
- package/ios/alarm/AlarmData.swift +23 -6
- package/ios/alarm/AlarmKitBackend.swift +43 -40
- package/ios/alarm/AlarmModule.swift +69 -64
- package/ios/alarm/AlarmRingPresenter.swift +26 -19
- package/ios/alarm/AlarmStore.swift +9 -9
- package/ios/alarm/NotificationBackend.swift +113 -64
- package/package.json +4 -4
- package/specs/NativeAlarm.ts +127 -74
- package/src/types/index.ts +266 -0
- package/src/utils/AlarmRegistry.ts +57 -0
- package/src/utils/index.ts +1 -0
- package/.bundle/config +0 -2
- package/.husky/commit-msg +0 -1
- package/.husky/pre-commit +0 -0
- package/AlarmService.d.ts +0 -117
- package/commitlint.config.mjs +0 -1
- package/index.d.ts +0 -20
- 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
|
@@ -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: [
|
|
@@ -77,41 +81,42 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
77
81
|
"timestamp": AlarmData.now(),
|
|
78
82
|
"title": a.title,
|
|
79
83
|
"body": a.body,
|
|
84
|
+
"image": a.image ?? "",
|
|
80
85
|
"data": a.data,
|
|
81
86
|
])
|
|
82
87
|
}
|
|
83
|
-
|
|
88
|
+
|
|
84
89
|
// MARK: - Backend selection
|
|
85
|
-
|
|
90
|
+
|
|
86
91
|
private var useAlarmKit: Bool {
|
|
87
|
-
|
|
92
|
+
#if canImport(AlarmKit)
|
|
88
93
|
if #available(iOS 26.0, *) { return AlarmKitBackend.shared.isAuthorized }
|
|
89
|
-
|
|
94
|
+
#endif
|
|
90
95
|
return false
|
|
91
96
|
}
|
|
92
|
-
|
|
97
|
+
|
|
93
98
|
private func cancelNative(_ id: String) {
|
|
94
99
|
AlarmRingPresenter.dismiss(id)
|
|
95
100
|
NotificationBackend.shared.cancel(id)
|
|
96
|
-
|
|
101
|
+
#if canImport(AlarmKit)
|
|
97
102
|
if #available(iOS 26.0, *) { AlarmKitBackend.shared.cancel(id) }
|
|
98
|
-
|
|
103
|
+
#endif
|
|
99
104
|
}
|
|
100
|
-
|
|
105
|
+
|
|
101
106
|
// MARK: - Permissions
|
|
102
|
-
|
|
107
|
+
|
|
103
108
|
/// Same keys as Android's `permissionMap()`. The Android-only flags are always
|
|
104
109
|
/// true here so shared UI code can treat them as granted.
|
|
105
110
|
private func permissionMap(_ done: @escaping ([String: Any]) -> Void) {
|
|
106
111
|
NotificationBackend.shared.status { notifications in
|
|
107
112
|
var status = notifications
|
|
108
113
|
var backend = "notifications"
|
|
109
|
-
|
|
114
|
+
#if canImport(AlarmKit)
|
|
110
115
|
if #available(iOS 26.0, *) {
|
|
111
116
|
status = AlarmKitBackend.shared.status
|
|
112
117
|
backend = "alarmkit"
|
|
113
118
|
}
|
|
114
|
-
|
|
119
|
+
#endif
|
|
115
120
|
done([
|
|
116
121
|
"status": status,
|
|
117
122
|
"notifications": status == "granted" || notifications == "granted",
|
|
@@ -122,15 +127,15 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
122
127
|
])
|
|
123
128
|
}
|
|
124
129
|
}
|
|
125
|
-
|
|
130
|
+
|
|
126
131
|
private func notificationStatus() async -> String {
|
|
127
132
|
await withCheckedContinuation { c in NotificationBackend.shared.status { c.resume(returning: $0) } }
|
|
128
133
|
}
|
|
129
|
-
|
|
134
|
+
|
|
130
135
|
private func requestNotifications() async {
|
|
131
136
|
_ = await withCheckedContinuation { c in NotificationBackend.shared.request { c.resume(returning: $0) } }
|
|
132
137
|
}
|
|
133
|
-
|
|
138
|
+
|
|
134
139
|
/// iOS never re-prompts after a denial, so (like Android for its special
|
|
135
140
|
/// permissions) send the user to the app's Settings page.
|
|
136
141
|
@MainActor private func openSettings() {
|
|
@@ -138,16 +143,16 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
138
143
|
UIApplication.shared.open(url)
|
|
139
144
|
}
|
|
140
145
|
}
|
|
141
|
-
|
|
146
|
+
|
|
142
147
|
@objc(checkPermission:reject:)
|
|
143
148
|
func checkPermission(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
144
149
|
permissionMap { resolve($0) }
|
|
145
150
|
}
|
|
146
|
-
|
|
151
|
+
|
|
147
152
|
@objc(requestPermission:reject:)
|
|
148
153
|
func requestPermission(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
149
154
|
Task {
|
|
150
|
-
|
|
155
|
+
#if canImport(AlarmKit)
|
|
151
156
|
if #available(iOS 26.0, *) {
|
|
152
157
|
let kit = AlarmKitBackend.shared
|
|
153
158
|
if kit.status == "notDetermined" { _ = await kit.requestAuthorization() }
|
|
@@ -157,7 +162,7 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
157
162
|
}
|
|
158
163
|
await self.openSettings()
|
|
159
164
|
}
|
|
160
|
-
|
|
165
|
+
#endif
|
|
161
166
|
// Notifications back the fallback path.
|
|
162
167
|
let notifications = await self.notificationStatus()
|
|
163
168
|
if notifications == "notDetermined" {
|
|
@@ -168,42 +173,42 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
168
173
|
self.permissionMap { resolve($0) }
|
|
169
174
|
}
|
|
170
175
|
}
|
|
171
|
-
|
|
176
|
+
|
|
172
177
|
@objc(canScheduleExactAlarms:reject:)
|
|
173
178
|
func canScheduleExactAlarms(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
174
179
|
permissionMap { (map: [String: Any]) in
|
|
175
180
|
resolve((map["canScheduleExact"] as? Bool) ?? false)
|
|
176
181
|
}
|
|
177
182
|
}
|
|
178
|
-
|
|
183
|
+
|
|
179
184
|
/// Android OEM concept; iOS has no such screen.
|
|
180
185
|
@objc(openAutoStartSettings:reject:)
|
|
181
186
|
func openAutoStartSettings(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
182
187
|
resolve(false)
|
|
183
188
|
}
|
|
184
|
-
|
|
189
|
+
|
|
185
190
|
// MARK: - Scheduling
|
|
186
|
-
|
|
191
|
+
|
|
187
192
|
@objc(scheduleAlarm:resolve:reject:)
|
|
188
193
|
func scheduleAlarm(_ config: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
189
194
|
let dict = config as? [String: Any] ?? [:]
|
|
190
195
|
let id = (dict["alarmId"] as? String) ?? UUID().uuidString
|
|
191
196
|
arm(AlarmData.from(config: dict, id: id), code: "E_SCHEDULE", reject: reject) { resolve(id) }
|
|
192
197
|
}
|
|
193
|
-
|
|
198
|
+
|
|
194
199
|
@objc(updateAlarm:config:resolve:reject:)
|
|
195
200
|
func updateAlarm(_ alarmId: String, config: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
196
201
|
guard AlarmStore.get(alarmId) != nil else { return resolve(false) }
|
|
197
202
|
let dict = config as? [String: Any] ?? [:]
|
|
198
203
|
arm(AlarmData.from(config: dict, id: alarmId), code: "E_UPDATE", reject: reject) { resolve(true) }
|
|
199
204
|
}
|
|
200
|
-
|
|
205
|
+
|
|
201
206
|
/// Cancel → persist → schedule, the same sequence as Android.
|
|
202
207
|
private func arm(_ alarm: AlarmData, code: String, reject: @escaping RCTPromiseRejectBlock, done: @escaping () -> Void) {
|
|
203
208
|
cancelNative(alarm.id)
|
|
204
209
|
AlarmStore.save(alarm)
|
|
205
|
-
|
|
206
|
-
|
|
210
|
+
|
|
211
|
+
#if canImport(AlarmKit)
|
|
207
212
|
if #available(iOS 26.0, *), useAlarmKit {
|
|
208
213
|
Task {
|
|
209
214
|
do {
|
|
@@ -215,8 +220,8 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
215
220
|
}
|
|
216
221
|
return
|
|
217
222
|
}
|
|
218
|
-
|
|
219
|
-
|
|
223
|
+
#endif
|
|
224
|
+
|
|
220
225
|
NotificationBackend.shared.status { status in
|
|
221
226
|
guard status == "granted" else {
|
|
222
227
|
// Mirrors Android's SecurityException when the exact-alarm permission is missing.
|
|
@@ -227,7 +232,7 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
227
232
|
}
|
|
228
233
|
}
|
|
229
234
|
}
|
|
230
|
-
|
|
235
|
+
|
|
231
236
|
@objc(cancelAlarm:resolve:reject:)
|
|
232
237
|
func cancelAlarm(_ alarmId: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
233
238
|
let existed = AlarmStore.get(alarmId) != nil
|
|
@@ -235,31 +240,31 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
235
240
|
AlarmStore.remove(alarmId)
|
|
236
241
|
resolve(existed)
|
|
237
242
|
}
|
|
238
|
-
|
|
243
|
+
|
|
239
244
|
@objc(cancelAllAlarms:reject:)
|
|
240
245
|
func cancelAllAlarms(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
241
246
|
AlarmRingPresenter.dismiss()
|
|
242
247
|
AlarmStore.all().forEach { cancelNative($0.id) }
|
|
243
248
|
NotificationBackend.shared.cancelAll()
|
|
244
|
-
|
|
249
|
+
#if canImport(AlarmKit)
|
|
245
250
|
if #available(iOS 26.0, *) { AlarmKitBackend.shared.cancelAll() }
|
|
246
|
-
|
|
251
|
+
#endif
|
|
247
252
|
AlarmStore.clear()
|
|
248
253
|
resolve(nil)
|
|
249
254
|
}
|
|
250
|
-
|
|
255
|
+
|
|
251
256
|
// MARK: - Control (while ringing)
|
|
252
|
-
|
|
257
|
+
|
|
253
258
|
@objc(stopAlarm:resolve:reject:)
|
|
254
259
|
func stopAlarm(_ alarmId: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
255
260
|
AlarmRingPresenter.dismiss(alarmId)
|
|
256
261
|
NotificationBackend.shared.stop(alarmId)
|
|
257
|
-
|
|
262
|
+
#if canImport(AlarmKit)
|
|
258
263
|
if #available(iOS 26.0, *) { AlarmKitBackend.shared.stop(alarmId) }
|
|
259
|
-
|
|
264
|
+
#endif
|
|
260
265
|
resolve(nil)
|
|
261
266
|
}
|
|
262
|
-
|
|
267
|
+
|
|
263
268
|
@objc(snoozeAlarm:minutes:resolve:reject:)
|
|
264
269
|
func snoozeAlarm(_ alarmId: String, minutes: Double, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
265
270
|
guard let alarm = AlarmStore.get(alarmId) else {
|
|
@@ -267,8 +272,8 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
267
272
|
}
|
|
268
273
|
let mins = minutes > 0 ? Int(minutes) : alarm.snoozeMinutes
|
|
269
274
|
AlarmRingPresenter.dismiss(alarmId)
|
|
270
|
-
|
|
271
|
-
|
|
275
|
+
|
|
276
|
+
#if canImport(AlarmKit)
|
|
272
277
|
if #available(iOS 26.0, *), useAlarmKit {
|
|
273
278
|
Task {
|
|
274
279
|
do {
|
|
@@ -280,36 +285,36 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
280
285
|
}
|
|
281
286
|
return
|
|
282
287
|
}
|
|
283
|
-
|
|
284
|
-
|
|
288
|
+
#endif
|
|
289
|
+
|
|
285
290
|
let wasRinging = NotificationBackend.shared.ringing?.id == alarmId
|
|
286
291
|
NotificationBackend.shared.snooze(alarm, minutes: mins)
|
|
287
292
|
if wasRinging { emit(Self.eventSnoozed, alarm) }
|
|
288
293
|
resolve(nil)
|
|
289
294
|
}
|
|
290
|
-
|
|
295
|
+
|
|
291
296
|
/// The alarm currently ringing (if any), so JS can show the ring UI on launch.
|
|
292
297
|
@objc(getRingingAlarm:reject:)
|
|
293
298
|
func getRingingAlarm(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
294
299
|
var ringing = NotificationBackend.shared.ringing
|
|
295
|
-
|
|
300
|
+
#if canImport(AlarmKit)
|
|
296
301
|
if #available(iOS 26.0, *), ringing == nil { ringing = AlarmKitBackend.shared.ringingAlarm() }
|
|
297
|
-
|
|
302
|
+
#endif
|
|
298
303
|
resolve(ringing?.toDictionary())
|
|
299
304
|
}
|
|
300
|
-
|
|
305
|
+
|
|
301
306
|
// MARK: - Queries
|
|
302
|
-
|
|
307
|
+
|
|
303
308
|
@objc(getAlarm:resolve:reject:)
|
|
304
309
|
func getAlarm(_ alarmId: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
305
310
|
resolve(AlarmStore.get(alarmId)?.toDictionary())
|
|
306
311
|
}
|
|
307
|
-
|
|
312
|
+
|
|
308
313
|
@objc(getAllAlarms:reject:)
|
|
309
314
|
func getAllAlarms(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
310
315
|
resolve(AlarmStore.all().sorted { $0.timestamp < $1.timestamp }.map { $0.toDictionary() })
|
|
311
316
|
}
|
|
312
|
-
|
|
317
|
+
|
|
313
318
|
@objc(isScheduled:resolve:reject:)
|
|
314
319
|
func isScheduled(_ alarmId: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
315
320
|
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)
|