react-native-alarms 0.0.7 → 0.0.8
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/AlarmService.ts +85 -72
- package/README.md +43 -3
- package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmData.kt +6 -0
- package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmModule.kt +1 -0
- package/android/app/src/main/java/com/reactnativealarm/alarm/AlarmService.kt +55 -1
- package/android-lib/src/main/AndroidManifest.xml +1 -0
- package/index.ts +2 -0
- package/ios/.xcode.env.local +1 -1
- package/ios/ReactNativeAlarm/Info.plist +2 -2
- package/ios/ReactNativeAlarm.xcodeproj/project.pbxproj +8 -0
- package/ios/alarm/AlarmData.swift +3 -0
- package/ios/alarm/AlarmModule.swift +1 -0
- package/ios/alarm/NotificationBackend.swift +88 -39
- package/package.json +4 -4
- package/specs/NativeAlarm.ts +8 -4
- package/src/types/index.ts +20 -0
- package/.bundle/config +0 -2
- package/.husky/commit-msg +0 -1
- package/.husky/pre-commit +0 -0
- package/AlarmService.d.ts +0 -221
- package/commitlint.config.mjs +0 -1
- package/index.d.ts +0 -20
- 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/AlarmService.ts
CHANGED
|
@@ -25,36 +25,60 @@
|
|
|
25
25
|
* });
|
|
26
26
|
* ```
|
|
27
27
|
*/
|
|
28
|
-
import { NativeEventEmitter,
|
|
28
|
+
import { NativeEventEmitter, type EmitterSubscription } from 'react-native';
|
|
29
29
|
import type {
|
|
30
30
|
AlarmConfig,
|
|
31
31
|
AlarmProp,
|
|
32
32
|
AlarmPermission,
|
|
33
33
|
AlarmEvent,
|
|
34
|
+
AlarmEventMap,
|
|
35
|
+
AlarmEventName,
|
|
34
36
|
} from './src/types';
|
|
35
37
|
import { AlarmRegistry } from './src/utils';
|
|
36
|
-
|
|
37
|
-
const { Alarm: NativeAlarm } = NativeModules;
|
|
38
|
+
import Alarm, { getNativeAlarmModule } from './specs/NativeAlarm';
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
|
-
* Resolves the native
|
|
41
|
-
*
|
|
42
|
-
* The module is loaded conditionally so importing the service does not
|
|
43
|
-
* immediately fail when the native module is unavailable.
|
|
41
|
+
* Resolves the native module when available and keeps the lazy proxy as a
|
|
42
|
+
* fallback so missing native linking is reported when an operation is called.
|
|
44
43
|
*/
|
|
45
|
-
const
|
|
44
|
+
const nativeAlarm = getNativeAlarmModule();
|
|
45
|
+
const alarm = nativeAlarm ?? Alarm;
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
48
|
* Native event emitter used to subscribe to alarm lifecycle events.
|
|
49
49
|
*/
|
|
50
|
-
const alarmEmitter =
|
|
50
|
+
const alarmEmitter = nativeAlarm ? new NativeEventEmitter(nativeAlarm) : null;
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* Callback invoked when a native alarm event is received.
|
|
54
54
|
*
|
|
55
55
|
* @param alarm Event data emitted by the native alarm module.
|
|
56
56
|
*/
|
|
57
|
-
type AlarmEventListener =
|
|
57
|
+
type AlarmEventListener<K extends AlarmEventName = AlarmEventName> = (
|
|
58
|
+
alarm: AlarmEventMap[K],
|
|
59
|
+
) => void;
|
|
60
|
+
|
|
61
|
+
const ALARM_EVENT_NAMES = [
|
|
62
|
+
'onAlarmFired',
|
|
63
|
+
'onAlarmDismissed',
|
|
64
|
+
'onAlarmSnoozed',
|
|
65
|
+
] as const satisfies readonly AlarmEventName[];
|
|
66
|
+
|
|
67
|
+
function isAlarmEvent(value: Object): value is AlarmEvent {
|
|
68
|
+
return (
|
|
69
|
+
'alarmId' in value &&
|
|
70
|
+
typeof value.alarmId === 'string' &&
|
|
71
|
+
'timestamp' in value &&
|
|
72
|
+
typeof value.timestamp === 'number' &&
|
|
73
|
+
'title' in value &&
|
|
74
|
+
typeof value.title === 'string' &&
|
|
75
|
+
'body' in value &&
|
|
76
|
+
typeof value.body === 'string' &&
|
|
77
|
+
'data' in value &&
|
|
78
|
+
typeof value.data === 'string' &&
|
|
79
|
+
(!('image' in value) || typeof value.image === 'string')
|
|
80
|
+
);
|
|
81
|
+
}
|
|
58
82
|
|
|
59
83
|
/**
|
|
60
84
|
* High-level service for scheduling and managing native alarms.
|
|
@@ -83,19 +107,17 @@ export class AlarmService {
|
|
|
83
107
|
* more than once for the same event.
|
|
84
108
|
*/
|
|
85
109
|
private static listeners: {
|
|
86
|
-
|
|
87
|
-
dismissed: Set<AlarmEventListener>;
|
|
88
|
-
snoozed: Set<AlarmEventListener>;
|
|
110
|
+
[K in AlarmEventName]: Set<AlarmEventListener<K>>;
|
|
89
111
|
} = {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
112
|
+
onAlarmFired: new Set(),
|
|
113
|
+
onAlarmDismissed: new Set(),
|
|
114
|
+
onAlarmSnoozed: new Set(),
|
|
93
115
|
};
|
|
94
116
|
|
|
95
117
|
/**
|
|
96
118
|
* Native event subscriptions created by {@link initialize}.
|
|
97
119
|
*/
|
|
98
|
-
private static subscriptions:
|
|
120
|
+
private static subscriptions: EmitterSubscription[] = [];
|
|
99
121
|
|
|
100
122
|
/**
|
|
101
123
|
* Initializes native event forwarding.
|
|
@@ -107,25 +129,18 @@ export class AlarmService {
|
|
|
107
129
|
* This method is called automatically by the event subscription helpers.
|
|
108
130
|
*/
|
|
109
131
|
static initialize(): void {
|
|
110
|
-
if (!alarmEmitter ||
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
132
|
+
if (!alarmEmitter || this.subscriptions.length) return;
|
|
133
|
+
this.subscriptions = ALARM_EVENT_NAMES.map(eventName =>
|
|
134
|
+
alarmEmitter.addListener(eventName, (event: Object) => {
|
|
135
|
+
if (!isAlarmEvent(event)) {
|
|
136
|
+
console.error(
|
|
137
|
+
`[react-native-alarms] Ignoring invalid ${eventName} event payload`,
|
|
115
138
|
);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}),
|
|
122
|
-
alarmEmitter?.addListener('onAlarmSnoozed', (data: any) => {
|
|
123
|
-
this.listeners.snoozed.forEach(listener =>
|
|
124
|
-
listener(data as AlarmEvent),
|
|
125
|
-
);
|
|
126
|
-
}),
|
|
127
|
-
].filter(Boolean);
|
|
128
|
-
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
this.listeners[eventName].forEach(listener => listener(event));
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
129
144
|
}
|
|
130
145
|
|
|
131
146
|
/**
|
|
@@ -139,7 +154,7 @@ export class AlarmService {
|
|
|
139
154
|
* remove individual callbacks.
|
|
140
155
|
*/
|
|
141
156
|
static cleanup(): void {
|
|
142
|
-
this.subscriptions.forEach(sub => sub
|
|
157
|
+
this.subscriptions.forEach(sub => sub.remove());
|
|
143
158
|
this.subscriptions = [];
|
|
144
159
|
}
|
|
145
160
|
|
|
@@ -161,11 +176,10 @@ export class AlarmService {
|
|
|
161
176
|
* unsubscribe();
|
|
162
177
|
* ```
|
|
163
178
|
*/
|
|
164
|
-
static onAlarmFired(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
return () => this.listeners.fired.delete(listener);
|
|
179
|
+
static onAlarmFired(
|
|
180
|
+
listener: AlarmEventListener<'onAlarmFired'>,
|
|
181
|
+
): () => void {
|
|
182
|
+
return this.subscribe('onAlarmFired', listener);
|
|
169
183
|
}
|
|
170
184
|
|
|
171
185
|
/**
|
|
@@ -174,11 +188,10 @@ export class AlarmService {
|
|
|
174
188
|
* @param listener Callback invoked with the dismissed alarm's event data.
|
|
175
189
|
* @returns A function that unregisters this callback.
|
|
176
190
|
*/
|
|
177
|
-
static onAlarmDismissed(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
return () => this.listeners.dismissed.delete(listener);
|
|
191
|
+
static onAlarmDismissed(
|
|
192
|
+
listener: AlarmEventListener<'onAlarmDismissed'>,
|
|
193
|
+
): () => void {
|
|
194
|
+
return this.subscribe('onAlarmDismissed', listener);
|
|
182
195
|
}
|
|
183
196
|
|
|
184
197
|
/**
|
|
@@ -187,11 +200,21 @@ export class AlarmService {
|
|
|
187
200
|
* @param listener Callback invoked with the snoozed alarm's event data.
|
|
188
201
|
* @returns A function that unregisters this callback.
|
|
189
202
|
*/
|
|
190
|
-
static onAlarmSnoozed(
|
|
191
|
-
|
|
192
|
-
|
|
203
|
+
static onAlarmSnoozed(
|
|
204
|
+
listener: AlarmEventListener<'onAlarmSnoozed'>,
|
|
205
|
+
): () => void {
|
|
206
|
+
return this.subscribe('onAlarmSnoozed', listener);
|
|
207
|
+
}
|
|
193
208
|
|
|
194
|
-
|
|
209
|
+
private static subscribe<K extends AlarmEventName>(
|
|
210
|
+
eventName: K,
|
|
211
|
+
listener: AlarmEventListener<K>,
|
|
212
|
+
): () => void {
|
|
213
|
+
this.initialize();
|
|
214
|
+
this.listeners[eventName].add(listener);
|
|
215
|
+
return () => {
|
|
216
|
+
this.listeners[eventName].delete(listener);
|
|
217
|
+
};
|
|
195
218
|
}
|
|
196
219
|
|
|
197
220
|
/**
|
|
@@ -201,7 +224,6 @@ export class AlarmService {
|
|
|
201
224
|
* @throws Error if the native alarm module is not linked.
|
|
202
225
|
*/
|
|
203
226
|
static async checkPermission(): Promise<AlarmPermission> {
|
|
204
|
-
if (!alarm) throw new Error('Alarm module not linked');
|
|
205
227
|
return alarm.checkPermission();
|
|
206
228
|
}
|
|
207
229
|
|
|
@@ -214,7 +236,6 @@ export class AlarmService {
|
|
|
214
236
|
* @throws Error if the native alarm module is not linked.
|
|
215
237
|
*/
|
|
216
238
|
static async requestPermission(): Promise<AlarmPermission> {
|
|
217
|
-
if (!alarm) throw new Error('Alarm module not linked');
|
|
218
239
|
return alarm.requestPermission();
|
|
219
240
|
}
|
|
220
241
|
|
|
@@ -237,15 +258,7 @@ export class AlarmService {
|
|
|
237
258
|
* ```
|
|
238
259
|
*/
|
|
239
260
|
static async scheduleAlarm(config: AlarmConfig): Promise<string> {
|
|
240
|
-
|
|
241
|
-
throw new Error('Alarm module not linked');
|
|
242
|
-
}
|
|
243
|
-
const componentName = AlarmRegistry.resolve(config.presentation?.type);
|
|
244
|
-
return alarm.scheduleAlarm({
|
|
245
|
-
...config,
|
|
246
|
-
componentName,
|
|
247
|
-
componentProps: config.presentation?.props ?? {},
|
|
248
|
-
});
|
|
261
|
+
return alarm.scheduleAlarm(this.toNativeConfig(config));
|
|
249
262
|
}
|
|
250
263
|
|
|
251
264
|
/**
|
|
@@ -260,8 +273,17 @@ export class AlarmService {
|
|
|
260
273
|
alarmId: string,
|
|
261
274
|
config: AlarmConfig,
|
|
262
275
|
): Promise<boolean> {
|
|
263
|
-
|
|
264
|
-
|
|
276
|
+
return alarm.updateAlarm(alarmId, this.toNativeConfig(config));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private static toNativeConfig<T extends AlarmConfig>(
|
|
280
|
+
config: T,
|
|
281
|
+
): T & { componentName: string; componentProps: Record<string, unknown> } {
|
|
282
|
+
return {
|
|
283
|
+
...config,
|
|
284
|
+
componentName: AlarmRegistry.resolve(config.presentation?.type),
|
|
285
|
+
componentProps: config.presentation?.props ?? {},
|
|
286
|
+
};
|
|
265
287
|
}
|
|
266
288
|
|
|
267
289
|
/**
|
|
@@ -272,7 +294,6 @@ export class AlarmService {
|
|
|
272
294
|
* @throws Error if the native alarm module is not linked.
|
|
273
295
|
*/
|
|
274
296
|
static async cancelAlarm(alarmId: string): Promise<boolean> {
|
|
275
|
-
if (!alarm) throw new Error('Alarm module not linked');
|
|
276
297
|
return alarm.cancelAlarm(alarmId);
|
|
277
298
|
}
|
|
278
299
|
|
|
@@ -283,7 +304,6 @@ export class AlarmService {
|
|
|
283
304
|
* @throws Error if the native alarm module is not linked.
|
|
284
305
|
*/
|
|
285
306
|
static async cancelAllAlarms(): Promise<void> {
|
|
286
|
-
if (!alarm) throw new Error('Alarm module not linked');
|
|
287
307
|
return alarm.cancelAllAlarms();
|
|
288
308
|
}
|
|
289
309
|
|
|
@@ -295,7 +315,6 @@ export class AlarmService {
|
|
|
295
315
|
* @throws Error if the native alarm module is not linked.
|
|
296
316
|
*/
|
|
297
317
|
static async stopAlarm(alarmId: string): Promise<void> {
|
|
298
|
-
if (!alarm) throw new Error('Alarm module not linked');
|
|
299
318
|
return alarm.stopAlarm(alarmId);
|
|
300
319
|
}
|
|
301
320
|
|
|
@@ -313,7 +332,6 @@ export class AlarmService {
|
|
|
313
332
|
* ```
|
|
314
333
|
*/
|
|
315
334
|
static async snoozeAlarm(alarmId: string, minutes?: number): Promise<void> {
|
|
316
|
-
if (!alarm) throw new Error('Alarm module not linked');
|
|
317
335
|
return alarm.snoozeAlarm(alarmId, minutes ?? 5);
|
|
318
336
|
}
|
|
319
337
|
|
|
@@ -325,7 +343,6 @@ export class AlarmService {
|
|
|
325
343
|
* @throws Error if the native alarm module is not linked.
|
|
326
344
|
*/
|
|
327
345
|
static async getRingingAlarm(): Promise<AlarmProp | null> {
|
|
328
|
-
if (!alarm) throw new Error('Alarm module not linked');
|
|
329
346
|
return alarm.getRingingAlarm();
|
|
330
347
|
}
|
|
331
348
|
|
|
@@ -337,7 +354,6 @@ export class AlarmService {
|
|
|
337
354
|
* @throws Error if the native alarm module is not linked.
|
|
338
355
|
*/
|
|
339
356
|
static async getAlarm(alarmId: string): Promise<AlarmProp | null> {
|
|
340
|
-
if (!alarm) throw new Error('Alarm module not linked');
|
|
341
357
|
return alarm.getAlarm(alarmId);
|
|
342
358
|
}
|
|
343
359
|
|
|
@@ -348,7 +364,6 @@ export class AlarmService {
|
|
|
348
364
|
* @throws Error if the native alarm module is not linked.
|
|
349
365
|
*/
|
|
350
366
|
static async getAllAlarms(): Promise<AlarmProp[]> {
|
|
351
|
-
if (!alarm) throw new Error('Alarm module not linked');
|
|
352
367
|
return alarm.getAllAlarms();
|
|
353
368
|
}
|
|
354
369
|
|
|
@@ -360,7 +375,6 @@ export class AlarmService {
|
|
|
360
375
|
* @throws Error if the native alarm module is not linked.
|
|
361
376
|
*/
|
|
362
377
|
static async isScheduled(alarmId: string): Promise<boolean> {
|
|
363
|
-
if (!alarm) throw new Error('Alarm module not linked');
|
|
364
378
|
return alarm.isScheduled(alarmId);
|
|
365
379
|
}
|
|
366
380
|
|
|
@@ -377,7 +391,6 @@ export class AlarmService {
|
|
|
377
391
|
* @throws Error if the native alarm module is not linked.
|
|
378
392
|
*/
|
|
379
393
|
static async openAutoStartSettings(): Promise<boolean> {
|
|
380
|
-
if (!alarm) throw new Error('Alarm module not linked');
|
|
381
394
|
return alarm.openAutoStartSettings();
|
|
382
395
|
}
|
|
383
396
|
}
|
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ yarn add react-native-alarms
|
|
|
44
44
|
|
|
45
45
|
### Native Module Linking
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
For a bare React Native app, install the iOS pods and rebuild:
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
50
|
cd ios && pod install && cd ..
|
|
@@ -54,11 +54,32 @@ For Android, the native code auto-links, including the permissions, ring activit
|
|
|
54
54
|
service and receivers in its manifest. No `MainApplication` or `AndroidManifest.xml`
|
|
55
55
|
changes are needed. Rebuild the app after installing.
|
|
56
56
|
|
|
57
|
+
### Expo
|
|
58
|
+
|
|
59
|
+
The library works with Expo projects that generate and build native projects
|
|
60
|
+
(prebuild, EAS Build, or a development build). It uses React Native autolinking and
|
|
61
|
+
does not require an Expo config plugin:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm install react-native-alarms
|
|
65
|
+
npx expo prebuild
|
|
66
|
+
npx expo run:android
|
|
67
|
+
# or
|
|
68
|
+
npx expo run:ios
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Rebuild the native app after installing or upgrading the library. **Expo Go is not
|
|
72
|
+
supported** because it cannot load this library's native alarm module. Existing bare
|
|
73
|
+
React Native installation and setup remain unchanged.
|
|
74
|
+
Register the `AlarmRing` component in your app's JavaScript entry point as described
|
|
75
|
+
below; Expo Router apps should do this from a custom entry point before loading
|
|
76
|
+
`expo-router/entry`.
|
|
77
|
+
|
|
57
78
|
### Register the ring screen
|
|
58
79
|
|
|
59
80
|
When an alarm rings, both platforms show a full-screen React component registered as
|
|
60
81
|
`AlarmRing`, with the alarm (`alarmId`, `title`, `body`, `snoozeEnabled`,
|
|
61
|
-
`snoozeMinutes`, `data`) as props. Register your own component in `index.js`:
|
|
82
|
+
`snoozeMinutes`, `data`, and optional `image`) as props. Register your own component in `index.js`:
|
|
62
83
|
|
|
63
84
|
```js
|
|
64
85
|
import { AppRegistry } from 'react-native';
|
|
@@ -216,6 +237,7 @@ type AlarmConfig = {
|
|
|
216
237
|
timestamp: number; // Time in milliseconds (Date.now() format)
|
|
217
238
|
title: string; // Alarm title/label
|
|
218
239
|
body?: string; // Alarm description
|
|
240
|
+
image?: string; // file:// or https:// image URI for ring screen/notifications
|
|
219
241
|
sound?: string; // 'default' or custom sound file name
|
|
220
242
|
vibrate?: boolean; // Enable vibration (Android only)
|
|
221
243
|
snoozeEnabled?: boolean; // Allow snoozing
|
|
@@ -236,6 +258,24 @@ type AlarmPresentation = {
|
|
|
236
258
|
};
|
|
237
259
|
```
|
|
238
260
|
|
|
261
|
+
Pass a URI string for `image`; for example:
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
await AlarmService.scheduleAlarm({
|
|
265
|
+
timestamp: Date.now() + 60_000,
|
|
266
|
+
title: 'Appointment',
|
|
267
|
+
image: 'https://example.com/appointment.jpg',
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
When provided, `image` is returned with queried alarms and alarm events, passed to
|
|
272
|
+
the registered ring screen as an initial prop, and used for the native notification
|
|
273
|
+
image where supported. Android accepts `https://`, `file://`, and `content://` URIs.
|
|
274
|
+
iOS notification attachments accept `https://` and `file://` URIs; the iOS 26+
|
|
275
|
+
AlarmKit system alert itself does not display custom images, but the image URI is
|
|
276
|
+
available to the React Native ring screen when it opens. Render it in that screen
|
|
277
|
+
with React Native's `Image` component and `source={{ uri: image }}`.
|
|
278
|
+
|
|
239
279
|
> **How the native layer stores these:** `AlarmConfig.presentation` is resolved by `AlarmService` before being handed off to the native module. The resulting fields stored in the native `AlarmData` model are:
|
|
240
280
|
>
|
|
241
281
|
> | Field | Type | Default | Description |
|
|
@@ -264,6 +304,7 @@ type Alarm = {
|
|
|
264
304
|
timestamp: number;
|
|
265
305
|
title: string;
|
|
266
306
|
body: string;
|
|
307
|
+
image?: string;
|
|
267
308
|
sound: string;
|
|
268
309
|
vibrate: boolean;
|
|
269
310
|
snoozeEnabled: boolean;
|
|
@@ -602,4 +643,3 @@ await AlarmService.scheduleAlarm({
|
|
|
602
643
|
```
|
|
603
644
|
|
|
604
645
|
**Full Guide:** See [CUSTOM_UI_GUIDE.md](./CUSTOM_UI_GUIDE.md) for complete examples and best practices.
|
|
605
|
-
|
|
@@ -14,6 +14,7 @@ data class AlarmData(
|
|
|
14
14
|
val timestamp: Long,
|
|
15
15
|
val title: String,
|
|
16
16
|
val body: String,
|
|
17
|
+
val image: String,
|
|
17
18
|
val sound: String,
|
|
18
19
|
val vibrate: Boolean,
|
|
19
20
|
val snoozeEnabled: Boolean,
|
|
@@ -77,6 +78,7 @@ data class AlarmData(
|
|
|
77
78
|
|
|
78
79
|
fun toJson(): String = JSONObject().apply {
|
|
79
80
|
put("id", id); put("timestamp", timestamp); put("title", title); put("body", body)
|
|
81
|
+
put("image", image)
|
|
80
82
|
put("sound", sound); put("vibrate", vibrate); put("snoozeEnabled", snoozeEnabled)
|
|
81
83
|
put("snoozeMinutes", snoozeMinutes); put("repeatType", repeatType)
|
|
82
84
|
put("weekdays", JSONArray(weekdays)); put("intervalMs", intervalMs)
|
|
@@ -90,6 +92,7 @@ data class AlarmData(
|
|
|
90
92
|
putDouble("timestamp", timestamp.toDouble())
|
|
91
93
|
putString("title", title)
|
|
92
94
|
putString("body", body)
|
|
95
|
+
putString("image", image)
|
|
93
96
|
putString("sound", sound)
|
|
94
97
|
putBoolean("vibrate", vibrate)
|
|
95
98
|
putBoolean("snoozeEnabled", snoozeEnabled)
|
|
@@ -117,6 +120,7 @@ data class AlarmData(
|
|
|
117
120
|
putDouble("timestamp", timestamp.toDouble())
|
|
118
121
|
putString("title", title)
|
|
119
122
|
putString("body", body)
|
|
123
|
+
putString("image", image)
|
|
120
124
|
putBoolean("snoozeEnabled", snoozeEnabled)
|
|
121
125
|
putInt("snoozeMinutes", snoozeMinutes)
|
|
122
126
|
putString("data", data)
|
|
@@ -173,6 +177,7 @@ data class AlarmData(
|
|
|
173
177
|
timestamp = o.getLong("timestamp"),
|
|
174
178
|
title = o.optString("title"),
|
|
175
179
|
body = o.optString("body"),
|
|
180
|
+
image = o.optString("image"),
|
|
176
181
|
sound = o.optString("sound", "default"),
|
|
177
182
|
vibrate = o.optBoolean("vibrate", true),
|
|
178
183
|
snoozeEnabled = o.optBoolean("snoozeEnabled", true),
|
|
@@ -214,6 +219,7 @@ data class AlarmData(
|
|
|
214
219
|
timestamp = m.getDouble("timestamp").toLong(),
|
|
215
220
|
title = m.str("title"),
|
|
216
221
|
body = m.str("body"),
|
|
222
|
+
image = m.str("image"),
|
|
217
223
|
sound = m.str("sound", "default"),
|
|
218
224
|
vibrate = m.bool("vibrate", true),
|
|
219
225
|
snoozeEnabled = m.bool("snoozeEnabled", true),
|
|
@@ -241,6 +241,7 @@ class AlarmModule(private val ctx: ReactApplicationContext) : ReactContextBaseJa
|
|
|
241
241
|
putDouble("timestamp", System.currentTimeMillis().toDouble())
|
|
242
242
|
putString("title", a.title)
|
|
243
243
|
putString("body", a.body)
|
|
244
|
+
putString("image", a.image)
|
|
244
245
|
putString("data", a.data)
|
|
245
246
|
}
|
|
246
247
|
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java).emit(eventName, map)
|
|
@@ -8,6 +8,8 @@ import android.app.Service
|
|
|
8
8
|
import android.content.Context
|
|
9
9
|
import android.content.Intent
|
|
10
10
|
import android.content.pm.ServiceInfo
|
|
11
|
+
import android.graphics.Bitmap
|
|
12
|
+
import android.graphics.BitmapFactory
|
|
11
13
|
import android.media.AudioAttributes
|
|
12
14
|
import android.media.MediaPlayer
|
|
13
15
|
import android.media.RingtoneManager
|
|
@@ -22,6 +24,9 @@ import android.os.Vibrator
|
|
|
22
24
|
import android.util.Log
|
|
23
25
|
import androidx.core.app.NotificationCompat
|
|
24
26
|
import androidx.core.app.ServiceCompat
|
|
27
|
+
import java.io.InputStream
|
|
28
|
+
import java.net.URL
|
|
29
|
+
import java.util.concurrent.Executors
|
|
25
30
|
|
|
26
31
|
/**
|
|
27
32
|
* Foreground service that actually rings: looping sound, vibration, a partial wake
|
|
@@ -39,6 +44,7 @@ class AlarmService : Service() {
|
|
|
39
44
|
private var player: MediaPlayer? = null
|
|
40
45
|
private var vibrator: Vibrator? = null
|
|
41
46
|
private var wakeLock: PowerManager.WakeLock? = null
|
|
47
|
+
private val imageExecutor = Executors.newSingleThreadExecutor()
|
|
42
48
|
|
|
43
49
|
override fun onBind(intent: Intent?): IBinder? = null
|
|
44
50
|
|
|
@@ -70,6 +76,7 @@ class AlarmService : Service() {
|
|
|
70
76
|
Log.i(TAG, "Ringing alarm ${alarm.id}")
|
|
71
77
|
|
|
72
78
|
startInForeground(buildNotification(alarm))
|
|
79
|
+
loadNotificationImage(alarm)
|
|
73
80
|
acquireWakeLock()
|
|
74
81
|
startSound(alarm)
|
|
75
82
|
if (alarm.vibrate) startVibration()
|
|
@@ -139,6 +146,7 @@ class AlarmService : Service() {
|
|
|
139
146
|
|
|
140
147
|
override fun onDestroy() {
|
|
141
148
|
stopRinging()
|
|
149
|
+
imageExecutor.shutdownNow()
|
|
142
150
|
super.onDestroy()
|
|
143
151
|
}
|
|
144
152
|
|
|
@@ -217,7 +225,7 @@ class AlarmService : Service() {
|
|
|
217
225
|
.build()
|
|
218
226
|
}
|
|
219
227
|
|
|
220
|
-
private fun buildNotification(alarm: AlarmData): Notification {
|
|
228
|
+
private fun buildNotification(alarm: AlarmData, image: Bitmap? = null): Notification {
|
|
221
229
|
ensureChannel()
|
|
222
230
|
val ringIntent = Intent(this, AlarmRingActivity::class.java)
|
|
223
231
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
@@ -235,12 +243,58 @@ class AlarmService : Service() {
|
|
|
235
243
|
.setFullScreenIntent(fullScreen, true)
|
|
236
244
|
.setContentIntent(fullScreen)
|
|
237
245
|
.addAction(0, "Stop", commandPI(ACTION_DISMISS, 2))
|
|
246
|
+
if (image != null) {
|
|
247
|
+
b.setStyle(
|
|
248
|
+
NotificationCompat.BigPictureStyle()
|
|
249
|
+
.bigPicture(image)
|
|
250
|
+
.setBigContentTitle(alarm.title.ifBlank { "Alarm" })
|
|
251
|
+
.setSummaryText(alarm.body.ifBlank { "Alarm is ringing" })
|
|
252
|
+
)
|
|
253
|
+
}
|
|
238
254
|
if (alarm.snoozeEnabled) {
|
|
239
255
|
b.addAction(0, "Snooze", commandPI(ACTION_SNOOZE, 3))
|
|
240
256
|
}
|
|
241
257
|
return b.build()
|
|
242
258
|
}
|
|
243
259
|
|
|
260
|
+
private fun loadNotificationImage(alarm: AlarmData) {
|
|
261
|
+
if (alarm.image.isBlank()) return
|
|
262
|
+
imageExecutor.execute {
|
|
263
|
+
try {
|
|
264
|
+
val bitmap = decodeImage(Uri.parse(alarm.image)) ?: return@execute
|
|
265
|
+
handler.post {
|
|
266
|
+
if (current?.id == alarm.id) {
|
|
267
|
+
ensureChannel().notify(NOTIFICATION_ID, buildNotification(alarm, bitmap))
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
} catch (e: Exception) {
|
|
271
|
+
Log.w(TAG, "Could not load notification image for alarm ${alarm.id}", e)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private fun decodeImage(uri: Uri): Bitmap? {
|
|
277
|
+
fun openStream(): InputStream? = when (uri.scheme?.lowercase()) {
|
|
278
|
+
"https" -> URL(uri.toString()).openConnection().apply {
|
|
279
|
+
connectTimeout = 5_000
|
|
280
|
+
readTimeout = 5_000
|
|
281
|
+
}.getInputStream()
|
|
282
|
+
"file", "content", "android.resource" -> contentResolver.openInputStream(uri)
|
|
283
|
+
else -> throw IllegalArgumentException("Unsupported alarm image URI: $uri")
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
|
287
|
+
openStream()?.use { BitmapFactory.decodeStream(it, null, bounds) }
|
|
288
|
+
if (bounds.outWidth <= 0 || bounds.outHeight <= 0) return null
|
|
289
|
+
|
|
290
|
+
var sampleSize = 1
|
|
291
|
+
while (bounds.outWidth / sampleSize > 1024 || bounds.outHeight / sampleSize > 1024) {
|
|
292
|
+
sampleSize *= 2
|
|
293
|
+
}
|
|
294
|
+
val options = BitmapFactory.Options().apply { inSampleSize = sampleSize }
|
|
295
|
+
return openStream()?.use { BitmapFactory.decodeStream(it, null, options) }
|
|
296
|
+
}
|
|
297
|
+
|
|
244
298
|
private fun commandPI(action: String, code: Int): PendingIntent =
|
|
245
299
|
PendingIntent.getService(
|
|
246
300
|
this, code, Intent(this, AlarmService::class.java).setAction(action),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
2
2
|
|
|
3
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
3
4
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
4
5
|
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
|
|
5
6
|
<uses-permission
|
package/index.ts
CHANGED
package/ios/.xcode.env.local
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export NODE_BINARY=/opt/homebrew/
|
|
1
|
+
export NODE_BINARY=/opt/homebrew/bin/node
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
|
27
27
|
<key>LSRequiresIPhoneOS</key>
|
|
28
28
|
<true/>
|
|
29
|
+
<key>NSAlarmKitUsageDescription</key>
|
|
30
|
+
<string>This app needs access to alarms to wake you up on time.</string>
|
|
29
31
|
<key>NSAppTransportSecurity</key>
|
|
30
32
|
<dict>
|
|
31
33
|
<key>NSAllowsArbitraryLoads</key>
|
|
@@ -73,7 +75,5 @@
|
|
|
73
75
|
</array>
|
|
74
76
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
|
75
77
|
<false/>
|
|
76
|
-
<key>NSAlarmKitUsageDescription</key>
|
|
77
|
-
<string>This app needs access to alarms to wake you up on time.</string>
|
|
78
78
|
</dict>
|
|
79
79
|
</plist>
|
|
@@ -223,10 +223,14 @@
|
|
|
223
223
|
inputFileListPaths = (
|
|
224
224
|
"${PODS_ROOT}/Target Support Files/Pods-ReactNativeAlarm/Pods-ReactNativeAlarm-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
|
225
225
|
);
|
|
226
|
+
inputPaths = (
|
|
227
|
+
);
|
|
226
228
|
name = "[CP] Embed Pods Frameworks";
|
|
227
229
|
outputFileListPaths = (
|
|
228
230
|
"${PODS_ROOT}/Target Support Files/Pods-ReactNativeAlarm/Pods-ReactNativeAlarm-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
|
229
231
|
);
|
|
232
|
+
outputPaths = (
|
|
233
|
+
);
|
|
230
234
|
runOnlyForDeploymentPostprocessing = 0;
|
|
231
235
|
shellPath = /bin/sh;
|
|
232
236
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeAlarm/Pods-ReactNativeAlarm-frameworks.sh\"\n";
|
|
@@ -262,10 +266,14 @@
|
|
|
262
266
|
inputFileListPaths = (
|
|
263
267
|
"${PODS_ROOT}/Target Support Files/Pods-ReactNativeAlarm/Pods-ReactNativeAlarm-resources-${CONFIGURATION}-input-files.xcfilelist",
|
|
264
268
|
);
|
|
269
|
+
inputPaths = (
|
|
270
|
+
);
|
|
265
271
|
name = "[CP] Copy Pods Resources";
|
|
266
272
|
outputFileListPaths = (
|
|
267
273
|
"${PODS_ROOT}/Target Support Files/Pods-ReactNativeAlarm/Pods-ReactNativeAlarm-resources-${CONFIGURATION}-output-files.xcfilelist",
|
|
268
274
|
);
|
|
275
|
+
outputPaths = (
|
|
276
|
+
);
|
|
269
277
|
runOnlyForDeploymentPostprocessing = 0;
|
|
270
278
|
shellPath = /bin/sh;
|
|
271
279
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeAlarm/Pods-ReactNativeAlarm-resources.sh\"\n";
|
|
@@ -7,6 +7,7 @@ struct AlarmData: Codable {
|
|
|
7
7
|
var timestamp: Double // ms since epoch
|
|
8
8
|
var title: String
|
|
9
9
|
var body: String
|
|
10
|
+
var image: String? = nil
|
|
10
11
|
var sound: String
|
|
11
12
|
var vibrate: Bool
|
|
12
13
|
var snoozeEnabled: Bool
|
|
@@ -78,6 +79,7 @@ struct AlarmData: Codable {
|
|
|
78
79
|
"timestamp": timestamp,
|
|
79
80
|
"title": title,
|
|
80
81
|
"body": body,
|
|
82
|
+
"image": image ?? "",
|
|
81
83
|
"sound": sound,
|
|
82
84
|
"vibrate": vibrate,
|
|
83
85
|
"snoozeEnabled": snoozeEnabled,
|
|
@@ -97,6 +99,7 @@ struct AlarmData: Codable {
|
|
|
97
99
|
timestamp: (m["timestamp"] as? NSNumber)?.doubleValue ?? 0,
|
|
98
100
|
title: m["title"] as? String ?? "",
|
|
99
101
|
body: m["body"] as? String ?? "",
|
|
102
|
+
image: m["image"] as? String,
|
|
100
103
|
sound: m["sound"] as? String ?? "default",
|
|
101
104
|
vibrate: (m["vibrate"] as? NSNumber)?.boolValue ?? true,
|
|
102
105
|
snoozeEnabled: (m["snoozeEnabled"] as? NSNumber)?.boolValue ?? true,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import UserNotifications
|
|
3
|
+
import UniformTypeIdentifiers
|
|
3
4
|
|
|
4
5
|
/// Fallback backend for devices without AlarmKit (iOS < 26, or AlarmKit denied):
|
|
5
6
|
/// local notifications with Stop / Snooze actions.
|
|
@@ -78,12 +79,13 @@ final class NotificationBackend: NSObject, UNUserNotificationCenterDelegate {
|
|
|
78
79
|
+ (0..<Self.intervalOccurrences).map { base + ":i\($0)" }
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
private func content(for a: AlarmData) -> UNMutableNotificationContent {
|
|
82
|
+
private func content(for a: AlarmData, attachment: UNNotificationAttachment? = nil) -> UNMutableNotificationContent {
|
|
82
83
|
let c = UNMutableNotificationContent()
|
|
83
84
|
c.title = a.title.isEmpty ? "Alarm" : a.title
|
|
84
85
|
c.body = a.body.isEmpty ? "Alarm is ringing" : a.body
|
|
85
86
|
c.categoryIdentifier = Self.category
|
|
86
|
-
c.userInfo = ["alarmId": a.id]
|
|
87
|
+
c.userInfo = ["alarmId": a.id, "image": a.image ?? ""]
|
|
88
|
+
if let attachment { c.attachments = [attachment] }
|
|
87
89
|
c.sound = (a.sound.isEmpty || a.sound == "default")
|
|
88
90
|
? .default
|
|
89
91
|
: UNNotificationSound(named: UNNotificationSoundName(rawValue: a.sound))
|
|
@@ -93,51 +95,96 @@ final class NotificationBackend: NSObject, UNUserNotificationCenterDelegate {
|
|
|
93
95
|
return c
|
|
94
96
|
}
|
|
95
97
|
|
|
96
|
-
private func request(_ id: String, _ a: AlarmData, _ trigger: UNNotificationTrigger
|
|
97
|
-
|
|
98
|
+
private func request(_ id: String, _ a: AlarmData, _ trigger: UNNotificationTrigger,
|
|
99
|
+
_ attachment: UNNotificationAttachment? = nil) -> UNNotificationRequest {
|
|
100
|
+
UNNotificationRequest(identifier: id, content: content(for: a, attachment: attachment), trigger: trigger)
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
func schedule(_ a: AlarmData, completion: @escaping (Error?) -> Void) {
|
|
101
104
|
cancel(a.id)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
dc.hour = hm.hour
|
|
111
|
-
dc.minute = hm.minute
|
|
112
|
-
requests.append(request(base, a, UNCalendarNotificationTrigger(dateMatching: dc, repeats: true)))
|
|
113
|
-
|
|
114
|
-
case "weekly":
|
|
115
|
-
let days = a.weekdays.isEmpty ? Array(1...7) : a.weekdays.filter { (1...7).contains($0) }
|
|
116
|
-
for d in days {
|
|
105
|
+
prepareAttachment(for: a.image) { attachment in
|
|
106
|
+
let cal = Calendar.current
|
|
107
|
+
let base = Self.prefix + a.id
|
|
108
|
+
let hm = cal.dateComponents([.hour, .minute], from: a.date)
|
|
109
|
+
var requests: [UNNotificationRequest] = []
|
|
110
|
+
|
|
111
|
+
switch a.repeatType {
|
|
112
|
+
case "daily":
|
|
117
113
|
var dc = DateComponents()
|
|
118
|
-
dc.weekday = d
|
|
119
114
|
dc.hour = hm.hour
|
|
120
115
|
dc.minute = hm.minute
|
|
121
|
-
requests.append(request(base
|
|
116
|
+
requests.append(self.request(base, a, UNCalendarNotificationTrigger(dateMatching: dc, repeats: true), attachment))
|
|
117
|
+
|
|
118
|
+
case "weekly":
|
|
119
|
+
let days = a.weekdays.isEmpty ? Array(1...7) : a.weekdays.filter { (1...7).contains($0) }
|
|
120
|
+
for d in days {
|
|
121
|
+
var dc = DateComponents()
|
|
122
|
+
dc.weekday = d
|
|
123
|
+
dc.hour = hm.hour
|
|
124
|
+
dc.minute = hm.minute
|
|
125
|
+
requests.append(self.request(base + ":wd\(d)", a, UNCalendarNotificationTrigger(dateMatching: dc, repeats: true), attachment))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case "interval":
|
|
129
|
+
// iOS repeating interval triggers count from "now", not from `timestamp`, so
|
|
130
|
+
// pre-schedule the next N exact occurrences instead.
|
|
131
|
+
var t = a.nextTrigger()
|
|
132
|
+
guard t > 0 else { return completion(nil) }
|
|
133
|
+
for k in 0..<Self.intervalOccurrences {
|
|
134
|
+
requests.append(self.request(base + ":i\(k)", a, self.exactTrigger(ms: t), attachment))
|
|
135
|
+
t += a.intervalMs
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
default:
|
|
139
|
+
let next = a.nextTrigger()
|
|
140
|
+
guard next > 0 else { return completion(nil) } // nothing in the future, same as Android
|
|
141
|
+
requests.append(self.request(base, a, self.exactTrigger(ms: next), attachment))
|
|
122
142
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
143
|
+
|
|
144
|
+
self.add(requests, completion: completion)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private func prepareAttachment(for image: String?, completion: @escaping (UNNotificationAttachment?) -> Void) {
|
|
149
|
+
guard let image, !image.isEmpty else { return completion(nil) }
|
|
150
|
+
guard let url = URL(string: image) else {
|
|
151
|
+
NSLog("[react-native-alarms] Invalid image URI: %@", image)
|
|
152
|
+
return completion(nil)
|
|
153
|
+
}
|
|
154
|
+
if url.isFileURL {
|
|
155
|
+
do {
|
|
156
|
+
completion(try UNNotificationAttachment(identifier: "alarm-image", url: url, options: nil))
|
|
157
|
+
} catch {
|
|
158
|
+
NSLog("[react-native-alarms] Could not attach alarm image: %@", error.localizedDescription)
|
|
159
|
+
completion(nil)
|
|
132
160
|
}
|
|
133
|
-
|
|
134
|
-
default:
|
|
135
|
-
let next = a.nextTrigger()
|
|
136
|
-
guard next > 0 else { return completion(nil) } // nothing in the future, same as Android
|
|
137
|
-
requests.append(request(base, a, exactTrigger(ms: next)))
|
|
161
|
+
return
|
|
138
162
|
}
|
|
139
|
-
|
|
140
|
-
|
|
163
|
+
guard url.scheme?.lowercased() == "https" else {
|
|
164
|
+
NSLog("[react-native-alarms] Alarm image URI must use file:// or https://: %@", image)
|
|
165
|
+
return completion(nil)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
URLSession.shared.downloadTask(with: url) { temporaryURL, response, error in
|
|
169
|
+
guard let temporaryURL, error == nil else {
|
|
170
|
+
NSLog("[react-native-alarms] Could not download alarm image: %@", error?.localizedDescription ?? "empty response")
|
|
171
|
+
return completion(nil)
|
|
172
|
+
}
|
|
173
|
+
let mimeType = response?.mimeType
|
|
174
|
+
let ext = url.pathExtension.isEmpty
|
|
175
|
+
? (mimeType.flatMap { UTType(mimeType: $0)?.preferredFilenameExtension } ?? "jpg")
|
|
176
|
+
: url.pathExtension
|
|
177
|
+
let destination = FileManager.default.temporaryDirectory
|
|
178
|
+
.appendingPathComponent(UUID().uuidString)
|
|
179
|
+
.appendingPathExtension(ext)
|
|
180
|
+
do {
|
|
181
|
+
try FileManager.default.moveItem(at: temporaryURL, to: destination)
|
|
182
|
+
completion(try UNNotificationAttachment(identifier: "alarm-image", url: destination, options: nil))
|
|
183
|
+
} catch {
|
|
184
|
+
NSLog("[react-native-alarms] Could not attach downloaded alarm image: %@", error.localizedDescription)
|
|
185
|
+
completion(nil)
|
|
186
|
+
}
|
|
187
|
+
}.resume()
|
|
141
188
|
}
|
|
142
189
|
|
|
143
190
|
private func exactTrigger(ms: Double) -> UNCalendarNotificationTrigger {
|
|
@@ -165,8 +212,10 @@ final class NotificationBackend: NSObject, UNUserNotificationCenterDelegate {
|
|
|
165
212
|
func snooze(_ a: AlarmData, minutes: Int) {
|
|
166
213
|
center.removeDeliveredNotifications(withIdentifiers: ids(for: a.id))
|
|
167
214
|
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: Double(max(1, minutes)) * 60, repeats: false)
|
|
168
|
-
|
|
169
|
-
|
|
215
|
+
prepareAttachment(for: a.image) { attachment in
|
|
216
|
+
self.center.add(self.request(Self.prefix + a.id + ":snooze", a, trigger, attachment))
|
|
217
|
+
if self.ringing?.id == a.id { self.ringing = nil }
|
|
218
|
+
}
|
|
170
219
|
}
|
|
171
220
|
|
|
172
221
|
/// Stops the notification's sound (by clearing the delivered notification) while the
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-alarms",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "A powerful, cross-platform alarm management library for React Native",
|
|
5
5
|
"private": false,
|
|
6
6
|
"main": "index.ts",
|
|
7
|
-
"types": "index.
|
|
7
|
+
"types": "index.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"types": "./index.
|
|
10
|
+
"types": "./index.ts",
|
|
11
11
|
"default": "./index.ts"
|
|
12
12
|
},
|
|
13
13
|
"./service": {
|
|
14
|
-
"types": "./AlarmService.
|
|
14
|
+
"types": "./AlarmService.ts",
|
|
15
15
|
"default": "./AlarmService.ts"
|
|
16
16
|
},
|
|
17
17
|
"./package.json": "./package.json"
|
package/specs/NativeAlarm.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TurboModule } from 'react-native';
|
|
2
2
|
import { NativeModules, Platform, TurboModuleRegistry } from 'react-native';
|
|
3
|
-
import { AlarmPermission, AlarmConfig, AlarmProp } from '../src/types';
|
|
3
|
+
import type { AlarmPermission, AlarmConfig, AlarmProp } from '../src/types';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Native Alarm module interface.
|
|
@@ -129,9 +129,14 @@ const LINKING_ERROR =
|
|
|
129
129
|
"react-native-alarms: the native 'Alarm' module is not linked. " +
|
|
130
130
|
Platform.select({
|
|
131
131
|
ios: "Run 'pod install' in the ios folder, then rebuild the app.",
|
|
132
|
-
default:
|
|
132
|
+
default:
|
|
133
|
+
'Rebuild the app after installing the package. Expo Go is unsupported; use a development build.',
|
|
133
134
|
});
|
|
134
135
|
|
|
136
|
+
export function getNativeAlarmModule(): Spec | null {
|
|
137
|
+
return TurboModuleRegistry.get<Spec>('Alarm') ?? NativeModules.Alarm ?? null;
|
|
138
|
+
}
|
|
139
|
+
|
|
135
140
|
/**
|
|
136
141
|
* Cross-platform native Alarm module.
|
|
137
142
|
*
|
|
@@ -151,8 +156,7 @@ const LINKING_ERROR =
|
|
|
151
156
|
* ```
|
|
152
157
|
*/
|
|
153
158
|
const Alarm: Spec =
|
|
154
|
-
|
|
155
|
-
NativeModules.Alarm ??
|
|
159
|
+
getNativeAlarmModule() ??
|
|
156
160
|
new Proxy({} as Spec, {
|
|
157
161
|
get() {
|
|
158
162
|
throw new Error(LINKING_ERROR);
|
package/src/types/index.ts
CHANGED
|
@@ -91,6 +91,12 @@ export type AlarmConfig = {
|
|
|
91
91
|
*/
|
|
92
92
|
body?: string;
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Optional image URI (for example, a `file://` path or an `https://` URL).
|
|
96
|
+
* Passed to the ringing screen and shown in native notifications when supported.
|
|
97
|
+
*/
|
|
98
|
+
image?: string;
|
|
99
|
+
|
|
94
100
|
/**
|
|
95
101
|
* Alarm sound.
|
|
96
102
|
*
|
|
@@ -152,6 +158,9 @@ export type AlarmProp = {
|
|
|
152
158
|
/** Alarm body. */
|
|
153
159
|
body: string;
|
|
154
160
|
|
|
161
|
+
/** Optional image URI. */
|
|
162
|
+
image?: string;
|
|
163
|
+
|
|
155
164
|
/** Sound being used. */
|
|
156
165
|
sound: string;
|
|
157
166
|
|
|
@@ -241,6 +250,17 @@ export type AlarmEvent = {
|
|
|
241
250
|
/** Alarm body. */
|
|
242
251
|
body: string;
|
|
243
252
|
|
|
253
|
+
/** Optional image URI. */
|
|
254
|
+
image?: string;
|
|
255
|
+
|
|
244
256
|
/** Custom JSON payload. */
|
|
245
257
|
data: string;
|
|
246
258
|
};
|
|
259
|
+
|
|
260
|
+
export type AlarmEventMap = {
|
|
261
|
+
onAlarmFired: AlarmEvent;
|
|
262
|
+
onAlarmDismissed: AlarmEvent;
|
|
263
|
+
onAlarmSnoozed: AlarmEvent;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
export type AlarmEventName = keyof AlarmEventMap;
|
package/.bundle/config
DELETED
package/.husky/commit-msg
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
npx --no -- commitlint --edit $1
|
package/.husky/pre-commit
DELETED
|
File without changes
|
package/AlarmService.d.ts
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* High-level alarm management API.
|
|
3
|
-
*
|
|
4
|
-
* Provides convenient wrapper methods for managing native alarms,
|
|
5
|
-
* including scheduling, updating, cancelling, snoozing, and monitoring
|
|
6
|
-
* alarm lifecycle events.
|
|
7
|
-
*
|
|
8
|
-
* This service can be used for alarms, scheduled reminders, and other
|
|
9
|
-
* time-sensitive experiences supported by the native alarm module.
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* ```typescript
|
|
13
|
-
* import { AlarmService } from 'react-native-alarms';
|
|
14
|
-
*
|
|
15
|
-
* const permission = await AlarmService.checkPermission();
|
|
16
|
-
*
|
|
17
|
-
* if (permission.status !== 'granted') {
|
|
18
|
-
* await AlarmService.requestPermission();
|
|
19
|
-
* }
|
|
20
|
-
*
|
|
21
|
-
* const alarmId = await AlarmService.scheduleAlarm({
|
|
22
|
-
* timestamp: Date.now() + 60_000,
|
|
23
|
-
* title: 'Reminder',
|
|
24
|
-
* body: 'Your scheduled reminder is ready.',
|
|
25
|
-
* });
|
|
26
|
-
* ```
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
import type {
|
|
30
|
-
AlarmConfig,
|
|
31
|
-
AlarmProp,
|
|
32
|
-
AlarmPermission,
|
|
33
|
-
AlarmEvent,
|
|
34
|
-
} from './src/types';
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Callback invoked when an alarm lifecycle event occurs.
|
|
38
|
-
*
|
|
39
|
-
* @param alarm - Information about the alarm associated with the event.
|
|
40
|
-
*/
|
|
41
|
-
type AlarmEventListener = (alarm: AlarmEvent) => void;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Static service for managing native alarms.
|
|
45
|
-
*
|
|
46
|
-
* All methods can be called directly without creating an instance.
|
|
47
|
-
*/
|
|
48
|
-
export class AlarmService {
|
|
49
|
-
/**
|
|
50
|
-
* Initializes the alarm service and registers native event listeners.
|
|
51
|
-
*
|
|
52
|
-
* Call this method to begin forwarding native alarm events to
|
|
53
|
-
* registered JavaScript listeners. Event registration methods may
|
|
54
|
-
* also initialize the service automatically.
|
|
55
|
-
*/
|
|
56
|
-
static initialize(): void;
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Removes native event subscriptions created by the service.
|
|
60
|
-
*
|
|
61
|
-
* Registered JavaScript callbacks may remain registered, depending
|
|
62
|
-
* on the implementation. Reinitialize the service before expecting
|
|
63
|
-
* to receive native events again.
|
|
64
|
-
*/
|
|
65
|
-
static cleanup(): void;
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Registers a listener for when an alarm fires.
|
|
69
|
-
*
|
|
70
|
-
* @param listener - Callback invoked with the fired alarm's event data.
|
|
71
|
-
* @returns A function that unregisters this listener.
|
|
72
|
-
*
|
|
73
|
-
* @example
|
|
74
|
-
* ```typescript
|
|
75
|
-
* const unsubscribe = AlarmService.onAlarmFired((alarm) => {
|
|
76
|
-
* console.log('Alarm fired:', alarm);
|
|
77
|
-
* });
|
|
78
|
-
*
|
|
79
|
-
* // Unsubscribe when no longer needed.
|
|
80
|
-
* unsubscribe();
|
|
81
|
-
* ```
|
|
82
|
-
*/
|
|
83
|
-
static onAlarmFired(listener: AlarmEventListener): () => void;
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Registers a listener for when an alarm is dismissed.
|
|
87
|
-
*
|
|
88
|
-
* @param listener - Callback invoked with the dismissed alarm's event data.
|
|
89
|
-
* @returns A function that unregisters this listener.
|
|
90
|
-
*/
|
|
91
|
-
static onAlarmDismissed(listener: AlarmEventListener): () => void;
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Registers a listener for when an alarm is snoozed.
|
|
95
|
-
*
|
|
96
|
-
* @param listener - Callback invoked with the snoozed alarm's event data.
|
|
97
|
-
* @returns A function that unregisters this listener.
|
|
98
|
-
*/
|
|
99
|
-
static onAlarmSnoozed(listener: AlarmEventListener): () => void;
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Checks the current alarm permission status.
|
|
103
|
-
*
|
|
104
|
-
* @returns A promise resolving to the current permission information.
|
|
105
|
-
* @throws If the native alarm module is unavailable.
|
|
106
|
-
*/
|
|
107
|
-
static checkPermission(): Promise<AlarmPermission>;
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Requests the permissions required by the native alarm functionality.
|
|
111
|
-
*
|
|
112
|
-
* @returns A promise resolving to the resulting permission information.
|
|
113
|
-
* @throws If the native alarm module is unavailable.
|
|
114
|
-
*/
|
|
115
|
-
static requestPermission(): Promise<AlarmPermission>;
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Schedules a new alarm.
|
|
119
|
-
*
|
|
120
|
-
* @param config - Configuration describing the alarm to schedule.
|
|
121
|
-
* @returns A promise resolving to the ID assigned to the scheduled alarm.
|
|
122
|
-
* @throws If the native alarm module is unavailable or scheduling fails.
|
|
123
|
-
*/
|
|
124
|
-
static scheduleAlarm(config: AlarmConfig): Promise<string>;
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Updates an existing scheduled alarm.
|
|
128
|
-
*
|
|
129
|
-
* @param alarmId - ID of the alarm to update.
|
|
130
|
-
* @param config - Updated configuration for the alarm.
|
|
131
|
-
* @returns A promise resolving to `true` if the update succeeds.
|
|
132
|
-
* @throws If the native alarm module is unavailable or the update fails.
|
|
133
|
-
*/
|
|
134
|
-
static updateAlarm(alarmId: string, config: AlarmConfig): Promise<boolean>;
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Cancels a scheduled alarm.
|
|
138
|
-
*
|
|
139
|
-
* @param alarmId - ID of the alarm to cancel.
|
|
140
|
-
* @returns A promise resolving to `true` if the alarm was cancelled.
|
|
141
|
-
* @throws If the native alarm module is unavailable.
|
|
142
|
-
*/
|
|
143
|
-
static cancelAlarm(alarmId: string): Promise<boolean>;
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Cancels all scheduled alarms.
|
|
147
|
-
*
|
|
148
|
-
* @returns A promise that resolves when the cancellation operation completes.
|
|
149
|
-
* @throws If the native alarm module is unavailable.
|
|
150
|
-
*/
|
|
151
|
-
static cancelAllAlarms(): Promise<void>;
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Stops a currently ringing alarm.
|
|
155
|
-
*
|
|
156
|
-
* @param alarmId - ID of the ringing alarm to stop.
|
|
157
|
-
* @returns A promise that resolves when the stop operation completes.
|
|
158
|
-
* @throws If the native alarm module is unavailable.
|
|
159
|
-
*/
|
|
160
|
-
static stopAlarm(alarmId: string): Promise<void>;
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Snoozes a currently ringing alarm.
|
|
164
|
-
*
|
|
165
|
-
* @param alarmId - ID of the alarm to snooze.
|
|
166
|
-
* @param minutes - Optional snooze duration in minutes. If omitted,
|
|
167
|
-
* the native implementation or alarm configuration determines the default.
|
|
168
|
-
* @returns A promise that resolves when the snooze operation completes.
|
|
169
|
-
* @throws If the native alarm module is unavailable or snoozing fails.
|
|
170
|
-
*/
|
|
171
|
-
static snoozeAlarm(alarmId: string, minutes?: number): Promise<void>;
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Retrieves the currently ringing alarm, if one exists.
|
|
175
|
-
*
|
|
176
|
-
* @returns A promise resolving to the ringing alarm, or `null` if none exists.
|
|
177
|
-
* @throws If the native alarm module is unavailable.
|
|
178
|
-
*/
|
|
179
|
-
static getRingingAlarm(): Promise<AlarmProp | null>;
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Retrieves a specific alarm by its ID.
|
|
183
|
-
*
|
|
184
|
-
* @param alarmId - ID of the alarm to retrieve.
|
|
185
|
-
* @returns A promise resolving to the alarm, or `null` if it is not found.
|
|
186
|
-
* @throws If the native alarm module is unavailable.
|
|
187
|
-
*/
|
|
188
|
-
static getAlarm(alarmId: string): Promise<AlarmProp | null>;
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Retrieves all alarms known to the native alarm module.
|
|
192
|
-
*
|
|
193
|
-
* @returns A promise resolving to an array of alarm records.
|
|
194
|
-
* @throws If the native alarm module is unavailable.
|
|
195
|
-
*/
|
|
196
|
-
static getAllAlarms(): Promise<AlarmProp[]>;
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Checks whether an alarm is currently scheduled.
|
|
200
|
-
*
|
|
201
|
-
* @param alarmId - ID of the alarm to check.
|
|
202
|
-
* @returns A promise resolving to `true` if the alarm is scheduled;
|
|
203
|
-
* otherwise, `false`.
|
|
204
|
-
* @throws If the native alarm module is unavailable.
|
|
205
|
-
*/
|
|
206
|
-
static isScheduled(alarmId: string): Promise<boolean>;
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Opens the device manufacturer's Auto Start settings on Android,
|
|
210
|
-
* where supported, to help users configure background launch behavior.
|
|
211
|
-
*
|
|
212
|
-
* On iOS, this method resolves to `false`.
|
|
213
|
-
*
|
|
214
|
-
* @returns A promise resolving to `true` if the settings screen was opened;
|
|
215
|
-
* otherwise, `false`.
|
|
216
|
-
* @throws If the native alarm module is unavailable.
|
|
217
|
-
*/
|
|
218
|
-
static openAutoStartSettings(): Promise<boolean>;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
export default AlarmService;
|
package/commitlint.config.mjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export default { extends: ['@commitlint/config-conventional'] };
|
package/index.d.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* React Native Alarms - Type definitions
|
|
3
|
-
* Cross-platform alarm management for React Native
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { Spec } from './specs/NativeAlarm';
|
|
7
|
-
|
|
8
|
-
export type {
|
|
9
|
-
AlarmRepeat,
|
|
10
|
-
AlarmConfig,
|
|
11
|
-
AlarmProp,
|
|
12
|
-
AlarmPermission,
|
|
13
|
-
AlarmEvent,
|
|
14
|
-
} from './src/types';
|
|
15
|
-
|
|
16
|
-
export { AlarmService } from './AlarmService';
|
|
17
|
-
|
|
18
|
-
declare const Alarm: Spec;
|
|
19
|
-
|
|
20
|
-
export default Alarm;
|
package/src/assets/recording.mp4
DELETED
|
Binary file
|
package/src/assets/sample-1.png
DELETED
|
Binary file
|
package/src/assets/sample-2.png
DELETED
|
Binary file
|
package/src/assets/sample-3.png
DELETED
|
Binary file
|