react-native-alarms 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +0 -0
- package/AGENTS.md +291 -0
- package/AlarmService.d.ts +141 -32
- package/AlarmService.ts +238 -39
- package/LICENSE.md +21 -0
- package/README.md +159 -9
- package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmData.kt +154 -8
- package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmModule.kt +2 -2
- package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmRingActivity.kt +3 -2
- package/android/local.properties +8 -0
- package/commitlint.config.mjs +1 -0
- package/index.d.ts +3 -3
- package/index.ts +5 -5
- package/ios/alarm/AlarmData.swift +20 -6
- package/ios/alarm/AlarmKitBackend.swift +43 -40
- package/ios/alarm/AlarmModule.swift +68 -64
- package/ios/alarm/AlarmRingPresenter.swift +26 -19
- package/ios/alarm/AlarmStore.swift +9 -9
- package/ios/alarm/NotificationBackend.swift +33 -33
- package/package.json +5 -2
- package/specs/NativeAlarm.ts +120 -71
- package/src/assets/recording.mp4 +0 -0
- package/src/assets/sample-1.png +0 -0
- package/src/assets/sample-2.png +0 -0
- package/src/assets/sample-3.png +0 -0
- package/src/screens/index.ts +0 -0
- package/src/types/index.ts +246 -0
- package/src/utils/AlarmRegistry.ts +57 -0
- package/src/utils/index.ts +1 -0
|
@@ -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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
o.optString("
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
11
|
+
AlarmProp,
|
|
12
12
|
AlarmPermission,
|
|
13
13
|
AlarmEvent,
|
|
14
|
-
|
|
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
|
-
|
|
11
|
+
AlarmProp,
|
|
12
12
|
AlarmPermission,
|
|
13
13
|
AlarmEvent,
|
|
14
|
-
|
|
15
|
-
} from './specs/NativeAlarm';
|
|
14
|
+
} from './src/types';
|
|
16
15
|
|
|
17
16
|
export { AlarmService } from './AlarmService';
|
|
18
17
|
|
|
19
|
-
import
|
|
20
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|