react-native-alarms 0.0.9 → 0.0.10
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/README.md +37 -1
- package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmModule.kt +30 -8
- package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmService.kt +50 -9
- package/ios/alarm/AlarmData.swift +17 -0
- package/ios/alarm/AlarmKitBackend.swift +6 -2
- package/ios/alarm/AlarmModule.swift +53 -9
- package/ios/alarm/NotificationBackend.swift +65 -9
- package/package.json +1 -1
- package/src/types/index.ts +4 -2
package/README.md
CHANGED
|
@@ -193,6 +193,20 @@ unsubscribeDismissed();
|
|
|
193
193
|
unsubscribeSnoozed();
|
|
194
194
|
```
|
|
195
195
|
|
|
196
|
+
Alarm events are delivered to JavaScript only while the native module and JS
|
|
197
|
+
runtime are active; they are not a durable event queue. On app startup, query
|
|
198
|
+
`getRingingAlarm()` to recover an alarm that started while the app was closed:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
const ringingAlarm = await AlarmService.getRingingAlarm();
|
|
202
|
+
if (ringingAlarm) {
|
|
203
|
+
// Restore any app-level state needed for the currently ringing alarm.
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Register event listeners as part of app initialization to receive subsequent
|
|
208
|
+
transitions. Native ring-screen presentation is independent of JS event delivery.
|
|
209
|
+
|
|
196
210
|
### Managing Alarms
|
|
197
211
|
|
|
198
212
|
```typescript
|
|
@@ -240,7 +254,7 @@ type AlarmConfig = {
|
|
|
240
254
|
title: string; // Alarm title/label
|
|
241
255
|
body?: string; // Alarm description
|
|
242
256
|
image?: string; // file:// or https:// image URI for ring screen/notifications
|
|
243
|
-
sound?: string; // 'default' or
|
|
257
|
+
sound?: string; // 'default' or a bundled sound filename, e.g. 'bell.wav'
|
|
244
258
|
vibrate?: boolean; // Enable vibration (Android only)
|
|
245
259
|
snoozeEnabled?: boolean; // Allow snoozing
|
|
246
260
|
snoozeMinutes?: number; // Snooze duration in minutes
|
|
@@ -278,6 +292,28 @@ AlarmKit system alert itself does not display custom images, but the image URI i
|
|
|
278
292
|
available to the React Native ring screen when it opens. Render it in that screen
|
|
279
293
|
with React Native's `Image` component and `source={{ uri: image }}`.
|
|
280
294
|
|
|
295
|
+
#### Custom alarm sound
|
|
296
|
+
|
|
297
|
+
Set `sound` to the same bundled filename on both platforms. For example, with
|
|
298
|
+
`sound: 'bell.wav'`, include `bell.wav` in the iOS app target's Copy Bundle
|
|
299
|
+
Resources and add it to Android at
|
|
300
|
+
`android/app/src/main/res/raw/bell.wav` (create the `raw` directory if needed).
|
|
301
|
+
Android raw resource filenames must be lowercase and use only letters, numbers,
|
|
302
|
+
and underscores. iOS notification sounds must use a supported audio format and
|
|
303
|
+
meet Apple's notification sound duration and size limits.
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
await AlarmService.scheduleAlarm({
|
|
307
|
+
timestamp: Date.now() + 60_000,
|
|
308
|
+
title: 'Wake up',
|
|
309
|
+
sound: 'bell.wav',
|
|
310
|
+
});
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
The alarm uses the custom sound when the file is bundled for that platform.
|
|
314
|
+
Otherwise, including when `sound` is omitted or set to `'default'`, it uses the
|
|
315
|
+
platform's default alarm sound.
|
|
316
|
+
|
|
281
317
|
> **How the native layer stores these:** `AlarmConfig.presentation` is resolved by `AlarmService` before being handed off to the native module. The resulting fields stored in the native `AlarmData` model are:
|
|
282
318
|
>
|
|
283
319
|
> | Field | Type | Default | Description |
|
|
@@ -138,9 +138,7 @@ class AlarmModule(private val ctx: ReactApplicationContext) : ReactContextBaseJa
|
|
|
138
138
|
val id = if (config.hasKey("alarmId") && !config.isNull("alarmId"))
|
|
139
139
|
config.getString("alarmId")!! else UUID.randomUUID().toString()
|
|
140
140
|
val alarm = AlarmData.fromMap(config, id)
|
|
141
|
-
|
|
142
|
-
AlarmStore.save(ctx, alarm)
|
|
143
|
-
AlarmScheduler.schedule(ctx, alarm)
|
|
141
|
+
scheduleReplacing(alarm)
|
|
144
142
|
promise.resolve(id)
|
|
145
143
|
} catch (e: Exception) {
|
|
146
144
|
promise.reject("E_SCHEDULE", e.message, e)
|
|
@@ -152,15 +150,39 @@ class AlarmModule(private val ctx: ReactApplicationContext) : ReactContextBaseJa
|
|
|
152
150
|
try {
|
|
153
151
|
if (AlarmStore.get(ctx, alarmId) == null) return promise.resolve(false)
|
|
154
152
|
val alarm = AlarmData.fromMap(config, alarmId)
|
|
155
|
-
|
|
156
|
-
AlarmStore.save(ctx, alarm)
|
|
157
|
-
AlarmScheduler.schedule(ctx, alarm)
|
|
153
|
+
scheduleReplacing(alarm)
|
|
158
154
|
promise.resolve(true)
|
|
159
155
|
} catch (e: Exception) {
|
|
160
156
|
promise.reject("E_UPDATE", e.message, e)
|
|
161
157
|
}
|
|
162
158
|
}
|
|
163
159
|
|
|
160
|
+
private fun scheduleReplacing(alarm: AlarmData) {
|
|
161
|
+
val previous = AlarmStore.get(ctx, alarm.id)
|
|
162
|
+
AlarmScheduler.cancel(ctx, alarm.id)
|
|
163
|
+
try {
|
|
164
|
+
AlarmStore.save(ctx, alarm)
|
|
165
|
+
AlarmScheduler.schedule(ctx, alarm)
|
|
166
|
+
} catch (error: Exception) {
|
|
167
|
+
AlarmScheduler.cancel(ctx, alarm.id)
|
|
168
|
+
if (previous == null) {
|
|
169
|
+
AlarmStore.remove(ctx, alarm.id)
|
|
170
|
+
} else {
|
|
171
|
+
AlarmStore.save(ctx, previous)
|
|
172
|
+
try {
|
|
173
|
+
AlarmScheduler.schedule(ctx, previous)
|
|
174
|
+
} catch (restoreError: Exception) {
|
|
175
|
+
android.util.Log.e(
|
|
176
|
+
"AlarmModule",
|
|
177
|
+
"Could not restore alarm ${alarm.id} after scheduling failed",
|
|
178
|
+
restoreError
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
throw error
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
164
186
|
@ReactMethod
|
|
165
187
|
fun cancelAlarm(alarmId: String, promise: Promise) {
|
|
166
188
|
val existed = AlarmStore.get(ctx, alarmId) != null
|
|
@@ -182,14 +204,14 @@ class AlarmModule(private val ctx: ReactApplicationContext) : ReactContextBaseJa
|
|
|
182
204
|
|
|
183
205
|
@ReactMethod
|
|
184
206
|
fun stopAlarm(alarmId: String, promise: Promise) {
|
|
185
|
-
AlarmService.dismiss(ctx)
|
|
207
|
+
if (AlarmService.current?.id == alarmId) AlarmService.dismiss(ctx)
|
|
186
208
|
promise.resolve(null)
|
|
187
209
|
}
|
|
188
210
|
|
|
189
211
|
@ReactMethod
|
|
190
212
|
fun snoozeAlarm(alarmId: String, minutes: Double, promise: Promise) {
|
|
191
213
|
val ringing = AlarmService.current
|
|
192
|
-
if (ringing
|
|
214
|
+
if (ringing?.id == alarmId) {
|
|
193
215
|
AlarmService.snooze(ctx, minutes.toInt())
|
|
194
216
|
return promise.resolve(null)
|
|
195
217
|
}
|
|
@@ -153,13 +153,55 @@ class AlarmService : Service() {
|
|
|
153
153
|
// ---- Sound ----
|
|
154
154
|
|
|
155
155
|
private fun startSound(alarm: AlarmData) {
|
|
156
|
-
val
|
|
157
|
-
|
|
156
|
+
val customUri = customSoundUri(alarm.sound)
|
|
157
|
+
if (customUri != null) {
|
|
158
|
+
try {
|
|
159
|
+
playSound(customUri)
|
|
160
|
+
return
|
|
161
|
+
} catch (e: Exception) {
|
|
162
|
+
Log.w(TAG, "Could not play custom alarm sound $customUri; using the default sound", e)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
val defaultUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)
|
|
158
167
|
?: RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
|
168
|
+
if (defaultUri == null) {
|
|
169
|
+
Log.e(TAG, "No default alarm or ringtone sound is available")
|
|
170
|
+
startVibration()
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
playSound(defaultUri)
|
|
175
|
+
} catch (e: Exception) {
|
|
176
|
+
Log.e(TAG, "Could not play default alarm sound $defaultUri", e)
|
|
177
|
+
startVibration() // last resort: at least wake the user somehow
|
|
178
|
+
}
|
|
179
|
+
}
|
|
159
180
|
|
|
181
|
+
private fun customSoundUri(sound: String): Uri? {
|
|
182
|
+
if (sound.isBlank() || sound == "default") return null
|
|
183
|
+
|
|
184
|
+
val parsed = Uri.parse(sound)
|
|
185
|
+
if (parsed.scheme != null) return parsed
|
|
186
|
+
|
|
187
|
+
val filename = sound.substringAfterLast('/')
|
|
188
|
+
val resourceName = filename.substringBeforeLast('.', filename)
|
|
189
|
+
if (filename != sound || !resourceName.matches(Regex("[a-z][a-z0-9_]*"))) {
|
|
190
|
+
Log.w(TAG, "Invalid custom alarm sound name '$sound'; using the default sound")
|
|
191
|
+
return null
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
val resourceId = resources.getIdentifier(resourceName, "raw", packageName)
|
|
195
|
+
if (resourceId == 0) {
|
|
196
|
+
Log.w(TAG, "Custom alarm sound '$sound' was not found; using the default sound")
|
|
197
|
+
return null
|
|
198
|
+
}
|
|
199
|
+
return Uri.parse("android.resource://$packageName/$resourceId")
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private fun playSound(uri: Uri) {
|
|
203
|
+
val mp = MediaPlayer()
|
|
160
204
|
try {
|
|
161
|
-
val mp = MediaPlayer()
|
|
162
|
-
player = mp
|
|
163
205
|
mp.apply {
|
|
164
206
|
setAudioAttributes(
|
|
165
207
|
AudioAttributes.Builder()
|
|
@@ -172,11 +214,10 @@ class AlarmService : Service() {
|
|
|
172
214
|
prepare()
|
|
173
215
|
start()
|
|
174
216
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
startVibration() // last resort: at least wake the user somehow
|
|
217
|
+
player = mp
|
|
218
|
+
} catch (error: Exception) {
|
|
219
|
+
mp.release()
|
|
220
|
+
throw error
|
|
180
221
|
}
|
|
181
222
|
}
|
|
182
223
|
|
|
@@ -35,6 +35,23 @@ struct AlarmData: Codable {
|
|
|
35
35
|
|
|
36
36
|
return props
|
|
37
37
|
}
|
|
38
|
+
|
|
39
|
+
var bundledSoundName: String? {
|
|
40
|
+
guard !sound.isEmpty, sound != "default",
|
|
41
|
+
URL(fileURLWithPath: sound).lastPathComponent == sound else {
|
|
42
|
+
return nil
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let filename = URL(fileURLWithPath: sound)
|
|
46
|
+
let name = filename.deletingPathExtension().lastPathComponent
|
|
47
|
+
let fileExtension = filename.pathExtension
|
|
48
|
+
guard !name.isEmpty, !fileExtension.isEmpty,
|
|
49
|
+
Bundle.main.url(forResource: name, withExtension: fileExtension) != nil else {
|
|
50
|
+
return nil
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return sound
|
|
54
|
+
}
|
|
38
55
|
|
|
39
56
|
/// Next time this alarm should ring, in ms since epoch, or 0 if never.
|
|
40
57
|
func nextTrigger(from: Double = AlarmData.now()) -> Double {
|
|
@@ -138,8 +138,12 @@ final class AlarmKitBackend {
|
|
|
138
138
|
metadata: RNAlarmMetadata(),
|
|
139
139
|
tintColor: .blue
|
|
140
140
|
)
|
|
141
|
-
let sound: AlertConfiguration.AlertSound
|
|
142
|
-
|
|
141
|
+
let sound: AlertConfiguration.AlertSound
|
|
142
|
+
if let soundName = a.bundledSoundName {
|
|
143
|
+
sound = .named(soundName)
|
|
144
|
+
} else {
|
|
145
|
+
sound = .default
|
|
146
|
+
}
|
|
143
147
|
|
|
144
148
|
return AlarmManager.AlarmConfiguration(
|
|
145
149
|
schedule: schedule,
|
|
@@ -17,6 +17,11 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
17
17
|
|
|
18
18
|
private var hasListeners = false
|
|
19
19
|
private var activeObserver: NSObjectProtocol?
|
|
20
|
+
private struct ScheduleTransaction {
|
|
21
|
+
let token: UUID
|
|
22
|
+
let original: AlarmData?
|
|
23
|
+
}
|
|
24
|
+
private var scheduleTransactions: [String: ScheduleTransaction] = [:]
|
|
20
25
|
|
|
21
26
|
override init() {
|
|
22
27
|
super.init()
|
|
@@ -95,7 +100,10 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
95
100
|
return false
|
|
96
101
|
}
|
|
97
102
|
|
|
98
|
-
private func cancelNative(_ id: String) {
|
|
103
|
+
private func cancelNative(_ id: String, preserveScheduleTransaction: Bool = false) {
|
|
104
|
+
if !preserveScheduleTransaction {
|
|
105
|
+
scheduleTransactions.removeValue(forKey: id)
|
|
106
|
+
}
|
|
99
107
|
AlarmRingPresenter.dismiss(id)
|
|
100
108
|
NotificationBackend.shared.cancel(id)
|
|
101
109
|
#if canImport(AlarmKit)
|
|
@@ -203,19 +211,51 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
203
211
|
arm(AlarmData.from(config: dict, id: alarmId), code: "E_UPDATE", reject: reject) { resolve(true) }
|
|
204
212
|
}
|
|
205
213
|
|
|
206
|
-
///
|
|
214
|
+
/// Replaces the persisted alarm only after the platform scheduler accepts it.
|
|
207
215
|
private func arm(_ alarm: AlarmData, code: String, reject: @escaping RCTPromiseRejectBlock, done: @escaping () -> Void) {
|
|
208
|
-
|
|
216
|
+
let original = scheduleTransactions[alarm.id].map { $0.original } ?? AlarmStore.get(alarm.id)
|
|
217
|
+
let token = UUID()
|
|
218
|
+
cancelNative(alarm.id, preserveScheduleTransaction: true)
|
|
219
|
+
scheduleTransactions[alarm.id] = ScheduleTransaction(token: token, original: original)
|
|
209
220
|
AlarmStore.save(alarm)
|
|
210
|
-
|
|
221
|
+
|
|
222
|
+
scheduleNative(alarm) { error in
|
|
223
|
+
guard self.scheduleTransactions[alarm.id]?.token == token else {
|
|
224
|
+
reject("E_CANCELLED", "Alarm scheduling was superseded or cancelled", nil)
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
guard let error else {
|
|
228
|
+
self.scheduleTransactions.removeValue(forKey: alarm.id)
|
|
229
|
+
done()
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
self.cancelNative(alarm.id, preserveScheduleTransaction: true)
|
|
234
|
+
if let original {
|
|
235
|
+
AlarmStore.save(original)
|
|
236
|
+
self.scheduleNative(original) { restoreError in
|
|
237
|
+
if let restoreError {
|
|
238
|
+
NSLog("[react-native-alarms] Could not restore alarm %@ after scheduling failed: %@",
|
|
239
|
+
alarm.id, restoreError.localizedDescription)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
AlarmStore.remove(alarm.id)
|
|
244
|
+
}
|
|
245
|
+
self.scheduleTransactions.removeValue(forKey: alarm.id)
|
|
246
|
+
reject(code, error.localizedDescription, error)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private func scheduleNative(_ alarm: AlarmData, completion: @escaping (Error?) -> Void) {
|
|
211
251
|
#if canImport(AlarmKit)
|
|
212
252
|
if #available(iOS 26.0, *), useAlarmKit {
|
|
213
253
|
Task {
|
|
214
254
|
do {
|
|
215
255
|
try await AlarmKitBackend.shared.schedule(alarm)
|
|
216
|
-
|
|
256
|
+
completion(nil)
|
|
217
257
|
} catch {
|
|
218
|
-
|
|
258
|
+
completion(error)
|
|
219
259
|
}
|
|
220
260
|
}
|
|
221
261
|
return
|
|
@@ -224,11 +264,15 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
224
264
|
|
|
225
265
|
NotificationBackend.shared.status { status in
|
|
226
266
|
guard status == "granted" else {
|
|
227
|
-
|
|
228
|
-
|
|
267
|
+
let error = NSError(
|
|
268
|
+
domain: "ReactNativeAlarms",
|
|
269
|
+
code: 1,
|
|
270
|
+
userInfo: [NSLocalizedDescriptionKey: "Alarm permission not granted; call requestPermission() first"]
|
|
271
|
+
)
|
|
272
|
+
return completion(error)
|
|
229
273
|
}
|
|
230
274
|
NotificationBackend.shared.schedule(alarm) { error in
|
|
231
|
-
|
|
275
|
+
completion(error)
|
|
232
276
|
}
|
|
233
277
|
}
|
|
234
278
|
}
|
|
@@ -23,6 +23,8 @@ final class NotificationBackend: NSObject, UNUserNotificationCenterDelegate {
|
|
|
23
23
|
|
|
24
24
|
/// The alarm whose notification is currently being shown (foreground) or was tapped.
|
|
25
25
|
private(set) var ringing: AlarmData?
|
|
26
|
+
private var imageScheduleTokens: [String: UUID] = [:]
|
|
27
|
+
private let imageScheduleTokensLock = NSLock()
|
|
26
28
|
/// Wired by `AlarmModule` so native events reach JS.
|
|
27
29
|
var onEvent: ((String, AlarmData) -> Void)?
|
|
28
30
|
|
|
@@ -86,9 +88,9 @@ final class NotificationBackend: NSObject, UNUserNotificationCenterDelegate {
|
|
|
86
88
|
c.categoryIdentifier = Self.category
|
|
87
89
|
c.userInfo = ["alarmId": a.id, "image": a.image ?? ""]
|
|
88
90
|
if let attachment { c.attachments = [attachment] }
|
|
89
|
-
c.sound =
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
c.sound = a.bundledSoundName.map {
|
|
92
|
+
UNNotificationSound(named: UNNotificationSoundName(rawValue: $0))
|
|
93
|
+
} ?? .default
|
|
92
94
|
// Breaks through Focus when the Time Sensitive Notifications capability is enabled;
|
|
93
95
|
// silently downgraded to a normal notification otherwise.
|
|
94
96
|
c.interruptionLevel = .timeSensitive
|
|
@@ -102,7 +104,12 @@ final class NotificationBackend: NSObject, UNUserNotificationCenterDelegate {
|
|
|
102
104
|
|
|
103
105
|
func schedule(_ a: AlarmData, completion: @escaping (Error?) -> Void) {
|
|
104
106
|
cancel(a.id)
|
|
107
|
+
let token = UUID()
|
|
108
|
+
setImageScheduleToken(token, for: a.id)
|
|
105
109
|
prepareAttachment(for: a.image) { attachment in
|
|
110
|
+
guard self.hasImageScheduleToken(token, for: a.id) else {
|
|
111
|
+
return completion(CancellationError())
|
|
112
|
+
}
|
|
106
113
|
let cal = Calendar.current
|
|
107
114
|
let base = Self.prefix + a.id
|
|
108
115
|
let hm = cal.dateComponents([.hour, .minute], from: a.date)
|
|
@@ -141,7 +148,7 @@ final class NotificationBackend: NSObject, UNUserNotificationCenterDelegate {
|
|
|
141
148
|
requests.append(self.request(base, a, self.exactTrigger(ms: next), attachment))
|
|
142
149
|
}
|
|
143
150
|
|
|
144
|
-
self.add(requests, completion: completion)
|
|
151
|
+
self.add(requests, alarmId: a.id, token: token, completion: completion)
|
|
145
152
|
}
|
|
146
153
|
}
|
|
147
154
|
|
|
@@ -165,7 +172,9 @@ final class NotificationBackend: NSObject, UNUserNotificationCenterDelegate {
|
|
|
165
172
|
return completion(nil)
|
|
166
173
|
}
|
|
167
174
|
|
|
168
|
-
|
|
175
|
+
var request = URLRequest(url: url)
|
|
176
|
+
request.timeoutInterval = 15
|
|
177
|
+
URLSession.shared.downloadTask(with: request) { temporaryURL, response, error in
|
|
169
178
|
guard let temporaryURL, error == nil else {
|
|
170
179
|
NSLog("[react-native-alarms] Could not download alarm image: %@", error?.localizedDescription ?? "empty response")
|
|
171
180
|
return completion(nil)
|
|
@@ -195,26 +204,69 @@ final class NotificationBackend: NSObject, UNUserNotificationCenterDelegate {
|
|
|
195
204
|
return UNCalendarNotificationTrigger(dateMatching: dc, repeats: false)
|
|
196
205
|
}
|
|
197
206
|
|
|
198
|
-
private func add(
|
|
207
|
+
private func add(
|
|
208
|
+
_ requests: [UNNotificationRequest],
|
|
209
|
+
alarmId: String,
|
|
210
|
+
token: UUID,
|
|
211
|
+
completion: @escaping (Error?) -> Void
|
|
212
|
+
) {
|
|
199
213
|
let group = DispatchGroup()
|
|
214
|
+
let errorLock = NSLock()
|
|
200
215
|
var firstError: Error?
|
|
201
216
|
for r in requests {
|
|
202
217
|
group.enter()
|
|
203
218
|
center.add(r) { error in
|
|
219
|
+
errorLock.lock()
|
|
204
220
|
if firstError == nil { firstError = error }
|
|
221
|
+
errorLock.unlock()
|
|
205
222
|
group.leave()
|
|
206
223
|
}
|
|
207
224
|
}
|
|
208
|
-
group.notify(queue: .main) {
|
|
225
|
+
group.notify(queue: .main) {
|
|
226
|
+
guard self.hasImageScheduleToken(token, for: alarmId) else {
|
|
227
|
+
self.center.removePendingNotificationRequests(withIdentifiers: requests.map(\.identifier))
|
|
228
|
+
return completion(CancellationError())
|
|
229
|
+
}
|
|
230
|
+
errorLock.lock()
|
|
231
|
+
let error = firstError
|
|
232
|
+
errorLock.unlock()
|
|
233
|
+
completion(error)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private func setImageScheduleToken(_ token: UUID, for alarmId: String) {
|
|
238
|
+
imageScheduleTokensLock.lock()
|
|
239
|
+
imageScheduleTokens[alarmId] = token
|
|
240
|
+
imageScheduleTokensLock.unlock()
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private func hasImageScheduleToken(_ token: UUID, for alarmId: String) -> Bool {
|
|
244
|
+
imageScheduleTokensLock.lock()
|
|
245
|
+
defer { imageScheduleTokensLock.unlock() }
|
|
246
|
+
return imageScheduleTokens[alarmId] == token
|
|
209
247
|
}
|
|
210
248
|
|
|
211
249
|
/// One-off re-ring in `minutes`, equivalent to Android's `AlarmScheduler.scheduleSnooze`.
|
|
212
250
|
func snooze(_ a: AlarmData, minutes: Int) {
|
|
213
251
|
center.removeDeliveredNotifications(withIdentifiers: ids(for: a.id))
|
|
214
252
|
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: Double(max(1, minutes)) * 60, repeats: false)
|
|
253
|
+
let token = UUID()
|
|
254
|
+
setImageScheduleToken(token, for: a.id)
|
|
215
255
|
prepareAttachment(for: a.image) { attachment in
|
|
216
|
-
self.
|
|
217
|
-
|
|
256
|
+
guard self.hasImageScheduleToken(token, for: a.id) else { return }
|
|
257
|
+
let request = self.request(Self.prefix + a.id + ":snooze", a, trigger, attachment)
|
|
258
|
+
self.center.add(request) { error in
|
|
259
|
+
guard self.hasImageScheduleToken(token, for: a.id) else {
|
|
260
|
+
self.center.removePendingNotificationRequests(withIdentifiers: [request.identifier])
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
if let error {
|
|
264
|
+
NSLog("[react-native-alarms] Could not schedule snooze for %@: %@",
|
|
265
|
+
a.id, error.localizedDescription)
|
|
266
|
+
} else if self.ringing?.id == a.id {
|
|
267
|
+
self.ringing = nil
|
|
268
|
+
}
|
|
269
|
+
}
|
|
218
270
|
}
|
|
219
271
|
}
|
|
220
272
|
|
|
@@ -225,12 +277,16 @@ final class NotificationBackend: NSObject, UNUserNotificationCenterDelegate {
|
|
|
225
277
|
}
|
|
226
278
|
|
|
227
279
|
func cancel(_ id: String) {
|
|
280
|
+
setImageScheduleToken(UUID(), for: id)
|
|
228
281
|
center.removePendingNotificationRequests(withIdentifiers: ids(for: id))
|
|
229
282
|
center.removeDeliveredNotifications(withIdentifiers: ids(for: id))
|
|
230
283
|
if ringing?.id == id { ringing = nil }
|
|
231
284
|
}
|
|
232
285
|
|
|
233
286
|
func cancelAll() {
|
|
287
|
+
imageScheduleTokensLock.lock()
|
|
288
|
+
imageScheduleTokens.removeAll()
|
|
289
|
+
imageScheduleTokensLock.unlock()
|
|
234
290
|
center.getPendingNotificationRequests { (requests: [UNNotificationRequest]) in
|
|
235
291
|
let ours = requests.map { $0.identifier }.filter { $0.hasPrefix(Self.prefix) }
|
|
236
292
|
self.center.removePendingNotificationRequests(withIdentifiers: ours)
|
package/package.json
CHANGED
package/src/types/index.ts
CHANGED
|
@@ -98,9 +98,11 @@ export type AlarmConfig = {
|
|
|
98
98
|
image?: string;
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
|
-
* Alarm sound.
|
|
101
|
+
* Alarm sound file name, such as `"bell.wav"`.
|
|
102
102
|
*
|
|
103
|
-
*
|
|
103
|
+
* Bundle the same filename with the iOS app and place it in Android's
|
|
104
|
+
* `android/app/src/main/res/raw` directory. Uses the system default if
|
|
105
|
+
* omitted, set to `"default"`, or the file is unavailable.
|
|
104
106
|
*/
|
|
105
107
|
sound?: string;
|
|
106
108
|
|