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/README.md CHANGED
@@ -1,6 +1,11 @@
1
1
  # React Native Alarms 🔔
2
2
 
3
- A powerful, cross-platform alarm management library for React Native. Schedule, manage, and control alarms on both Android and iOS with a unified API.
3
+ A cross-platform native scheduling, reminder, and wake-up system for React Native. Schedule time-sensitive events on Android and iOS, deliver reminders, and present a registered custom React Native screen when a scheduled event fires—using one unified API.
4
+
5
+ Although it can power a traditional alarm clock, react-native-alarms is also useful for exact-time reminders, scheduled check-ins, custom lock-screen experiences, and time-based app workflows. Build the experience around your own UI and event data instead of being limited to a standard alarm screen.
6
+
7
+ > **Platform note:** The operating system controls background delivery and screen presentation. Exact timing, full-screen presentation, and behavior when an app is terminated depend on platform version, permissions, device settings, and the native scheduling mechanism. This library does not provide unrestricted background JavaScript execution.
8
+
4
9
 
5
10
  [![npm version](https://badge.fury.io/js/react-native-alarms.svg)](https://badge.fury.io/js/react-native-alarms)
6
11
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -65,6 +70,44 @@ AppRegistry.registerComponent('AlarmRing', () => AlarmRingScreen);
65
70
  Call `Alarm.stopAlarm(alarmId)` or `Alarm.snoozeAlarm(alarmId, minutes)` from its
66
71
  buttons; the screen closes itself.
67
72
 
73
+ #### Custom presentation component
74
+
75
+ You can register **multiple** ring screens and choose which one fires per alarm using the `presentation` field in `AlarmConfig`:
76
+
77
+ ```js
78
+ // index.js
79
+ import { AppRegistry } from 'react-native';
80
+ import { AlarmRingScreen } from './src/AlarmRingScreen';
81
+ import { ReminderScreen } from './src/ReminderScreen';
82
+
83
+ AppRegistry.registerComponent('AlarmRing', () => AlarmRingScreen); // default
84
+ AppRegistry.registerComponent('Reminder', () => ReminderScreen); // custom
85
+ ```
86
+
87
+ ```ts
88
+ import { AlarmRegistry, AlarmService } from 'react-native-alarms/service';
89
+
90
+ // 1. Map your key to the registered component name
91
+ AlarmRegistry.register('reminder', 'Reminder');
92
+
93
+ // 2. Pass it when scheduling
94
+ await AlarmService.scheduleAlarm({
95
+ timestamp: Date.now() + 60_000,
96
+ title: 'Standup',
97
+ presentation: {
98
+ type: 'reminder', // resolves to the 'Reminder' component
99
+ props: { color: '#6200EE' } // forwarded to the screen as componentProps
100
+ },
101
+ });
102
+ ```
103
+
104
+ The native layer maps `presentation` to two internal fields:
105
+
106
+ | Internal field | Type | Default | Description |
107
+ |---|---|---|---|
108
+ | `componentName` | `string` | `"AlarmRing"` | Registered `AppRegistry` component name shown when the alarm rings. |
109
+ | `componentProps` / `componentPropsJSON` | `object` / `string` | `{}` | Key-value props forwarded to the ring screen on **both platforms**. On Android they arrive as top-level bundle extras and a nested `componentProps` map; on iOS as a parsed `[String: Any]` dictionary. |
110
+
68
111
  ### Direct module access
69
112
 
70
113
  ```ts
@@ -179,9 +222,28 @@ type AlarmConfig = {
179
222
  snoozeMinutes?: number; // Snooze duration in minutes
180
223
  repeat?: AlarmRepeat; // Repeat configuration
181
224
  data?: string; // Custom JSON string payload
225
+ presentation?: AlarmPresentation; // Custom ring screen & props
226
+ };
227
+
228
+ type AlarmPresentation = {
229
+ /** Registered presentation type (key passed to AlarmRegistry.register). */
230
+ type?: string;
231
+ /**
232
+ * Props forwarded to the ring screen component.
233
+ * Available as `componentProps` on both Android and iOS.
234
+ */
235
+ props?: Record<string, unknown>;
182
236
  };
183
237
  ```
184
238
 
239
+ > **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:
240
+ >
241
+ > | Field | Type | Default | Description |
242
+ > |---|---|---|---|
243
+ > | `componentName` | `string` | `"AlarmRing"` | The `AppRegistry` name of the component shown at ring time. Derived from `AlarmRegistry.resolve(presentation.type)`. |
244
+ > | `componentProps` | `object` | `{}` | Parsed props object (from `presentation.props`). Available on both platforms at ring time as `componentProps`. |
245
+ > | `componentPropsJSON` | `string` | `"{}"` | JSON-serialised representation of `componentProps`, persisted alongside the alarm so props survive a device reboot. |
246
+
185
247
  #### `AlarmRepeat`
186
248
  Repeat configuration for recurring alarms.
187
249
 
@@ -433,27 +495,99 @@ For issues and questions, please visit:
433
495
 
434
496
  The library supports completely custom alarm UI screens with custom data payloads.
435
497
 
436
- ### Basic Custom Screen Example
498
+ ### Using `componentName` and `componentProps`
499
+
500
+ The `presentation` field lets you choose **which registered component** is displayed and pass **arbitrary props** directly to it — without encoding everything inside a JSON `data` string.
501
+
502
+ #### 1. Register your screens
503
+
504
+ ```js
505
+ // index.js
506
+ import { AppRegistry } from 'react-native';
507
+ import { AlarmRingScreen } from './src/AlarmRingScreen';
508
+ import { MeetingAlarmScreen } from './src/MeetingAlarmScreen';
509
+
510
+ AppRegistry.registerComponent('AlarmRing', () => AlarmRingScreen); // default fallback
511
+ AppRegistry.registerComponent('MeetingAlarm', () => MeetingAlarmScreen); // custom
512
+ ```
513
+
514
+ #### 2. Map a key to the component name
515
+
516
+ ```ts
517
+ import { AlarmRegistry } from 'react-native-alarms';
518
+
519
+ AlarmRegistry.register('meeting', 'MeetingAlarm');
520
+ ```
521
+
522
+ #### 3. Schedule with `presentation`
437
523
 
438
524
  ```typescript
439
- import { NativeModules } from 'react-native';
440
525
  import { AlarmService } from 'react-native-alarms/service';
441
526
 
442
- export function MyCustomAlarmScreen({ alarmId, title, data }) {
443
- const customData = JSON.parse(data || '{}');
527
+ await AlarmService.scheduleAlarm({
528
+ timestamp: Date.now() + 5 * 60 * 1000,
529
+ title: 'Team standup',
530
+ presentation: {
531
+ type: 'meeting', // resolves to 'MeetingAlarm'
532
+ props: {
533
+ roomName: 'Zoom Room A',
534
+ meetingUrl: 'https://zoom.us/j/123',
535
+ accentColor: '#6200EE',
536
+ },
537
+ },
538
+ });
539
+ ```
540
+
541
+ #### 4. Receive `componentProps` in your screen
542
+
543
+ The props passed via `presentation.props` are forwarded to the ring screen on both platforms:
544
+
545
+ ```typescript
546
+ // MeetingAlarmScreen.tsx
547
+ type Props = {
548
+ alarmId: string;
549
+ title: string;
550
+ snoozeEnabled: boolean;
551
+ snoozeMinutes: number;
552
+ data?: string;
553
+ // componentProps fields arrive as top-level props:
554
+ roomName?: string;
555
+ meetingUrl?: string;
556
+ accentColor?: string;
557
+ };
444
558
 
559
+ export function MeetingAlarmScreen({
560
+ alarmId,
561
+ title,
562
+ snoozeEnabled,
563
+ snoozeMinutes,
564
+ roomName,
565
+ meetingUrl,
566
+ accentColor = '#007AFF',
567
+ }: Props) {
445
568
  return (
446
- <View style={{ backgroundColor: customData.color }}>
447
- <Text>{title}</Text>
569
+ <View style={{ flex: 1, backgroundColor: accentColor }}>
570
+ <Text style={{ fontSize: 28, color: '#fff' }}>{title}</Text>
571
+ <Text style={{ color: '#fff' }}>{roomName}</Text>
572
+
573
+ {snoozeEnabled && (
574
+ <TouchableOpacity onPress={() => AlarmService.snoozeAlarm(alarmId, snoozeMinutes)}>
575
+ <Text>Snooze {snoozeMinutes} min</Text>
576
+ </TouchableOpacity>
577
+ )}
448
578
  <TouchableOpacity onPress={() => AlarmService.stopAlarm(alarmId)}>
449
- <Text>Stop</Text>
579
+ <Text>Dismiss</Text>
450
580
  </TouchableOpacity>
451
581
  </View>
452
582
  );
453
583
  }
454
584
  ```
455
585
 
456
- ### Passing Custom Data
586
+ > **Platform behaviour**
587
+ > - **Android**: `componentProps` entries are flattened into the launch `Bundle` alongside standard fields (`alarmId`, `title`, etc.), and also available as a nested `componentProps` bundle. The `AlarmRingActivity` uses `componentName` to resolve which registered React root to display.
588
+ > - **iOS**: `componentProps` (parsed from `componentPropsJSON`) is merged into the surface's initial properties passed to the React component.
589
+
590
+ ### Passing Custom Data (legacy approach)
457
591
 
458
592
  ```typescript
459
593
  await AlarmService.scheduleAlarm({
@@ -3,6 +3,7 @@ package com.alarm
3
3
  import android.os.Bundle
4
4
  import com.facebook.react.bridge.Arguments
5
5
  import com.facebook.react.bridge.ReadableMap
6
+ import com.facebook.react.bridge.WritableArray
6
7
  import com.facebook.react.bridge.WritableMap
7
8
  import org.json.JSONArray
8
9
  import org.json.JSONObject
@@ -21,8 +22,29 @@ data class AlarmData(
21
22
  val weekdays: List<Int>,
22
23
  val intervalMs: Long,
23
24
  val data: String,
24
- var enabled: Boolean = true
25
+ var enabled: Boolean = true,
26
+ val componentName: String = "AlarmRing",
27
+ val componentPropsJSON: String = "{}"
25
28
  ) {
29
+ val componentProps: Map<String, Any>
30
+ get() {
31
+ return try {
32
+ val obj = JSONObject(componentPropsJSON)
33
+ val map = mutableMapOf<String, Any>()
34
+ val keys = obj.keys()
35
+ while (keys.hasNext()) {
36
+ val key = keys.next()
37
+ val value = obj.get(key)
38
+ if (value != JSONObject.NULL) {
39
+ map[key] = value
40
+ }
41
+ }
42
+ map
43
+ } catch (_: Exception) {
44
+ emptyMap()
45
+ }
46
+ }
47
+
26
48
  fun nextTrigger(from: Long = System.currentTimeMillis()): Long {
27
49
  if (!enabled) return 0
28
50
  return when (repeatType) {
@@ -59,6 +81,8 @@ data class AlarmData(
59
81
  put("snoozeMinutes", snoozeMinutes); put("repeatType", repeatType)
60
82
  put("weekdays", JSONArray(weekdays)); put("intervalMs", intervalMs)
61
83
  put("data", data); put("enabled", enabled)
84
+ put("componentName", componentName)
85
+ put("componentPropsJSON", componentPropsJSON)
62
86
  }.toString()
63
87
 
64
88
  fun toWritableMap(): WritableMap = Arguments.createMap().apply {
@@ -78,6 +102,13 @@ data class AlarmData(
78
102
  putString("data", data)
79
103
  putBoolean("enabled", enabled)
80
104
  putDouble("nextTriggerAt", nextTrigger().toDouble())
105
+ putString("componentName", componentName)
106
+ putString("componentPropsJSON", componentPropsJSON)
107
+ try {
108
+ putMap("componentProps", jsonToWritableMap(JSONObject(componentPropsJSON)))
109
+ } catch (_: Exception) {
110
+ putMap("componentProps", Arguments.createMap())
111
+ }
81
112
  }
82
113
 
83
114
  /** Initial props for the React ring screen hosted by [AlarmRingActivity]. */
@@ -89,19 +120,70 @@ data class AlarmData(
89
120
  putBoolean("snoozeEnabled", snoozeEnabled)
90
121
  putInt("snoozeMinutes", snoozeMinutes)
91
122
  putString("data", data)
123
+ putString("componentName", componentName)
124
+ putString("componentPropsJSON", componentPropsJSON)
125
+ try {
126
+ val propsObj = JSONObject(componentPropsJSON)
127
+ val propsBundle = jsonToBundle(propsObj)
128
+ putBundle("componentProps", propsBundle)
129
+ for (key in propsBundle.keySet()) {
130
+ if (!containsKey(key)) {
131
+ val v = propsBundle.get(key)
132
+ when (v) {
133
+ is String -> putString(key, v)
134
+ is Boolean -> putBoolean(key, v)
135
+ is Int -> putInt(key, v)
136
+ is Long -> putLong(key, v)
137
+ is Double -> putDouble(key, v)
138
+ is Bundle -> putBundle(key, v)
139
+ }
140
+ }
141
+ }
142
+ } catch (_: Exception) {}
143
+ }
144
+
145
+ private fun jsonToBundle(jsonObject: JSONObject): Bundle {
146
+ val bundle = Bundle()
147
+ val iter = jsonObject.keys()
148
+ while (iter.hasNext()) {
149
+ val key = iter.next()
150
+ when (val value = jsonObject.opt(key)) {
151
+ is JSONObject -> bundle.putBundle(key, jsonToBundle(value))
152
+ is JSONArray -> bundle.putString(key, value.toString())
153
+ is Boolean -> bundle.putBoolean(key, value)
154
+ is Int -> bundle.putInt(key, value)
155
+ is Long -> bundle.putLong(key, value)
156
+ is Double -> bundle.putDouble(key, value)
157
+ is String -> bundle.putString(key, value)
158
+ else -> if (value != null && value != JSONObject.NULL) {
159
+ bundle.putString(key, value.toString())
160
+ }
161
+ }
162
+ }
163
+ return bundle
92
164
  }
93
165
 
94
166
  companion object {
95
167
  fun fromJson(s: String): AlarmData {
96
168
  val o = JSONObject(s)
97
169
  val wd = o.optJSONArray("weekdays")
170
+ val compName = o.optString("componentName", "AlarmRing").trim()
98
171
  return AlarmData(
99
- o.getString("id"), o.getLong("timestamp"), o.optString("title"), o.optString("body"),
100
- o.optString("sound", "default"), o.optBoolean("vibrate", true),
101
- o.optBoolean("snoozeEnabled", true), o.optInt("snoozeMinutes", 5),
102
- o.optString("repeatType", "none"),
103
- if (wd == null) emptyList() else (0 until wd.length()).map { wd.getInt(it) },
104
- o.optLong("intervalMs", 0), o.optString("data"), o.optBoolean("enabled", true)
172
+ id = o.getString("id"),
173
+ timestamp = o.getLong("timestamp"),
174
+ title = o.optString("title"),
175
+ body = o.optString("body"),
176
+ sound = o.optString("sound", "default"),
177
+ vibrate = o.optBoolean("vibrate", true),
178
+ snoozeEnabled = o.optBoolean("snoozeEnabled", true),
179
+ snoozeMinutes = o.optInt("snoozeMinutes", 5),
180
+ repeatType = o.optString("repeatType", "none"),
181
+ weekdays = if (wd == null) emptyList() else (0 until wd.length()).map { wd.getInt(it) },
182
+ intervalMs = o.optLong("intervalMs", 0),
183
+ data = o.optString("data"),
184
+ enabled = o.optBoolean("enabled", true),
185
+ componentName = if (compName.isEmpty()) "AlarmRing" else compName,
186
+ componentPropsJSON = o.optString("componentPropsJSON", "{}").ifBlank { "{}" }
105
187
  )
106
188
  }
107
189
 
@@ -109,6 +191,24 @@ data class AlarmData(
109
191
  val r = if (m.hasKey("repeat") && !m.isNull("repeat")) m.getMap("repeat") else null
110
192
  val wd = r?.takeIf { it.hasKey("weekdays") }?.getArray("weekdays")
111
193
  ?.let { a -> (0 until a.size()).map { a.getInt(it) } } ?: emptyList()
194
+
195
+ val componentPropsStr = when {
196
+ m.hasKey("componentPropsJSON") && !m.isNull("componentPropsJSON") ->
197
+ m.getString("componentPropsJSON") ?: "{}"
198
+ m.hasKey("componentProps") && !m.isNull("componentProps") -> {
199
+ val propsMap = m.getMap("componentProps")
200
+ try {
201
+ val hashMap = propsMap?.toHashMap() ?: emptyMap<String, Any>()
202
+ JSONObject(hashMap).toString()
203
+ } catch (_: Exception) {
204
+ "{}"
205
+ }
206
+ }
207
+ else -> "{}"
208
+ }
209
+
210
+ val compName = m.str("componentName", "AlarmRing").trim()
211
+
112
212
  return AlarmData(
113
213
  id = id,
114
214
  timestamp = m.getDouble("timestamp").toLong(),
@@ -121,10 +221,56 @@ data class AlarmData(
121
221
  repeatType = r?.str("type", "none") ?: "none",
122
222
  weekdays = wd,
123
223
  intervalMs = if (r != null && r.hasKey("intervalMs")) r.getDouble("intervalMs").toLong() else 0,
124
- data = m.str("data")
224
+ data = m.str("data"),
225
+ componentName = if (compName.isEmpty()) "AlarmRing" else compName,
226
+ componentPropsJSON = componentPropsStr
125
227
  )
126
228
  }
127
229
 
230
+ private fun jsonToWritableMap(jsonObject: JSONObject): WritableMap {
231
+ val map = Arguments.createMap()
232
+ val iter = jsonObject.keys()
233
+ while (iter.hasNext()) {
234
+ val key = iter.next()
235
+ when (val value = jsonObject.opt(key)) {
236
+ is JSONObject -> map.putMap(key, jsonToWritableMap(value))
237
+ is JSONArray -> map.putArray(key, jsonToWritableArray(value))
238
+ is Boolean -> map.putBoolean(key, value)
239
+ is Int -> map.putInt(key, value)
240
+ is Long -> map.putDouble(key, value.toDouble())
241
+ is Double -> map.putDouble(key, value)
242
+ is String -> map.putString(key, value)
243
+ else -> if (value == null || value == JSONObject.NULL) {
244
+ map.putNull(key)
245
+ } else {
246
+ map.putString(key, value.toString())
247
+ }
248
+ }
249
+ }
250
+ return map
251
+ }
252
+
253
+ private fun jsonToWritableArray(jsonArray: JSONArray): WritableArray {
254
+ val array = Arguments.createArray()
255
+ for (i in 0 until jsonArray.length()) {
256
+ when (val value = jsonArray.opt(i)) {
257
+ is JSONObject -> array.pushMap(jsonToWritableMap(value))
258
+ is JSONArray -> array.pushArray(jsonToWritableArray(value))
259
+ is Boolean -> array.pushBoolean(value)
260
+ is Int -> array.pushInt(value)
261
+ is Long -> array.pushDouble(value.toDouble())
262
+ is Double -> array.pushDouble(value)
263
+ is String -> array.pushString(value)
264
+ else -> if (value == null || value == JSONObject.NULL) {
265
+ array.pushNull()
266
+ } else {
267
+ array.pushString(value.toString())
268
+ }
269
+ }
270
+ }
271
+ return array
272
+ }
273
+
128
274
  private fun ReadableMap.str(k: String, d: String = "") =
129
275
  if (hasKey(k) && !isNull(k)) getString(k) ?: d else d
130
276
 
@@ -31,14 +31,14 @@ class AlarmModule(private val ctx: ReactApplicationContext) : ReactContextBaseJa
31
31
  private fun notificationsGranted(): Boolean =
32
32
  if (Build.VERSION.SDK_INT >= 33)
33
33
  ContextCompat.checkSelfPermission(ctx, Manifest.permission.POST_NOTIFICATIONS) ==
34
- PackageManager.PERMISSION_GRANTED
34
+ PackageManager.PERMISSION_GRANTED
35
35
  else NotificationManagerCompat.from(ctx).areNotificationsEnabled()
36
36
 
37
37
  private fun canDrawOverlays(): Boolean = Settings.canDrawOverlays(ctx)
38
38
 
39
39
  private fun canUseFullScreenIntent(): Boolean =
40
40
  Build.VERSION.SDK_INT < 34 ||
41
- ctx.getSystemService(NotificationManager::class.java).canUseFullScreenIntent()
41
+ ctx.getSystemService(NotificationManager::class.java).canUseFullScreenIntent()
42
42
 
43
43
  private fun permissionMap(): WritableMap = Arguments.createMap().apply {
44
44
  val ok = notificationsGranted()
@@ -18,7 +18,8 @@ import java.lang.ref.WeakReference
18
18
  */
19
19
  class AlarmRingActivity : ReactActivity() {
20
20
 
21
- override fun getMainComponentName(): String = COMPONENT_NAME
21
+ override fun getMainComponentName(): String =
22
+ AlarmService.current?.componentName?.takeIf { it.isNotBlank() } ?: COMPONENT_NAME
22
23
 
23
24
  override fun createReactActivityDelegate(): ReactActivityDelegate =
24
25
  object : DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) {
@@ -45,7 +46,7 @@ class AlarmRingActivity : ReactActivity() {
45
46
  @Suppress("DEPRECATION")
46
47
  window.addFlags(
47
48
  WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
48
- WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
49
+ WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
49
50
  )
50
51
  }
51
52
  window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
@@ -0,0 +1,8 @@
1
+ ## This file must *NOT* be checked into Version Control Systems,
2
+ # as it contains information specific to your local configuration.
3
+ #
4
+ # Location of the SDK. This is only used by Gradle.
5
+ # For customization when using a Version Control System, please read the
6
+ # header note.
7
+ #Fri Oct 23 16:38:30 IST 2026
8
+ sdk.dir=/Users/apple/Library/Android/sdk
package/index.d.ts CHANGED
@@ -8,13 +8,13 @@ import type { Spec } from './specs/NativeAlarm';
8
8
  export type {
9
9
  AlarmRepeat,
10
10
  AlarmConfig,
11
- Alarm,
11
+ AlarmProp,
12
12
  AlarmPermission,
13
13
  AlarmEvent,
14
- Spec as AlarmModuleSpec,
15
- } from './specs/NativeAlarm';
14
+ } from './src/types';
16
15
 
17
16
  export { AlarmService } from './AlarmService';
18
17
 
19
18
  declare const Alarm: Spec;
19
+
20
20
  export default Alarm;
package/index.ts CHANGED
@@ -8,13 +8,13 @@
8
8
  export type {
9
9
  AlarmRepeat,
10
10
  AlarmConfig,
11
- Alarm,
11
+ AlarmProp,
12
12
  AlarmPermission,
13
13
  AlarmEvent,
14
- Spec as AlarmModuleSpec,
15
- } from './specs/NativeAlarm';
14
+ } from './src/types';
16
15
 
17
16
  export { AlarmService } from './AlarmService';
18
17
 
19
- import NativeAlarm from './specs/NativeAlarm';
20
- export default NativeAlarm;
18
+ import Alarm from './specs/NativeAlarm';
19
+
20
+ export default Alarm;
@@ -16,11 +16,25 @@ struct AlarmData: Codable {
16
16
  var intervalMs: Double
17
17
  var data: String
18
18
  var enabled: Bool = true
19
-
19
+ var componentName: String = "AlarmRing"
20
+ var componentPropsJSON: String = "{}"
21
+
20
22
  static func now() -> Double { Date().timeIntervalSince1970 * 1000 }
21
-
23
+
22
24
  var date: Date { Date(timeIntervalSince1970: timestamp / 1000) }
23
25
 
26
+ var componentProps: [String: Any] {
27
+ guard
28
+ let jsonData = componentPropsJSON.data(using: .utf8),
29
+ let object = try? JSONSerialization.jsonObject(with: jsonData),
30
+ let props = object as? [String: Any]
31
+ else {
32
+ return [:]
33
+ }
34
+
35
+ return props
36
+ }
37
+
24
38
  /// Next time this alarm should ring, in ms since epoch, or 0 if never.
25
39
  func nextTrigger(from: Double = AlarmData.now()) -> Double {
26
40
  if !enabled { return 0 }
@@ -37,7 +51,7 @@ struct AlarmData: Codable {
37
51
  return timestamp > from ? timestamp : 0
38
52
  }
39
53
  }
40
-
54
+
41
55
  private func nextMatching(from: Double, ok: (Int) -> Bool) -> Double {
42
56
  let cal = Calendar.current
43
57
  let base = cal.dateComponents([.hour, .minute], from: date)
@@ -55,9 +69,9 @@ struct AlarmData: Codable {
55
69
  }
56
70
  return 0
57
71
  }
58
-
72
+
59
73
  // MARK: - JS bridge shape (same keys as Android)
60
-
74
+
61
75
  func toDictionary() -> [String: Any] {
62
76
  [
63
77
  "alarmId": id,
@@ -74,7 +88,7 @@ struct AlarmData: Codable {
74
88
  "nextTriggerAt": nextTrigger(),
75
89
  ]
76
90
  }
77
-
91
+
78
92
  /// Parses the `AlarmConfig` object JS passes to `scheduleAlarm` / `updateAlarm`.
79
93
  static func from(config m: [String: Any], id: String) -> AlarmData {
80
94
  let r = m["repeat"] as? [String: Any]