react-native-alarms 0.0.4 → 0.0.6
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 +5 -0
- package/AlarmService.ts +15 -3
- package/LICENSE.md +21 -0
- package/README.md +16 -0
- package/commitlint.config.mjs +1 -0
- package/ios/alarm/AlarmKitBackend.swift +94 -27
- package/ios/alarm/AlarmModule.swift +21 -4
- package/ios/alarm/AlarmRingPresenter.swift +22 -4
- package/ios/alarm/NotificationBackend.swift +6 -0
- package/package.json +5 -2
- 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/utils/index.ts +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
npx --no -- commitlint --edit $1
|
|
File without changes
|
package/AGENTS.md
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
`react-native-alarms` is a React Native library designed to provide reliable alarm functionality across Android and iOS.
|
|
6
|
+
|
|
7
|
+
The library may include JavaScript/TypeScript APIs, native Android and iOS implementations, alarm scheduling, notifications, event handling, and custom alarm interfaces.
|
|
8
|
+
|
|
9
|
+
The primary goals are:
|
|
10
|
+
|
|
11
|
+
- Reliability across supported platforms and operating-system versions.
|
|
12
|
+
- Predictable alarm scheduling, cancellation, and lifecycle behavior.
|
|
13
|
+
- Efficient resource and battery usage.
|
|
14
|
+
- A consistent developer experience for React Native applications.
|
|
15
|
+
- Backward compatibility and maintainable native code.
|
|
16
|
+
|
|
17
|
+
**Important:** Treat the existing implementation and documented behavior as the source of truth. Do not assume that a feature is supported on both platforms until you have verified the implementation.
|
|
18
|
+
|
|
19
|
+
## Instructions for AI Coding Agents
|
|
20
|
+
|
|
21
|
+
Before making changes:
|
|
22
|
+
|
|
23
|
+
1. Inspect the repository structure and `package.json`.
|
|
24
|
+
2. Read `README.md`, `CONTRIBUTING.md`, and relevant documentation.
|
|
25
|
+
3. Inspect the existing implementation before proposing a replacement.
|
|
26
|
+
4. Identify whether the requested change affects JavaScript/TypeScript, Android, iOS, or multiple layers.
|
|
27
|
+
5. Review related tests, configuration files, and existing patches.
|
|
28
|
+
6. Understand the expected behavior and platform-specific constraints before editing code.
|
|
29
|
+
|
|
30
|
+
Do not make broad or unrelated changes when a focused fix is sufficient.
|
|
31
|
+
|
|
32
|
+
### General Rules
|
|
33
|
+
|
|
34
|
+
- Preserve existing project conventions and architecture.
|
|
35
|
+
- Keep changes small, focused, and easy to review.
|
|
36
|
+
- Prefer existing dependencies and utilities over introducing new ones.
|
|
37
|
+
- Avoid unnecessary public API changes.
|
|
38
|
+
- Preserve TypeScript type safety.
|
|
39
|
+
- Handle errors and edge cases explicitly.
|
|
40
|
+
- Do not silently swallow errors.
|
|
41
|
+
- Do not modify generated files unless required by the project's build process.
|
|
42
|
+
- Never commit secrets, credentials, signing keys, or local environment files.
|
|
43
|
+
- Do not claim a test, build, or device scenario passed unless it was actually verified.
|
|
44
|
+
- Clearly identify any behavior that remains untested.
|
|
45
|
+
|
|
46
|
+
## Repository Exploration
|
|
47
|
+
|
|
48
|
+
Before changing files, inspect the available project scripts and directory layout.
|
|
49
|
+
|
|
50
|
+
Useful commands:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
cat package.json
|
|
54
|
+
find . -maxdepth 3 -type f | sort
|
|
55
|
+
git status --short
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Review the repository's actual structure rather than assuming specific directories, frameworks, or build commands exist.
|
|
59
|
+
|
|
60
|
+
Do not overwrite user changes or discard uncommitted work.
|
|
61
|
+
|
|
62
|
+
## JavaScript and TypeScript Guidelines
|
|
63
|
+
|
|
64
|
+
- Follow the existing formatting and naming conventions.
|
|
65
|
+
- Use explicit types for public APIs and event payloads.
|
|
66
|
+
- Keep platform-independent business logic separate from native implementation details.
|
|
67
|
+
- Validate inputs before invoking native modules.
|
|
68
|
+
- Maintain consistent error handling.
|
|
69
|
+
- Clean up event subscriptions and listeners when no longer required.
|
|
70
|
+
- Avoid memory leaks, duplicated listeners, and unnecessary re-renders.
|
|
71
|
+
- Document public methods, configuration options, and callbacks.
|
|
72
|
+
|
|
73
|
+
### Public API Compatibility
|
|
74
|
+
|
|
75
|
+
Before changing an exported method, event, property, or type:
|
|
76
|
+
|
|
77
|
+
1. Search for all usages in the repository.
|
|
78
|
+
2. Review the documented public API.
|
|
79
|
+
3. Check whether existing consumers could be affected.
|
|
80
|
+
4. Add or update tests.
|
|
81
|
+
5. Document any breaking changes and migration steps.
|
|
82
|
+
|
|
83
|
+
Do not rename or remove public exports without a clear requirement.
|
|
84
|
+
|
|
85
|
+
## Android Development Guidelines
|
|
86
|
+
|
|
87
|
+
When modifying Android native code:
|
|
88
|
+
|
|
89
|
+
- Follow the existing Java or Kotlin conventions used by the project.
|
|
90
|
+
- Respect Android application and activity lifecycle requirements.
|
|
91
|
+
- Check the minimum SDK and supported Android versions before using APIs.
|
|
92
|
+
- Handle notification and exact-alarm permissions where applicable.
|
|
93
|
+
- Respect Android background-execution and battery-management restrictions.
|
|
94
|
+
- Avoid unnecessary wake locks and background resource consumption.
|
|
95
|
+
- Ensure pending intents and alarm identifiers behave predictably.
|
|
96
|
+
- Handle cancellation, rescheduling, and duplicate requests correctly.
|
|
97
|
+
- Clean up receivers, callbacks, and other resources when appropriate.
|
|
98
|
+
- Follow applicable Android security and platform policies.
|
|
99
|
+
|
|
100
|
+
### Android Alarm Behavior
|
|
101
|
+
|
|
102
|
+
When modifying alarm scheduling or delivery, consider:
|
|
103
|
+
|
|
104
|
+
- Scheduling and cancellation.
|
|
105
|
+
- Repeated alarms.
|
|
106
|
+
- Device reboot behavior, if supported.
|
|
107
|
+
- Application foreground and background states.
|
|
108
|
+
- Process termination.
|
|
109
|
+
- Notification delivery.
|
|
110
|
+
- Full-screen intent restrictions and permissions.
|
|
111
|
+
- Doze mode and battery optimization.
|
|
112
|
+
- Android version and manufacturer differences.
|
|
113
|
+
|
|
114
|
+
Do not promise guaranteed delivery under every system condition. Verify the actual implementation and account for platform restrictions.
|
|
115
|
+
|
|
116
|
+
## iOS Development Guidelines
|
|
117
|
+
|
|
118
|
+
When modifying iOS native code:
|
|
119
|
+
|
|
120
|
+
- Follow the existing Swift or Objective-C conventions.
|
|
121
|
+
- Respect UIKit and application lifecycle requirements.
|
|
122
|
+
- Use the deployment target defined by the project.
|
|
123
|
+
- Handle notification authorization and scheduling correctly.
|
|
124
|
+
- Manage observers, callbacks, timers, and native resources safely.
|
|
125
|
+
- Avoid retain cycles and resource leaks.
|
|
126
|
+
- Handle app foreground and background transitions.
|
|
127
|
+
- Preserve compatibility with supported iOS versions.
|
|
128
|
+
- Respect system restrictions on background execution and alarm-like behavior.
|
|
129
|
+
|
|
130
|
+
### iOS Alarm Behavior
|
|
131
|
+
|
|
132
|
+
When working on alarm functionality, consider:
|
|
133
|
+
|
|
134
|
+
- Alarm scheduling and cancellation.
|
|
135
|
+
- Notification authorization.
|
|
136
|
+
- Application lifecycle transitions.
|
|
137
|
+
- Audio session configuration, where applicable.
|
|
138
|
+
- Repeated alarms and rescheduling.
|
|
139
|
+
- Device restart behavior, where supported.
|
|
140
|
+
- System-managed alarm capabilities, where available.
|
|
141
|
+
- Custom alarm interfaces and their presentation constraints.
|
|
142
|
+
|
|
143
|
+
Do not assume an application can execute arbitrary code or present a custom interface while suspended or terminated. Verify platform capabilities and use supported APIs.
|
|
144
|
+
|
|
145
|
+
## Cross-Platform Behavior
|
|
146
|
+
|
|
147
|
+
Android and iOS do not provide identical capabilities.
|
|
148
|
+
|
|
149
|
+
When implementing a cross-platform feature:
|
|
150
|
+
|
|
151
|
+
1. Define the expected public API.
|
|
152
|
+
2. Identify platform-specific behavior.
|
|
153
|
+
3. Implement each platform using its supported mechanisms.
|
|
154
|
+
4. Return consistent results where practical.
|
|
155
|
+
5. Document differences and limitations.
|
|
156
|
+
6. Test both platforms when the change affects shared behavior.
|
|
157
|
+
|
|
158
|
+
Never simulate unsupported operating-system guarantees through misleading documentation or API responses.
|
|
159
|
+
|
|
160
|
+
## Event and Lifecycle Management
|
|
161
|
+
|
|
162
|
+
For alarm-related events and callbacks:
|
|
163
|
+
|
|
164
|
+
- Preserve the documented event names and payload structure.
|
|
165
|
+
- Avoid firing the same event multiple times for one logical transition.
|
|
166
|
+
- Ensure cancellation and dismissal are handled consistently.
|
|
167
|
+
- Clean up native and JavaScript listeners appropriately.
|
|
168
|
+
- Avoid race conditions between scheduling, firing, dismissal, and snoozing.
|
|
169
|
+
- Ensure callbacks do not unintentionally retain obsolete objects.
|
|
170
|
+
- Consider repeated initialization and application lifecycle transitions.
|
|
171
|
+
|
|
172
|
+
Test event sequences, not just individual callbacks.
|
|
173
|
+
|
|
174
|
+
## Dependencies and Configuration
|
|
175
|
+
|
|
176
|
+
Before adding or upgrading dependencies:
|
|
177
|
+
|
|
178
|
+
- Verify React Native compatibility.
|
|
179
|
+
- Review peer dependencies.
|
|
180
|
+
- Check Android and iOS requirements.
|
|
181
|
+
- Consider bundle size and runtime overhead.
|
|
182
|
+
- Avoid unnecessary native dependencies.
|
|
183
|
+
- Update lockfiles when required by the project's package manager.
|
|
184
|
+
- Document any additional setup steps.
|
|
185
|
+
|
|
186
|
+
Do not change the package manager or lockfile format without a clear reason.
|
|
187
|
+
|
|
188
|
+
## Testing Requirements
|
|
189
|
+
|
|
190
|
+
Inspect `package.json` and existing project configuration to determine which checks are available.
|
|
191
|
+
|
|
192
|
+
Run the applicable checks before completing a change, such as:
|
|
193
|
+
|
|
194
|
+
- Linting.
|
|
195
|
+
- Type checking.
|
|
196
|
+
- Unit tests.
|
|
197
|
+
- Example application builds.
|
|
198
|
+
- Android native compilation.
|
|
199
|
+
- iOS native compilation.
|
|
200
|
+
- Manual device or emulator tests.
|
|
201
|
+
|
|
202
|
+
Do not invent npm scripts or assume a test framework is installed.
|
|
203
|
+
|
|
204
|
+
### Alarm-Specific Test Scenarios
|
|
205
|
+
|
|
206
|
+
Where relevant, verify:
|
|
207
|
+
|
|
208
|
+
- Creating and scheduling an alarm.
|
|
209
|
+
- Cancelling a scheduled alarm.
|
|
210
|
+
- Rescheduling an existing alarm.
|
|
211
|
+
- Handling duplicate scheduling requests.
|
|
212
|
+
- Triggering the correct event.
|
|
213
|
+
- Dismissing and snoozing alarms.
|
|
214
|
+
- Handling denied permissions.
|
|
215
|
+
- Managing foreground and background transitions.
|
|
216
|
+
- Releasing resources and event listeners.
|
|
217
|
+
- Handling supported reboot and process-restart scenarios.
|
|
218
|
+
|
|
219
|
+
Test on real devices when system-level behavior cannot be reliably validated in a simulator.
|
|
220
|
+
|
|
221
|
+
If only one platform is tested, state which platform remains unverified.
|
|
222
|
+
|
|
223
|
+
## Build and Debugging
|
|
224
|
+
|
|
225
|
+
When investigating build failures:
|
|
226
|
+
|
|
227
|
+
1. Capture the complete error message.
|
|
228
|
+
2. Identify the failing project, target, task, or script.
|
|
229
|
+
3. Determine whether the failure is caused by the current change or the existing environment.
|
|
230
|
+
4. Make the smallest appropriate fix.
|
|
231
|
+
5. Re-run the failing check.
|
|
232
|
+
6. Inspect the final diff.
|
|
233
|
+
|
|
234
|
+
Avoid destructive cleanup commands unless necessary. Never delete project files, signing configuration, or user changes simply to make a build pass.
|
|
235
|
+
|
|
236
|
+
## Documentation Requirements
|
|
237
|
+
|
|
238
|
+
Update documentation whenever a change affects:
|
|
239
|
+
|
|
240
|
+
- Installation or native setup.
|
|
241
|
+
- Public methods and configuration options.
|
|
242
|
+
- Permissions or platform requirements.
|
|
243
|
+
- Supported operating-system versions.
|
|
244
|
+
- Events and callback payloads.
|
|
245
|
+
- Alarm scheduling semantics.
|
|
246
|
+
- Known limitations.
|
|
247
|
+
- Troubleshooting instructions.
|
|
248
|
+
|
|
249
|
+
Examples must match the actual implementation.
|
|
250
|
+
|
|
251
|
+
## Git and Code Review
|
|
252
|
+
|
|
253
|
+
Before committing changes:
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
git status --short
|
|
257
|
+
git diff --check
|
|
258
|
+
git diff
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Follow the commit conventions in `CONTRIBUTING.md`.
|
|
262
|
+
|
|
263
|
+
Prefer focused commits and pull requests. Do not rewrite Git history, force-push, or amend existing commits unless explicitly requested.
|
|
264
|
+
|
|
265
|
+
Never include unrelated changes in a pull request.
|
|
266
|
+
|
|
267
|
+
## Security and Privacy
|
|
268
|
+
|
|
269
|
+
- Do not hardcode credentials or API keys.
|
|
270
|
+
- Do not log sensitive information unnecessarily.
|
|
271
|
+
- Validate data crossing the JavaScript/native boundary.
|
|
272
|
+
- Use appropriate Android permissions and iOS authorization mechanisms.
|
|
273
|
+
- Follow platform security requirements.
|
|
274
|
+
- Report security-sensitive issues through the repository's designated private reporting channel.
|
|
275
|
+
|
|
276
|
+
## Completion Checklist
|
|
277
|
+
|
|
278
|
+
Before declaring a task complete:
|
|
279
|
+
|
|
280
|
+
- [ ] The requested behavior is implemented.
|
|
281
|
+
- [ ] Existing functionality and public APIs are preserved where possible.
|
|
282
|
+
- [ ] Relevant platform-specific requirements are respected.
|
|
283
|
+
- [ ] Tests or builds have been run where available.
|
|
284
|
+
- [ ] Changes have been reviewed with `git diff`.
|
|
285
|
+
- [ ] Documentation has been updated where needed.
|
|
286
|
+
- [ ] Limitations and untested scenarios are clearly stated.
|
|
287
|
+
- [ ] No unrelated changes, credentials, or generated artifacts were added.
|
|
288
|
+
|
|
289
|
+
## Final Response Requirements
|
|
290
|
+
|
|
291
|
+
When reporting completed
|
package/AlarmService.d.ts
CHANGED
|
@@ -107,6 +107,11 @@ export class AlarmService {
|
|
|
107
107
|
* Check if an alarm is scheduled
|
|
108
108
|
*/
|
|
109
109
|
static isScheduled(alarmId: string): Promise<boolean>;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Opens the vendor Autostart screen on Android; resolves false on iOS.
|
|
113
|
+
*/
|
|
114
|
+
static openAutoStartSettings(): Promise<boolean>;
|
|
110
115
|
}
|
|
111
116
|
|
|
112
117
|
export default AlarmService;
|
package/AlarmService.ts
CHANGED
|
@@ -45,13 +45,19 @@ export class AlarmService {
|
|
|
45
45
|
if (!alarmEmitter || !this.subscriptions.length) {
|
|
46
46
|
this.subscriptions = [
|
|
47
47
|
alarmEmitter?.addListener('onAlarmFired', (data: any) => {
|
|
48
|
-
this.listeners.fired.forEach(listener =>
|
|
48
|
+
this.listeners.fired.forEach(listener =>
|
|
49
|
+
listener(data as AlarmEvent),
|
|
50
|
+
);
|
|
49
51
|
}),
|
|
50
52
|
alarmEmitter?.addListener('onAlarmDismissed', (data: any) => {
|
|
51
|
-
this.listeners.dismissed.forEach(listener =>
|
|
53
|
+
this.listeners.dismissed.forEach(listener =>
|
|
54
|
+
listener(data as AlarmEvent),
|
|
55
|
+
);
|
|
52
56
|
}),
|
|
53
57
|
alarmEmitter?.addListener('onAlarmSnoozed', (data: any) => {
|
|
54
|
-
this.listeners.snoozed.forEach(listener =>
|
|
58
|
+
this.listeners.snoozed.forEach(listener =>
|
|
59
|
+
listener(data as AlarmEvent),
|
|
60
|
+
);
|
|
55
61
|
}),
|
|
56
62
|
].filter(Boolean);
|
|
57
63
|
}
|
|
@@ -190,6 +196,12 @@ export class AlarmService {
|
|
|
190
196
|
if (!alarm) throw new Error('Alarm module not linked');
|
|
191
197
|
return alarm.isScheduled(alarmId);
|
|
192
198
|
}
|
|
199
|
+
|
|
200
|
+
/** Opens the vendor Autostart screen on Android; resolves false on iOS. */
|
|
201
|
+
static async openAutoStartSettings(): Promise<boolean> {
|
|
202
|
+
if (!alarm) throw new Error('Alarm module not linked');
|
|
203
|
+
return alarm.openAutoStartSettings();
|
|
204
|
+
}
|
|
193
205
|
}
|
|
194
206
|
|
|
195
207
|
// Export a singleton instance
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 react-native-alarm contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -361,6 +361,22 @@ const styles = StyleSheet.create({
|
|
|
361
361
|
});
|
|
362
362
|
```
|
|
363
363
|
|
|
364
|
+
## 📸 Screenshots, Demos & Examples
|
|
365
|
+
|
|
366
|
+
Explore how `react-native-alarms` works on Android and iOS.
|
|
367
|
+
|
|
368
|
+
### 🖼️ Screenshots
|
|
369
|
+
|
|
370
|
+
| Sample 1 | Sample 2 | Sample 3 |
|
|
371
|
+
|---|---|---|
|
|
372
|
+
| <img src="src/assets/sample-1.png" width="180" alt="Sample 1" /> | <img src="src/assets/sample-2.png" width="180" alt="Sample 2" /> | <img src="src/assets/sample-3.png" width="180" alt="Sample 3" /> |
|
|
373
|
+
|
|
374
|
+
### 🎥 Demo Video
|
|
375
|
+
|
|
376
|
+
Watch the library in action, including alarm scheduling, ringing, and Stop/Snooze interactions.
|
|
377
|
+
|
|
378
|
+
[▶️ Watch the React Native Alarms Demo](src/assets/recording.mp4)
|
|
379
|
+
|
|
364
380
|
## Troubleshooting
|
|
365
381
|
|
|
366
382
|
### "Alarm Native module is not linked"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default { extends: ['@commitlint/config-conventional'] };
|
|
@@ -5,11 +5,34 @@ import SwiftUI
|
|
|
5
5
|
#if canImport(AlarmKit)
|
|
6
6
|
import ActivityKit
|
|
7
7
|
import AlarmKit
|
|
8
|
+
import AppIntents
|
|
8
9
|
|
|
9
10
|
/// Metadata type required by `AlarmAttributes`; extend it if you add a Live Activity widget.
|
|
10
11
|
@available(iOS 26.0, *)
|
|
11
12
|
struct RNAlarmMetadata: AlarmMetadata {}
|
|
12
13
|
|
|
14
|
+
/// Backs the "Open" button on the AlarmKit alert. An AlarmKit alert ignores taps on
|
|
15
|
+
/// its body, so this intent (run in the app process, `openAppWhenRun`) is the only
|
|
16
|
+
/// way the alert can open the app and show the React ring screen.
|
|
17
|
+
@available(iOS 26.0, *)
|
|
18
|
+
struct OpenAlarmAppIntent: LiveActivityIntent {
|
|
19
|
+
static let title: LocalizedStringResource = "Open Alarm"
|
|
20
|
+
static let description = IntentDescription("Opens the ringing alarm's screen")
|
|
21
|
+
static let openAppWhenRun: Bool = true
|
|
22
|
+
static let isDiscoverable: Bool = false
|
|
23
|
+
|
|
24
|
+
@Parameter(title: "alarmID")
|
|
25
|
+
var alarmID: String
|
|
26
|
+
|
|
27
|
+
init() {}
|
|
28
|
+
init(alarmID: String) { self.alarmID = alarmID }
|
|
29
|
+
|
|
30
|
+
func perform() async throws -> some IntentResult {
|
|
31
|
+
AlarmKitBackend.shared.openFromAlert(alarmID)
|
|
32
|
+
return .result()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
13
36
|
/// Real alarms on iOS 26+: ring through silent mode and Focus with the system alarm UI.
|
|
14
37
|
/// Only compiled with an SDK that ships AlarmKit (Xcode 26+); older toolchains build
|
|
15
38
|
/// the app with the notification fallback only.
|
|
@@ -26,6 +49,10 @@ final class AlarmKitBackend {
|
|
|
26
49
|
/// Alarms we are stopping as part of a programmatic snooze (so the observer
|
|
27
50
|
/// reports "snoozed" instead of "dismissed").
|
|
28
51
|
private var snoozing: Set<UUID> = []
|
|
52
|
+
/// Alarms stopped only to silence the system sound while the in-app ring screen
|
|
53
|
+
/// (with its own TTS) is showing. The ring session stays alive until JS calls
|
|
54
|
+
/// stop/snooze, so the observer must not report these as dismissed.
|
|
55
|
+
private var silenced: [UUID: AlarmData] = [:]
|
|
29
56
|
|
|
30
57
|
private static let allWeekdays: [Locale.Weekday] = [
|
|
31
58
|
.sunday, .monday, .tuesday, .wednesday, .thursday, .friday, .saturday,
|
|
@@ -98,23 +125,16 @@ final class AlarmKitBackend {
|
|
|
98
125
|
{
|
|
99
126
|
let title = LocalizedStringResource(stringLiteral: a.title.isEmpty ? "Alarm" : a.title)
|
|
100
127
|
let stop = AlarmButton(text: "Stop", textColor: .white, systemImageName: "stop.fill")
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
title: title, stopButton: stop, secondaryButton: snooze, secondaryButtonBehavior: .countdown
|
|
109
|
-
)
|
|
110
|
-
countdown = AlarmPresentation.Countdown(title: "Snoozed", pauseButton: nil)
|
|
111
|
-
duration = Alarm.CountdownDuration(preAlert: nil, postAlert: Double(max(1, a.snoozeMinutes)) * 60)
|
|
112
|
-
} else {
|
|
113
|
-
alert = AlarmPresentation.Alert(title: title, stopButton: stop)
|
|
114
|
-
}
|
|
128
|
+
// The alert's body ignores taps and it allows a single secondary button, so it
|
|
129
|
+
// must be "Open" (via OpenAlarmAppIntent): the React ring screen it opens has
|
|
130
|
+
// its own Snooze/Finish. The system countdown-snooze button is dropped for it.
|
|
131
|
+
let open = AlarmButton(text: "Open", textColor: .white, systemImageName: "arrow.up.forward.app")
|
|
132
|
+
let alert = AlarmPresentation.Alert(
|
|
133
|
+
title: title, stopButton: stop, secondaryButton: open, secondaryButtonBehavior: .custom
|
|
134
|
+
)
|
|
115
135
|
|
|
116
136
|
let attributes = AlarmAttributes<RNAlarmMetadata>(
|
|
117
|
-
presentation: AlarmPresentation(alert: alert
|
|
137
|
+
presentation: AlarmPresentation(alert: alert),
|
|
118
138
|
metadata: RNAlarmMetadata(),
|
|
119
139
|
tintColor: .blue
|
|
120
140
|
)
|
|
@@ -122,16 +142,18 @@ final class AlarmKitBackend {
|
|
|
122
142
|
(a.sound.isEmpty || a.sound == "default") ? .default : .named(a.sound)
|
|
123
143
|
|
|
124
144
|
return AlarmManager.AlarmConfiguration(
|
|
125
|
-
countdownDuration: duration,
|
|
126
145
|
schedule: schedule,
|
|
127
146
|
attributes: attributes,
|
|
147
|
+
secondaryIntent: OpenAlarmAppIntent(alarmID: a.id),
|
|
128
148
|
sound: sound
|
|
129
149
|
)
|
|
130
150
|
}
|
|
131
151
|
|
|
132
152
|
func cancel(_ id: String) {
|
|
133
|
-
|
|
134
|
-
|
|
153
|
+
for uuid in [Self.uuid(for: id), Self.uuid(for: id, snooze: true)] {
|
|
154
|
+
try? manager.cancel(id: uuid)
|
|
155
|
+
silenced.removeValue(forKey: uuid)
|
|
156
|
+
}
|
|
135
157
|
}
|
|
136
158
|
|
|
137
159
|
func cancelAll() {
|
|
@@ -139,19 +161,60 @@ final class AlarmKitBackend {
|
|
|
139
161
|
}
|
|
140
162
|
|
|
141
163
|
func stop(_ id: String) {
|
|
142
|
-
|
|
143
|
-
|
|
164
|
+
for uuid in [Self.uuid(for: id), Self.uuid(for: id, snooze: true)] {
|
|
165
|
+
try? manager.stop(id: uuid)
|
|
166
|
+
// Ended a session we had silenced earlier: the observer saw nothing change,
|
|
167
|
+
// so report the dismissal (and re-arm intervals) here instead.
|
|
168
|
+
if let a = silenced.removeValue(forKey: uuid) {
|
|
169
|
+
onEvent?(AlarmModule.eventDismissed, a)
|
|
170
|
+
if a.repeatType == "interval" { Task { try? await self.schedule(a) } }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
144
173
|
}
|
|
145
174
|
|
|
146
|
-
///
|
|
147
|
-
///
|
|
148
|
-
|
|
175
|
+
/// "Open" tapped on the system alert (OpenAlarmAppIntent). The tap stops the
|
|
176
|
+
/// AlarmKit alarm, so adopt the session as `silenced` — still ringing as far as
|
|
177
|
+
/// JS is concerned — and show the React ring screen once the app is frontmost.
|
|
178
|
+
func openFromAlert(_ id: String) {
|
|
179
|
+
guard let a = AlarmStore.get(id) else { return }
|
|
149
180
|
let current = (try? manager.alarms) ?? []
|
|
150
181
|
for uuid in [Self.uuid(for: a.id), Self.uuid(for: a.id, snooze: true)]
|
|
151
182
|
where current.contains(where: { $0.id == uuid && $0.state == .alerting }) {
|
|
152
|
-
|
|
183
|
+
silenced[uuid] = a
|
|
184
|
+
try? manager.stop(id: uuid)
|
|
185
|
+
}
|
|
186
|
+
// The system may have stopped it before we ran; the session is still ours.
|
|
187
|
+
if !silenced.values.contains(where: { $0.id == a.id }) {
|
|
188
|
+
silenced[Self.uuid(for: a.id)] = a
|
|
189
|
+
}
|
|
190
|
+
DispatchQueue.main.async { AlarmRingPresenter.present(a, bridge: nil) }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/// Stops the system alert's sound/UI while the in-app ring screen takes over.
|
|
194
|
+
/// The session stays "ringing" (see `silenced`) until JS stops or snoozes it.
|
|
195
|
+
func silence(_ id: String) {
|
|
196
|
+
let current = (try? manager.alarms) ?? []
|
|
197
|
+
for uuid in [Self.uuid(for: id), Self.uuid(for: id, snooze: true)]
|
|
198
|
+
where current.contains(where: { $0.id == uuid && $0.state == .alerting }) {
|
|
199
|
+
silenced[uuid] = stored(for: uuid)
|
|
153
200
|
try? manager.stop(id: uuid)
|
|
154
201
|
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/// Silences the ringing alarm (if any) and re-rings once in `minutes`,
|
|
205
|
+
/// equivalent to Android's `AlarmScheduler.scheduleSnooze`.
|
|
206
|
+
func snooze(_ a: AlarmData, minutes: Int) async throws {
|
|
207
|
+
let current = (try? manager.alarms) ?? []
|
|
208
|
+
var wasSilenced = false
|
|
209
|
+
for uuid in [Self.uuid(for: a.id), Self.uuid(for: a.id, snooze: true)] {
|
|
210
|
+
if silenced.removeValue(forKey: uuid) != nil { wasSilenced = true }
|
|
211
|
+
if current.contains(where: { $0.id == uuid && $0.state == .alerting }) {
|
|
212
|
+
snoozing.insert(uuid)
|
|
213
|
+
try? manager.stop(id: uuid)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Snoozed a session we had silenced: the observer saw nothing change, report it here.
|
|
217
|
+
if wasSilenced { onEvent?(AlarmModule.eventSnoozed, a) }
|
|
155
218
|
let date = Date().addingTimeInterval(Double(max(1, minutes)) * 60)
|
|
156
219
|
_ = try await manager.schedule(
|
|
157
220
|
id: Self.uuid(for: a.id, snooze: true),
|
|
@@ -161,10 +224,11 @@ final class AlarmKitBackend {
|
|
|
161
224
|
}
|
|
162
225
|
|
|
163
226
|
func ringingAlarm() -> AlarmData? {
|
|
164
|
-
|
|
165
|
-
return
|
|
227
|
+
if let alerting = ((try? manager.alarms) ?? []).first(where: { $0.state == .alerting }) {
|
|
228
|
+
return stored(for: alerting.id)
|
|
166
229
|
}
|
|
167
|
-
|
|
230
|
+
// Sound silenced for the in-app ring screen, but the session is still live.
|
|
231
|
+
return silenced.values.first
|
|
168
232
|
}
|
|
169
233
|
|
|
170
234
|
// MARK: - Events
|
|
@@ -187,14 +251,17 @@ final class AlarmKitBackend {
|
|
|
187
251
|
for (uuid, state) in states where lastStates[uuid] != state {
|
|
188
252
|
guard let a = stored(for: uuid) else { continue }
|
|
189
253
|
if state == .alerting {
|
|
254
|
+
silenced.removeValue(forKey: uuid) // ringing again supersedes any old silence
|
|
190
255
|
onEvent?(AlarmModule.eventFired, a)
|
|
191
256
|
} else if lastStates[uuid] == .alerting {
|
|
257
|
+
if silenced[uuid] != nil { continue } // silenced for the ring screen, not over yet
|
|
192
258
|
onEvent?(state == .countdown ? AlarmModule.eventSnoozed : AlarmModule.eventDismissed, a)
|
|
193
259
|
}
|
|
194
260
|
}
|
|
195
261
|
|
|
196
262
|
// One-off (`.fixed`) alarms disappear once stopped.
|
|
197
263
|
for (uuid, previous) in lastStates where states[uuid] == nil && previous == .alerting {
|
|
264
|
+
if silenced[uuid] != nil { continue } // silenced for the ring screen, not over yet
|
|
198
265
|
guard let a = stored(for: uuid) else { continue }
|
|
199
266
|
if snoozing.remove(uuid) != nil {
|
|
200
267
|
onEvent?(AlarmModule.eventSnoozed, a)
|
|
@@ -16,6 +16,7 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
16
16
|
static let eventSnoozed = "onAlarmSnoozed"
|
|
17
17
|
|
|
18
18
|
private var hasListeners = false
|
|
19
|
+
private var activeObserver: NSObjectProtocol?
|
|
19
20
|
|
|
20
21
|
override init() {
|
|
21
22
|
super.init()
|
|
@@ -27,10 +28,26 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
27
28
|
AlarmKitBackend.shared.startObserving()
|
|
28
29
|
}
|
|
29
30
|
#endif
|
|
30
|
-
// App launched by tapping the alarm notification before this module existed.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
// App launched by tapping the alarm notification/alert before this module existed.
|
|
32
|
+
DispatchQueue.main.async { [weak self] in self?.presentIfRinging() }
|
|
33
|
+
// Tapping an AlarmKit alert (and some notification taps on a warm start) only
|
|
34
|
+
// foregrounds the app without any delegate callback, so also check on activation.
|
|
35
|
+
activeObserver = NotificationCenter.default.addObserver(
|
|
36
|
+
forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main
|
|
37
|
+
) { [weak self] _ in self?.presentIfRinging() }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
deinit {
|
|
41
|
+
if let activeObserver { NotificationCenter.default.removeObserver(activeObserver) }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Shows the ring screen when a backend says an alarm is ringing right now.
|
|
45
|
+
private func presentIfRinging() {
|
|
46
|
+
var ringing = NotificationBackend.shared.ringing
|
|
47
|
+
#if canImport(AlarmKit)
|
|
48
|
+
if #available(iOS 26.0, *), ringing == nil { ringing = AlarmKitBackend.shared.ringingAlarm() }
|
|
49
|
+
#endif
|
|
50
|
+
if let ringing { AlarmRingPresenter.present(ringing, bridge: bridge) }
|
|
34
51
|
}
|
|
35
52
|
|
|
36
53
|
private func handle(_ name: String, _ alarm: AlarmData) {
|
|
@@ -15,12 +15,17 @@ final class AlarmRingPresenter {
|
|
|
15
15
|
|
|
16
16
|
static func present(_ alarm: AlarmData, bridge: RCTBridge?, attempt: Int = 0) {
|
|
17
17
|
DispatchQueue.main.async {
|
|
18
|
-
if controller != nil, alarmId == alarm.id {
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
if controller != nil, alarmId == alarm.id {
|
|
19
|
+
// Already showing (e.g. presented while backgrounded, now foregrounded):
|
|
20
|
+
// still make sure the native sound yields to the screen's own TTS.
|
|
21
|
+
silenceIfActive(alarm.id)
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
// Cold start from a notification tap or the AlarmKit "Open" button: the root
|
|
25
|
+
// surface or window may not be ready yet, so retry for up to ~15 s.
|
|
21
26
|
guard let surface = makeSurface(bridge, props: alarm.toDictionary()),
|
|
22
27
|
let top = topViewController() else {
|
|
23
|
-
if attempt <
|
|
28
|
+
if attempt < 60 {
|
|
24
29
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
|
25
30
|
present(alarm, bridge: bridge, attempt: attempt + 1)
|
|
26
31
|
}
|
|
@@ -42,6 +47,7 @@ final class AlarmRingPresenter {
|
|
|
42
47
|
top.present(vc, animated: true)
|
|
43
48
|
controller = vc
|
|
44
49
|
alarmId = alarm.id
|
|
50
|
+
silenceIfActive(alarm.id)
|
|
45
51
|
}
|
|
46
52
|
if let existing = controller {
|
|
47
53
|
existing.dismiss(animated: false, completion: show)
|
|
@@ -51,6 +57,18 @@ final class AlarmRingPresenter {
|
|
|
51
57
|
}
|
|
52
58
|
}
|
|
53
59
|
|
|
60
|
+
/// The ring screen speaks the task via TTS, and iOS won't play it over the
|
|
61
|
+
/// notification / AlarmKit sound, so stop that sound once the screen is up.
|
|
62
|
+
/// Only while the app is frontmost: in the background the system alert is the
|
|
63
|
+
/// only thing the user can hear, so it must keep ringing.
|
|
64
|
+
private static func silenceIfActive(_ id: String) {
|
|
65
|
+
guard UIApplication.shared.applicationState == .active else { return }
|
|
66
|
+
NotificationBackend.shared.silence(id)
|
|
67
|
+
#if canImport(AlarmKit)
|
|
68
|
+
if #available(iOS 26.0, *) { AlarmKitBackend.shared.silence(id) }
|
|
69
|
+
#endif
|
|
70
|
+
}
|
|
71
|
+
|
|
54
72
|
/// Called when ringing ends for any reason (stop, snooze, cancel).
|
|
55
73
|
static func dismiss(_ id: String? = nil) {
|
|
56
74
|
DispatchQueue.main.async {
|
|
@@ -169,6 +169,12 @@ final class NotificationBackend: NSObject, UNUserNotificationCenterDelegate {
|
|
|
169
169
|
if ringing?.id == a.id { ringing = nil }
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
/// Stops the notification's sound (by clearing the delivered notification) while the
|
|
173
|
+
/// in-app ring screen takes over; `ringing` stays set so the session is still live.
|
|
174
|
+
func silence(_ id: String) {
|
|
175
|
+
center.removeDeliveredNotifications(withIdentifiers: ids(for: id))
|
|
176
|
+
}
|
|
177
|
+
|
|
172
178
|
func cancel(_ id: String) {
|
|
173
179
|
center.removePendingNotificationRequests(withIdentifiers: ids(for: id))
|
|
174
180
|
center.removeDeliveredNotifications(withIdentifiers: ids(for: id))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-alarms",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "A powerful, cross-platform alarm management library for React Native",
|
|
5
5
|
"private": false,
|
|
6
6
|
"main": "index.ts",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"lint": "eslint .",
|
|
23
23
|
"start": "react-native start",
|
|
24
24
|
"test": "jest",
|
|
25
|
-
"prepare": "
|
|
25
|
+
"prepare": "husky",
|
|
26
26
|
"build": "tsc",
|
|
27
27
|
"clean": "rm -rf dist types *.d.ts"
|
|
28
28
|
},
|
|
@@ -56,6 +56,8 @@
|
|
|
56
56
|
"@babel/core": "^7.25.2",
|
|
57
57
|
"@babel/preset-env": "^7.25.3",
|
|
58
58
|
"@babel/runtime": "^7.25.0",
|
|
59
|
+
"@commitlint/cli": "^21.2.4",
|
|
60
|
+
"@commitlint/config-conventional": "^21.2.4",
|
|
59
61
|
"@react-native-community/cli": "20.2.0",
|
|
60
62
|
"@react-native-community/cli-platform-android": "20.2.0",
|
|
61
63
|
"@react-native-community/cli-platform-ios": "20.2.0",
|
|
@@ -68,6 +70,7 @@
|
|
|
68
70
|
"@types/react": "^19.2.0",
|
|
69
71
|
"@types/react-test-renderer": "^19.1.0",
|
|
70
72
|
"eslint": "^8.19.0",
|
|
73
|
+
"husky": "^9.1.7",
|
|
71
74
|
"jest": "^29.6.3",
|
|
72
75
|
"prettier": "2.8.8",
|
|
73
76
|
"react": "19.3.0",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
File without changes
|