react-native-alarms 0.0.0-stage → 0.0.2

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.
Files changed (64) hide show
  1. package/.bundle/config +2 -0
  2. package/AlarmService.d.ts +112 -0
  3. package/AlarmService.ts +196 -0
  4. package/README.md +385 -2
  5. package/android/app/build.gradle +119 -0
  6. package/android/app/debug.keystore +0 -0
  7. package/android/app/proguard-rules.pro +10 -0
  8. package/android/app/src/main/AndroidManifest.xml +81 -0
  9. package/android/app/src/main/java/com/reactnativealarm/MainActivity.kt +29 -0
  10. package/android/app/src/main/java/com/reactnativealarm/MainApplication.kt +29 -0
  11. package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmData.kt +134 -0
  12. package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmModule.kt +263 -0
  13. package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmPackage.kt +24 -0
  14. package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmReceiver.kt +43 -0
  15. package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmRingActivity.kt +86 -0
  16. package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmScheduler.kt +74 -0
  17. package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmService.kt +285 -0
  18. package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmStore.kt +13 -0
  19. package/android/app/src/main/java/com/reactnativealarm/alarm/BootReceiver.kt +29 -0
  20. package/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
  21. package/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  22. package/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
  23. package/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  24. package/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
  25. package/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  26. package/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
  27. package/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  28. package/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  29. package/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  30. package/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  31. package/android/app/src/main/res/values/strings.xml +3 -0
  32. package/android/app/src/main/res/values/styles.xml +9 -0
  33. package/android/build.gradle +21 -0
  34. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  35. package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  36. package/android/gradle.properties +49 -0
  37. package/android/gradlew +248 -0
  38. package/android/gradlew.bat +98 -0
  39. package/android/settings.gradle +6 -0
  40. package/app.json +4 -0
  41. package/index.d.ts +20 -0
  42. package/index.js +12 -0
  43. package/index.ts +21 -0
  44. package/ios/.xcode.env +11 -0
  45. package/ios/Podfile +34 -0
  46. package/ios/Podfile.lock +2179 -0
  47. package/ios/ReactNativeAlarm/AppDelegate.swift +14 -0
  48. package/ios/ReactNativeAlarm/Images.xcassets/AppIcon.appiconset/Contents.json +53 -0
  49. package/ios/ReactNativeAlarm/Images.xcassets/Contents.json +6 -0
  50. package/ios/ReactNativeAlarm/Info.plist +79 -0
  51. package/ios/ReactNativeAlarm/LaunchScreen.storyboard +47 -0
  52. package/ios/ReactNativeAlarm/PrivacyInfo.xcprivacy +37 -0
  53. package/ios/ReactNativeAlarm/SceneDelegate.swift +56 -0
  54. package/ios/ReactNativeAlarm.xcodeproj/project.pbxproj +530 -0
  55. package/ios/ReactNativeAlarm.xcodeproj/xcshareddata/xcschemes/ReactNativeAlarm.xcscheme +88 -0
  56. package/ios/ReactNativeAlarm.xcworkspace/contents.xcworkspacedata +10 -0
  57. package/ios/alarm/AlarmData.swift +97 -0
  58. package/ios/alarm/AlarmKitBackend.swift +212 -0
  59. package/ios/alarm/AlarmModule.m +53 -0
  60. package/ios/alarm/AlarmModule.swift +284 -0
  61. package/ios/alarm/AlarmStore.swift +51 -0
  62. package/ios/alarm/NotificationBackend.swift +227 -0
  63. package/package.json +91 -3
  64. package/specs/NativeAlarm.ts +95 -0
@@ -0,0 +1,227 @@
1
+ import Foundation
2
+ import UserNotifications
3
+
4
+ /// Fallback backend for devices without AlarmKit (iOS < 26, or AlarmKit denied):
5
+ /// local notifications with Stop / Snooze actions.
6
+ ///
7
+ /// Limitations compared with a real alarm: sound plays for at most ~30 s, the
8
+ /// silent switch and Focus modes are respected, and `interval` repeats are
9
+ /// pre-scheduled as a batch of exact one-off notifications (see `schedule`).
10
+ final class NotificationBackend: NSObject, UNUserNotificationCenterDelegate {
11
+ static let shared = NotificationBackend()
12
+
13
+ static let category = "RN_ALARM"
14
+ static let actionSnooze = "RN_ALARM_SNOOZE"
15
+ static let actionStop = "RN_ALARM_STOP"
16
+ private static let prefix = "rnalarm:"
17
+ /// How many `interval` occurrences to pre-schedule (iOS caps pending requests at 64).
18
+ private static let intervalOccurrences = 12
19
+
20
+ private let center = UNUserNotificationCenter.current()
21
+
22
+ /// The alarm whose notification is currently being shown (foreground) or was tapped.
23
+ private(set) var ringing: AlarmData?
24
+ /// Wired by `AlarmModule` so native events reach JS.
25
+ var onEvent: ((String, AlarmData) -> Void)?
26
+
27
+ /// Installs the delegate and actions. Call once at launch so a tap on a
28
+ /// notification that starts the app is delivered to the module.
29
+ static func install() {
30
+ let center = UNUserNotificationCenter.current()
31
+ if center.delegate == nil { center.delegate = shared }
32
+ let snooze = UNNotificationAction(identifier: actionSnooze, title: "Snooze", options: [])
33
+ let stop = UNNotificationAction(identifier: actionStop, title: "Stop", options: [.destructive])
34
+ center.setNotificationCategories([
35
+ UNNotificationCategory(
36
+ identifier: category, actions: [snooze, stop], intentIdentifiers: [],
37
+ options: [.customDismissAction]
38
+ ),
39
+ ])
40
+ }
41
+
42
+ // MARK: - Permission
43
+
44
+ /// 'granted' | 'denied' | 'notDetermined', same vocabulary as Android.
45
+ func status(_ completion: @escaping (String) -> Void) {
46
+ center.getNotificationSettings { settings in
47
+ switch settings.authorizationStatus {
48
+ case .authorized, .provisional, .ephemeral: completion("granted")
49
+ case .denied: completion("denied")
50
+ default: completion("notDetermined")
51
+ }
52
+ }
53
+ }
54
+
55
+ func request(_ completion: @escaping (Bool) -> Void) {
56
+ center.requestAuthorization(options: [.alert, .sound, .badge]) { ok, _ in completion(ok) }
57
+ }
58
+
59
+ // MARK: - Scheduling
60
+
61
+ private func ids(for alarmId: String) -> [String] {
62
+ let base = Self.prefix + alarmId
63
+ return [base, base + ":snooze"]
64
+ + (1...7).map { base + ":wd\($0)" }
65
+ + (0..<Self.intervalOccurrences).map { base + ":i\($0)" }
66
+ }
67
+
68
+ private func content(for a: AlarmData) -> UNMutableNotificationContent {
69
+ let c = UNMutableNotificationContent()
70
+ c.title = a.title.isEmpty ? "Alarm" : a.title
71
+ c.body = a.body.isEmpty ? "Alarm is ringing" : a.body
72
+ c.categoryIdentifier = Self.category
73
+ c.userInfo = ["alarmId": a.id]
74
+ c.sound = (a.sound.isEmpty || a.sound == "default")
75
+ ? .default
76
+ : UNNotificationSound(named: UNNotificationSoundName(rawValue: a.sound))
77
+ // Breaks through Focus when the Time Sensitive Notifications capability is enabled;
78
+ // silently downgraded to a normal notification otherwise.
79
+ c.interruptionLevel = .timeSensitive
80
+ return c
81
+ }
82
+
83
+ private func request(_ id: String, _ a: AlarmData, _ trigger: UNNotificationTrigger) -> UNNotificationRequest {
84
+ UNNotificationRequest(identifier: id, content: content(for: a), trigger: trigger)
85
+ }
86
+
87
+ func schedule(_ a: AlarmData, completion: @escaping (Error?) -> Void) {
88
+ cancel(a.id)
89
+ let cal = Calendar.current
90
+ let base = Self.prefix + a.id
91
+ let hm = cal.dateComponents([.hour, .minute], from: a.date)
92
+ var requests: [UNNotificationRequest] = []
93
+
94
+ switch a.repeatType {
95
+ case "daily":
96
+ var dc = DateComponents()
97
+ dc.hour = hm.hour
98
+ dc.minute = hm.minute
99
+ requests.append(request(base, a, UNCalendarNotificationTrigger(dateMatching: dc, repeats: true)))
100
+
101
+ case "weekly":
102
+ let days = a.weekdays.isEmpty ? Array(1...7) : a.weekdays.filter { (1...7).contains($0) }
103
+ for d in days {
104
+ var dc = DateComponents()
105
+ dc.weekday = d
106
+ dc.hour = hm.hour
107
+ dc.minute = hm.minute
108
+ requests.append(request(base + ":wd\(d)", a, UNCalendarNotificationTrigger(dateMatching: dc, repeats: true)))
109
+ }
110
+
111
+ case "interval":
112
+ // iOS repeating interval triggers count from "now", not from `timestamp`, so
113
+ // pre-schedule the next N exact occurrences instead.
114
+ var t = a.nextTrigger()
115
+ guard t > 0 else { return completion(nil) }
116
+ for k in 0..<Self.intervalOccurrences {
117
+ requests.append(request(base + ":i\(k)", a, exactTrigger(ms: t)))
118
+ t += a.intervalMs
119
+ }
120
+
121
+ default:
122
+ let next = a.nextTrigger()
123
+ guard next > 0 else { return completion(nil) } // nothing in the future, same as Android
124
+ requests.append(request(base, a, exactTrigger(ms: next)))
125
+ }
126
+
127
+ add(requests, completion: completion)
128
+ }
129
+
130
+ private func exactTrigger(ms: Double) -> UNCalendarNotificationTrigger {
131
+ let dc = Calendar.current.dateComponents(
132
+ [.year, .month, .day, .hour, .minute, .second],
133
+ from: Date(timeIntervalSince1970: ms / 1000)
134
+ )
135
+ return UNCalendarNotificationTrigger(dateMatching: dc, repeats: false)
136
+ }
137
+
138
+ private func add(_ requests: [UNNotificationRequest], completion: @escaping (Error?) -> Void) {
139
+ let group = DispatchGroup()
140
+ var firstError: Error?
141
+ for r in requests {
142
+ group.enter()
143
+ center.add(r) { error in
144
+ if firstError == nil { firstError = error }
145
+ group.leave()
146
+ }
147
+ }
148
+ group.notify(queue: .main) { completion(firstError) }
149
+ }
150
+
151
+ /// One-off re-ring in `minutes`, equivalent to Android's `AlarmScheduler.scheduleSnooze`.
152
+ func snooze(_ a: AlarmData, minutes: Int) {
153
+ center.removeDeliveredNotifications(withIdentifiers: ids(for: a.id))
154
+ let trigger = UNTimeIntervalNotificationTrigger(timeInterval: Double(max(1, minutes)) * 60, repeats: false)
155
+ center.add(request(Self.prefix + a.id + ":snooze", a, trigger))
156
+ if ringing?.id == a.id { ringing = nil }
157
+ }
158
+
159
+ func cancel(_ id: String) {
160
+ center.removePendingNotificationRequests(withIdentifiers: ids(for: id))
161
+ center.removeDeliveredNotifications(withIdentifiers: ids(for: id))
162
+ if ringing?.id == id { ringing = nil }
163
+ }
164
+
165
+ func cancelAll() {
166
+ center.getPendingNotificationRequests { (requests: [UNNotificationRequest]) in
167
+ let ours = requests.map { $0.identifier }.filter { $0.hasPrefix(Self.prefix) }
168
+ self.center.removePendingNotificationRequests(withIdentifiers: ours)
169
+ }
170
+ center.getDeliveredNotifications { (notifications: [UNNotification]) in
171
+ let ours = notifications.map { $0.request.identifier }.filter { $0.hasPrefix(Self.prefix) }
172
+ self.center.removeDeliveredNotifications(withIdentifiers: ours)
173
+ }
174
+ ringing = nil
175
+ }
176
+
177
+ /// Dismiss: clear the shown notification and, like Android, drop a pending snooze.
178
+ func stop(_ id: String) {
179
+ center.removeDeliveredNotifications(withIdentifiers: ids(for: id))
180
+ center.removePendingNotificationRequests(withIdentifiers: [Self.prefix + id + ":snooze"])
181
+ if let a = ringing, a.id == id {
182
+ ringing = nil
183
+ onEvent?(AlarmModule.eventDismissed, a)
184
+ }
185
+ }
186
+
187
+ // MARK: - UNUserNotificationCenterDelegate
188
+
189
+ private func alarm(from notification: UNNotification) -> AlarmData? {
190
+ guard let id = notification.request.content.userInfo["alarmId"] as? String else { return nil }
191
+ return AlarmStore.get(id)
192
+ }
193
+
194
+ func userNotificationCenter(
195
+ _ center: UNUserNotificationCenter,
196
+ willPresent notification: UNNotification,
197
+ withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
198
+ ) {
199
+ if let a = alarm(from: notification) {
200
+ ringing = a
201
+ onEvent?(AlarmModule.eventFired, a)
202
+ }
203
+ completionHandler([.banner, .list, .sound])
204
+ }
205
+
206
+ func userNotificationCenter(
207
+ _ center: UNUserNotificationCenter,
208
+ didReceive response: UNNotificationResponse,
209
+ withCompletionHandler completionHandler: @escaping () -> Void
210
+ ) {
211
+ defer { completionHandler() }
212
+ guard let a = alarm(from: response.notification) else { return }
213
+ switch response.actionIdentifier {
214
+ case Self.actionSnooze:
215
+ snooze(a, minutes: a.snoozeMinutes)
216
+ onEvent?(AlarmModule.eventSnoozed, a)
217
+ case UNNotificationDefaultActionIdentifier:
218
+ // Tapped: the app is now open; let JS show its ring UI (getRingingAlarm / onAlarmFired).
219
+ ringing = a
220
+ onEvent?(AlarmModule.eventFired, a)
221
+ default: // Stop action or swiped away
222
+ ringing = nil
223
+ center.removePendingNotificationRequests(withIdentifiers: [Self.prefix + a.id + ":snooze"])
224
+ onEvent?(AlarmModule.eventDismissed, a)
225
+ }
226
+ }
227
+ }
package/package.json CHANGED
@@ -1,6 +1,94 @@
1
1
  {
2
2
  "name": "react-native-alarms",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.0.2",
4
+ "description": "A powerful, cross-platform alarm management library for React Native",
5
+ "private": false,
6
+ "main": "index.ts",
7
+ "types": "index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./index.d.ts",
11
+ "default": "./index.ts"
12
+ },
13
+ "./service": {
14
+ "types": "./AlarmService.d.ts",
15
+ "default": "./AlarmService.ts"
16
+ }
17
+ },
18
+ "scripts": {
19
+ "android": "react-native run-android",
20
+ "ios": "react-native run-ios",
21
+ "lint": "eslint .",
22
+ "start": "react-native start",
23
+ "test": "jest",
24
+ "prepare": "npm run build",
25
+ "build": "tsc",
26
+ "clean": "rm -rf dist types *.d.ts"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/surajprasad13/react-native-alarms"
31
+ },
32
+ "keywords": [
33
+ "react-native",
34
+ "alarm",
35
+ "notification",
36
+ "schedule",
37
+ "cross-platform",
38
+ "ios",
39
+ "android"
40
+ ],
41
+ "author": {
42
+ "name": "Suraj Prasad",
43
+ "email": "surajvishwakarma830@gmail.com",
44
+ "url": "https://github.com/surajprasad13"
45
+ },
46
+ "license": "MIT",
47
+ "peerDependencies": {
48
+ "react": ">=18.0.0",
49
+ "react-native": ">=0.70.0"
50
+ },
51
+ "dependencies": {
52
+ "react": "19.3.0",
53
+ "react-native": "0.88.0",
54
+ "@react-native/new-app-screen": "0.88.0",
55
+ "react-native-safe-area-context": "^5.5.2"
56
+ },
57
+ "devDependencies": {
58
+ "@babel/core": "^7.25.2",
59
+ "@babel/preset-env": "^7.25.3",
60
+ "@babel/runtime": "^7.25.0",
61
+ "@react-native-community/cli": "20.2.0",
62
+ "@react-native-community/cli-platform-android": "20.2.0",
63
+ "@react-native-community/cli-platform-ios": "20.2.0",
64
+ "@react-native/babel-preset": "0.88.0",
65
+ "@react-native/eslint-config": "0.88.0",
66
+ "@react-native/jest-preset": "0.88.0",
67
+ "@react-native/metro-config": "0.88.0",
68
+ "@react-native/typescript-config": "0.88.0",
69
+ "@types/jest": "^29.5.13",
70
+ "@types/react": "^19.2.0",
71
+ "@types/react-test-renderer": "^19.1.0",
72
+ "eslint": "^8.19.0",
73
+ "jest": "^29.6.3",
74
+ "prettier": "2.8.8",
75
+ "react-test-renderer": "19.3.0",
76
+ "typescript": "^6.0.3"
77
+ },
78
+ "engines": {
79
+ "node": ">= 22.11.0"
80
+ },
81
+ "codegenConfig": {
82
+ "name": "RNAlarmSpec",
83
+ "type": "modules",
84
+ "jsSrcsDir": "specs",
85
+ "android": {
86
+ "javaPackageName": "com.reactnativealarm.alarm"
87
+ },
88
+ "ios": {
89
+ "modulesProvider": {
90
+ "Alarm": "Alarm"
91
+ }
92
+ }
93
+ }
6
94
  }
@@ -0,0 +1,95 @@
1
+ import type { TurboModule } from 'react-native';
2
+ import { TurboModuleRegistry } from 'react-native';
3
+
4
+ /**
5
+ * Shared contract of the native `Alarm` module.
6
+ *
7
+ * Both platforms register a legacy-style module named `Alarm` with these exact
8
+ * method names (Android: AlarmModule.kt, iOS: ios/alarm/AlarmModule.swift) and
9
+ * emit the same events through `NativeEventEmitter`:
10
+ * onAlarmFired | onAlarmDismissed | onAlarmSnoozed -> AlarmEvent
11
+ */
12
+
13
+ /** type: 'none' | 'daily' | 'weekly' | 'interval' */
14
+ export type AlarmRepeat = {
15
+ type: string;
16
+ weekdays?: number[]; // 1 = Sunday ... 7 = Saturday (weekly)
17
+ intervalMs?: number; // interval
18
+ };
19
+
20
+ export type AlarmConfig = {
21
+ alarmId?: string;
22
+ timestamp: number; // ms since epoch
23
+ title: string;
24
+ body?: string;
25
+ sound?: string; // 'default' or a bundled sound file name
26
+ vibrate?: boolean; // Android only (iOS alarms always vibrate per system settings)
27
+ snoozeEnabled?: boolean;
28
+ snoozeMinutes?: number;
29
+ repeat?: AlarmRepeat;
30
+ data?: string; // JSON string payload
31
+ };
32
+
33
+ export type Alarm = {
34
+ alarmId: string;
35
+ timestamp: number;
36
+ title: string;
37
+ body: string;
38
+ sound: string;
39
+ vibrate: boolean;
40
+ snoozeEnabled: boolean;
41
+ snoozeMinutes: number;
42
+ repeat: AlarmRepeat;
43
+ data: string;
44
+ enabled: boolean;
45
+ nextTriggerAt: number; // 0 if none
46
+ };
47
+
48
+ /** status: 'granted' | 'denied' | 'notDetermined' */
49
+ export type AlarmPermission = {
50
+ status: string;
51
+ notifications: boolean;
52
+ canScheduleExact: boolean;
53
+ canDrawOverlays: boolean; // Android only; always true on iOS
54
+ canUseFullScreenIntent: boolean; // Android only; always true on iOS
55
+ backend?: string; // iOS only: 'alarmkit' (iOS 26+) | 'notifications'
56
+ };
57
+
58
+ export type AlarmEvent = {
59
+ alarmId: string;
60
+ timestamp: number;
61
+ title: string;
62
+ body: string;
63
+ data: string;
64
+ };
65
+
66
+ export interface Spec extends TurboModule {
67
+ // ---------- Permissions ----------
68
+ checkPermission(): Promise<AlarmPermission>;
69
+ requestPermission(): Promise<AlarmPermission>;
70
+ canScheduleExactAlarms(): Promise<boolean>;
71
+ /** Opens the vendor Autostart screen on Android; resolves false on iOS. */
72
+ openAutoStartSettings(): Promise<boolean>;
73
+
74
+ // ---------- Scheduling ----------
75
+ scheduleAlarm(config: AlarmConfig): Promise<string>;
76
+ updateAlarm(alarmId: string, config: AlarmConfig): Promise<boolean>;
77
+ cancelAlarm(alarmId: string): Promise<boolean>;
78
+ cancelAllAlarms(): Promise<void>;
79
+
80
+ // ---------- Control (while ringing) ----------
81
+ stopAlarm(alarmId: string): Promise<void>;
82
+ snoozeAlarm(alarmId: string, minutes: number): Promise<void>;
83
+ getRingingAlarm(): Promise<Alarm | null>;
84
+
85
+ // ---------- Queries ----------
86
+ getAlarm(alarmId: string): Promise<Alarm | null>;
87
+ getAllAlarms(): Promise<Alarm[]>;
88
+ isScheduled(alarmId: string): Promise<boolean>;
89
+
90
+ // ---------- Events (used by NativeEventEmitter) ----------
91
+ addListener(eventName: string): void;
92
+ removeListeners(count: number): void;
93
+ }
94
+
95
+ export default TurboModuleRegistry.getEnforcing<Spec>('Alarm');