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.
@@ -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
@@ -0,0 +1 @@
1
+ export default { extends: ['@commitlint/config-conventional'] };
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]
@@ -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
- ? Self.allWeekdays
111
- : a.weekdays.filter { (1...7).contains($0) }.map { Self.allWeekdays[$0 - 1] }
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
- -> AlarmManager.AlarmConfiguration<RNAlarmMetadata>
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
- (a.sound.isEmpty || a.sound == "default") ? .default : .named(a.sound)
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
- where current.contains(where: { $0.id == uuid && $0.state == .alerting }) {
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
- where current.contains(where: { $0.id == uuid && $0.state == .alerting }) {
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
  }