react-native-alarms 0.0.3 → 0.0.4
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/README.md +27 -1
- package/android-lib/build.gradle +36 -0
- package/android-lib/src/main/AndroidManifest.xml +49 -0
- package/android-lib/src/main/java/com/reactnativealarms/RNAlarmsPackage.kt +18 -0
- package/android-lib/src/main/res/values/styles.xml +3 -0
- package/index.d.ts +6 -2
- package/index.ts +2 -1
- package/ios/ReactNativeAlarm.xcodeproj/project.pbxproj +4 -0
- package/ios/alarm/AlarmModule.m +25 -0
- package/ios/alarm/AlarmModule.swift +19 -2
- package/ios/alarm/AlarmRingPresenter.swift +108 -0
- package/ios/alarm/NotificationBackend.swift +36 -7
- package/package.json +7 -20
- package/react-native-alarms.podspec +20 -0
- package/react-native.config.js +16 -0
- package/specs/NativeAlarm.ts +20 -2
- package/app.json +0 -4
- package/index.js +0 -12
- package/ios/Podfile.lock +0 -2179
package/README.md
CHANGED
|
@@ -45,7 +45,33 @@ Since this is a native module, you'll need to link it:
|
|
|
45
45
|
cd ios && pod install && cd ..
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
For Android, the native code
|
|
48
|
+
For Android, the native code auto-links, including the permissions, ring activity,
|
|
49
|
+
service and receivers in its manifest. No `MainApplication` or `AndroidManifest.xml`
|
|
50
|
+
changes are needed. Rebuild the app after installing.
|
|
51
|
+
|
|
52
|
+
### Register the ring screen
|
|
53
|
+
|
|
54
|
+
When an alarm rings, both platforms show a full-screen React component registered as
|
|
55
|
+
`AlarmRing`, with the alarm (`alarmId`, `title`, `body`, `snoozeEnabled`,
|
|
56
|
+
`snoozeMinutes`, `data`) as props. Register your own component in `index.js`:
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
import { AppRegistry } from 'react-native';
|
|
60
|
+
import { AlarmRingScreen } from './src/AlarmRingScreen';
|
|
61
|
+
|
|
62
|
+
AppRegistry.registerComponent('AlarmRing', () => AlarmRingScreen);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Call `Alarm.stopAlarm(alarmId)` or `Alarm.snoozeAlarm(alarmId, minutes)` from its
|
|
66
|
+
buttons; the screen closes itself.
|
|
67
|
+
|
|
68
|
+
### Direct module access
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import Alarm, { AlarmService } from 'react-native-alarms';
|
|
72
|
+
|
|
73
|
+
const id = await Alarm.scheduleAlarm({ timestamp: Date.now() + 60_000, title: 'Wake up' });
|
|
74
|
+
```
|
|
49
75
|
|
|
50
76
|
## Quick Start
|
|
51
77
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Library module autolinked into consumer apps (see react-native.config.js).
|
|
2
|
+
// Sources stay in the example app's tree; this module only references them.
|
|
3
|
+
apply plugin: "com.android.library"
|
|
4
|
+
apply plugin: "org.jetbrains.kotlin.android"
|
|
5
|
+
|
|
6
|
+
def safeExtGet(prop, fallback) {
|
|
7
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
android {
|
|
11
|
+
namespace "com.reactnativealarms"
|
|
12
|
+
compileSdk safeExtGet("compileSdkVersion", 35)
|
|
13
|
+
|
|
14
|
+
defaultConfig {
|
|
15
|
+
minSdkVersion safeExtGet("minSdkVersion", 24)
|
|
16
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 35)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
sourceSets {
|
|
20
|
+
main {
|
|
21
|
+
manifest.srcFile "src/main/AndroidManifest.xml"
|
|
22
|
+
java.srcDirs = ["src/main/java", "../android/app/src/main/java/com/reactnativealarm/alarm"]
|
|
23
|
+
res.srcDirs = ["src/main/res"]
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
repositories {
|
|
29
|
+
google()
|
|
30
|
+
mavenCentral()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
dependencies {
|
|
34
|
+
implementation "com.facebook.react:react-android"
|
|
35
|
+
implementation "androidx.appcompat:appcompat:1.7.0"
|
|
36
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
2
|
+
|
|
3
|
+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
4
|
+
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
|
|
5
|
+
<uses-permission
|
|
6
|
+
android:name="android.permission.SCHEDULE_EXACT_ALARM"
|
|
7
|
+
android:maxSdkVersion="32" />
|
|
8
|
+
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
9
|
+
<uses-permission android:name="android.permission.VIBRATE" />
|
|
10
|
+
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
|
11
|
+
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
|
12
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
13
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
|
14
|
+
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
|
15
|
+
|
|
16
|
+
<application>
|
|
17
|
+
<activity
|
|
18
|
+
android:name="com.alarm.AlarmRingActivity"
|
|
19
|
+
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
|
|
20
|
+
android:excludeFromRecents="true"
|
|
21
|
+
android:exported="false"
|
|
22
|
+
android:launchMode="singleInstance"
|
|
23
|
+
android:showWhenLocked="true"
|
|
24
|
+
android:taskAffinity=""
|
|
25
|
+
android:theme="@style/RNAlarmRingTheme"
|
|
26
|
+
android:turnScreenOn="true" />
|
|
27
|
+
|
|
28
|
+
<service
|
|
29
|
+
android:name="com.alarm.AlarmService"
|
|
30
|
+
android:exported="false"
|
|
31
|
+
android:foregroundServiceType="mediaPlayback" />
|
|
32
|
+
|
|
33
|
+
<receiver
|
|
34
|
+
android:name="com.alarm.AlarmReceiver"
|
|
35
|
+
android:exported="false" />
|
|
36
|
+
|
|
37
|
+
<receiver
|
|
38
|
+
android:name="com.alarm.BootReceiver"
|
|
39
|
+
android:enabled="true"
|
|
40
|
+
android:exported="true">
|
|
41
|
+
<intent-filter>
|
|
42
|
+
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
|
43
|
+
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
|
44
|
+
<action android:name="android.intent.action.TIME_SET" />
|
|
45
|
+
<action android:name="android.intent.action.TIMEZONE_CHANGED" />
|
|
46
|
+
</intent-filter>
|
|
47
|
+
</receiver>
|
|
48
|
+
</application>
|
|
49
|
+
</manifest>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
package com.reactnativealarms
|
|
2
|
+
|
|
3
|
+
import com.alarm.AlarmPackage
|
|
4
|
+
import com.facebook.react.BaseReactPackage
|
|
5
|
+
import com.facebook.react.bridge.NativeModule
|
|
6
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
|
|
9
|
+
// Autolinking entry point: the RN CLI only detects ReactPackage classes located inside
|
|
10
|
+
// android-lib/, while the real implementation lives in the example app's source tree.
|
|
11
|
+
class RNAlarmsPackage : BaseReactPackage() {
|
|
12
|
+
private val impl = AlarmPackage()
|
|
13
|
+
|
|
14
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
|
|
15
|
+
impl.getModule(name, reactContext)
|
|
16
|
+
|
|
17
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = impl.getReactModuleInfoProvider()
|
|
18
|
+
}
|
package/index.d.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Cross-platform alarm management for React Native
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import type { Spec } from './specs/NativeAlarm';
|
|
7
|
+
|
|
6
8
|
export type {
|
|
7
9
|
AlarmRepeat,
|
|
8
10
|
AlarmConfig,
|
|
@@ -12,5 +14,7 @@ export type {
|
|
|
12
14
|
Spec as AlarmModuleSpec,
|
|
13
15
|
} from './specs/NativeAlarm';
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
export { AlarmService } from './AlarmService';
|
|
18
|
+
|
|
19
|
+
declare const Alarm: Spec;
|
|
20
|
+
export default Alarm;
|
package/index.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
7A1A000000000000000B0004 /* AlarmStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1A000000000000000A0004 /* AlarmStore.swift */; };
|
|
19
19
|
7A1A000000000000000B0005 /* AlarmKitBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1A000000000000000A0005 /* AlarmKitBackend.swift */; };
|
|
20
20
|
7A1A000000000000000B0006 /* NotificationBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1A000000000000000A0006 /* NotificationBackend.swift */; };
|
|
21
|
+
7A1A000000000000000B0007 /* AlarmRingPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1A000000000000000A0007 /* AlarmRingPresenter.swift */; };
|
|
21
22
|
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
|
|
22
23
|
/* End PBXBuildFile section */
|
|
23
24
|
|
|
@@ -37,6 +38,7 @@
|
|
|
37
38
|
7A1A000000000000000A0004 /* AlarmStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmStore.swift; sourceTree = "<group>"; };
|
|
38
39
|
7A1A000000000000000A0005 /* AlarmKitBackend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmKitBackend.swift; sourceTree = "<group>"; };
|
|
39
40
|
7A1A000000000000000A0006 /* NotificationBackend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationBackend.swift; sourceTree = "<group>"; };
|
|
41
|
+
7A1A000000000000000A0007 /* AlarmRingPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmRingPresenter.swift; sourceTree = "<group>"; };
|
|
40
42
|
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = ReactNativeAlarm/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
|
41
43
|
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
|
42
44
|
/* End PBXFileReference section */
|
|
@@ -84,6 +86,7 @@
|
|
|
84
86
|
7A1A000000000000000A0004 /* AlarmStore.swift */,
|
|
85
87
|
7A1A000000000000000A0005 /* AlarmKitBackend.swift */,
|
|
86
88
|
7A1A000000000000000A0006 /* NotificationBackend.swift */,
|
|
89
|
+
7A1A000000000000000A0007 /* AlarmRingPresenter.swift */,
|
|
87
90
|
);
|
|
88
91
|
path = alarm;
|
|
89
92
|
sourceTree = "<group>";
|
|
@@ -283,6 +286,7 @@
|
|
|
283
286
|
7A1A000000000000000B0004 /* AlarmStore.swift in Sources */,
|
|
284
287
|
7A1A000000000000000B0005 /* AlarmKitBackend.swift in Sources */,
|
|
285
288
|
7A1A000000000000000B0006 /* NotificationBackend.swift in Sources */,
|
|
289
|
+
7A1A000000000000000B0007 /* AlarmRingPresenter.swift in Sources */,
|
|
286
290
|
);
|
|
287
291
|
runOnlyForDeploymentPostprocessing = 0;
|
|
288
292
|
};
|
package/ios/alarm/AlarmModule.m
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
#import <React/RCTBridgeModule.h>
|
|
2
2
|
#import <React/RCTEventEmitter.h>
|
|
3
|
+
#import <UIKit/UIKit.h>
|
|
4
|
+
|
|
5
|
+
// The notification delegate must be set before launch finishes, or a tap on an alarm
|
|
6
|
+
// notification that launches the app is lost. JS creates the module lazily, so install
|
|
7
|
+
// it here instead of requiring an AppDelegate change in every app.
|
|
8
|
+
@interface RNAlarmBootstrap : NSObject
|
|
9
|
+
@end
|
|
10
|
+
|
|
11
|
+
@implementation RNAlarmBootstrap
|
|
12
|
+
|
|
13
|
+
+ (void)load
|
|
14
|
+
{
|
|
15
|
+
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification
|
|
16
|
+
object:nil
|
|
17
|
+
queue:nil
|
|
18
|
+
usingBlock:^(NSNotification *note) {
|
|
19
|
+
Class backend = NSClassFromString(@"RNAlarmNotificationBackend");
|
|
20
|
+
SEL install = NSSelectorFromString(@"install");
|
|
21
|
+
if ([backend respondsToSelector:install]) {
|
|
22
|
+
((void (*)(id, SEL))[backend methodForSelector:install])(backend, install);
|
|
23
|
+
}
|
|
24
|
+
}];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@end
|
|
3
28
|
|
|
4
29
|
// Exposes the Swift `AlarmModule` to JS as `NativeModules.Alarm`, the same name
|
|
5
30
|
// as Android's `AlarmModule.NAME`. Method names and signatures match Android.
|
|
@@ -20,13 +20,26 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
20
20
|
override init() {
|
|
21
21
|
super.init()
|
|
22
22
|
NotificationBackend.install()
|
|
23
|
-
NotificationBackend.shared.onEvent = { [weak self] name, alarm in self?.
|
|
23
|
+
NotificationBackend.shared.onEvent = { [weak self] name, alarm in self?.handle(name, alarm) }
|
|
24
24
|
#if canImport(AlarmKit)
|
|
25
25
|
if #available(iOS 26.0, *) {
|
|
26
|
-
AlarmKitBackend.shared.onEvent = { [weak self] name, alarm in self?.
|
|
26
|
+
AlarmKitBackend.shared.onEvent = { [weak self] name, alarm in self?.handle(name, alarm) }
|
|
27
27
|
AlarmKitBackend.shared.startObserving()
|
|
28
28
|
}
|
|
29
29
|
#endif
|
|
30
|
+
// App launched by tapping the alarm notification before this module existed.
|
|
31
|
+
if let ringing = NotificationBackend.shared.ringing {
|
|
32
|
+
DispatchQueue.main.async { [weak self] in AlarmRingPresenter.present(ringing, bridge: self?.bridge) }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private func handle(_ name: String, _ alarm: AlarmData) {
|
|
37
|
+
if name == Self.eventFired {
|
|
38
|
+
AlarmRingPresenter.present(alarm, bridge: bridge)
|
|
39
|
+
} else {
|
|
40
|
+
AlarmRingPresenter.dismiss(alarm.id)
|
|
41
|
+
}
|
|
42
|
+
emit(name, alarm)
|
|
30
43
|
}
|
|
31
44
|
|
|
32
45
|
@objc override static func requiresMainQueueSetup() -> Bool { false }
|
|
@@ -61,6 +74,7 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
61
74
|
}
|
|
62
75
|
|
|
63
76
|
private func cancelNative(_ id: String) {
|
|
77
|
+
AlarmRingPresenter.dismiss(id)
|
|
64
78
|
NotificationBackend.shared.cancel(id)
|
|
65
79
|
#if canImport(AlarmKit)
|
|
66
80
|
if #available(iOS 26.0, *) { AlarmKitBackend.shared.cancel(id) }
|
|
@@ -207,6 +221,7 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
207
221
|
|
|
208
222
|
@objc(cancelAllAlarms:reject:)
|
|
209
223
|
func cancelAllAlarms(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
224
|
+
AlarmRingPresenter.dismiss()
|
|
210
225
|
AlarmStore.all().forEach { cancelNative($0.id) }
|
|
211
226
|
NotificationBackend.shared.cancelAll()
|
|
212
227
|
#if canImport(AlarmKit)
|
|
@@ -220,6 +235,7 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
220
235
|
|
|
221
236
|
@objc(stopAlarm:resolve:reject:)
|
|
222
237
|
func stopAlarm(_ alarmId: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
238
|
+
AlarmRingPresenter.dismiss(alarmId)
|
|
223
239
|
NotificationBackend.shared.stop(alarmId)
|
|
224
240
|
#if canImport(AlarmKit)
|
|
225
241
|
if #available(iOS 26.0, *) { AlarmKitBackend.shared.stop(alarmId) }
|
|
@@ -233,6 +249,7 @@ final class AlarmModule: RCTEventEmitter {
|
|
|
233
249
|
return reject("E_NOT_FOUND", "Alarm \(alarmId) not found", nil)
|
|
234
250
|
}
|
|
235
251
|
let mins = minutes > 0 ? Int(minutes) : alarm.snoozeMinutes
|
|
252
|
+
AlarmRingPresenter.dismiss(alarmId)
|
|
236
253
|
|
|
237
254
|
#if canImport(AlarmKit)
|
|
238
255
|
if #available(iOS 26.0, *), useAlarmKit {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import React
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
/// iOS counterpart of Android's `AlarmRingActivity`: presents the React component
|
|
5
|
+
/// registered as `AlarmRing` full screen, with the ringing alarm as initial props.
|
|
6
|
+
///
|
|
7
|
+
/// iOS cannot launch an app or draw over the lock screen from a local notification,
|
|
8
|
+
/// so this shows when the alarm fires while the app is in the foreground, or when the
|
|
9
|
+
/// user opens the app by tapping the alarm notification.
|
|
10
|
+
final class AlarmRingPresenter {
|
|
11
|
+
static let componentName = "AlarmRing"
|
|
12
|
+
|
|
13
|
+
private static weak var controller: UIViewController?
|
|
14
|
+
private static var alarmId: String?
|
|
15
|
+
|
|
16
|
+
static func present(_ alarm: AlarmData, bridge: RCTBridge?, attempt: Int = 0) {
|
|
17
|
+
DispatchQueue.main.async {
|
|
18
|
+
if controller != nil, alarmId == alarm.id { return }
|
|
19
|
+
// Cold start from a notification tap: the root surface or window may not be
|
|
20
|
+
// ready yet, so retry briefly.
|
|
21
|
+
guard let surface = makeSurface(bridge, props: alarm.toDictionary()),
|
|
22
|
+
let top = topViewController() else {
|
|
23
|
+
if attempt < 20 {
|
|
24
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
|
25
|
+
present(alarm, bridge: bridge, attempt: attempt + 1)
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
NSLog("[react-native-alarms] Could not present %@: no surface presenter or window", componentName)
|
|
29
|
+
}
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let vc = UIViewController()
|
|
34
|
+
let root = RCTSurfaceHostingProxyRootView(surface: surface)
|
|
35
|
+
root.backgroundColor = .systemBackground
|
|
36
|
+
vc.view = root
|
|
37
|
+
surface.start()
|
|
38
|
+
vc.modalPresentationStyle = .fullScreen
|
|
39
|
+
vc.isModalInPresentation = true
|
|
40
|
+
|
|
41
|
+
let show = {
|
|
42
|
+
top.present(vc, animated: true)
|
|
43
|
+
controller = vc
|
|
44
|
+
alarmId = alarm.id
|
|
45
|
+
}
|
|
46
|
+
if let existing = controller {
|
|
47
|
+
existing.dismiss(animated: false, completion: show)
|
|
48
|
+
} else {
|
|
49
|
+
show()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Called when ringing ends for any reason (stop, snooze, cancel).
|
|
55
|
+
static func dismiss(_ id: String? = nil) {
|
|
56
|
+
DispatchQueue.main.async {
|
|
57
|
+
guard let vc = controller, id == nil || id == alarmId else { return }
|
|
58
|
+
controller = nil
|
|
59
|
+
alarmId = nil
|
|
60
|
+
vc.dismiss(animated: true)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private static let createSurface = NSSelectorFromString("createFabricSurfaceForModuleName:initialProperties:")
|
|
65
|
+
|
|
66
|
+
/// `RCTSurfacePresenter` implements `createFabricSurfaceForModuleName:initialProperties:`
|
|
67
|
+
/// without formally adopting `RCTSurfacePresenterStub`, and its header is ObjC++ (not
|
|
68
|
+
/// importable from Swift), so call it by selector.
|
|
69
|
+
private static func makeSurface(_ bridge: RCTBridge?, props: [String: Any]) -> RCTSurfaceProtocol? {
|
|
70
|
+
guard let presenter = surfacePresenter(bridge), presenter.responds(to: createSurface) else { return nil }
|
|
71
|
+
return presenter.perform(createSurface, with: componentName, with: props)?
|
|
72
|
+
.takeUnretainedValue() as? RCTSurfaceProtocol
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Bridgeless mode skips `bridge.surfacePresenter` (and injection) for modules
|
|
76
|
+
/// registered with `RCT_EXTERN_MODULE`, so fall back to the presenter of the app's
|
|
77
|
+
/// root React surface, which `RCTFabricSurface` keeps in a weak `_surfacePresenter` ivar.
|
|
78
|
+
private static func surfacePresenter(_ bridge: RCTBridge?) -> NSObject? {
|
|
79
|
+
if let p = bridge?.surfacePresenter() as? NSObject { return p }
|
|
80
|
+
guard let root = keyWindow()?.rootViewController?.view,
|
|
81
|
+
let hosting = findHostingView(in: root),
|
|
82
|
+
let surface = hosting.surface as AnyObject?,
|
|
83
|
+
let ivar = class_getInstanceVariable(type(of: surface), "_surfacePresenter")
|
|
84
|
+
else { return nil }
|
|
85
|
+
return object_getIvar(surface, ivar) as? NSObject
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private static func findHostingView(in view: UIView) -> RCTSurfaceHostingView? {
|
|
89
|
+
if let hosting = view as? RCTSurfaceHostingView { return hosting }
|
|
90
|
+
for sub in view.subviews {
|
|
91
|
+
if let hosting = findHostingView(in: sub) { return hosting }
|
|
92
|
+
}
|
|
93
|
+
return nil
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private static func keyWindow() -> UIWindow? {
|
|
97
|
+
UIApplication.shared.connectedScenes
|
|
98
|
+
.compactMap { $0 as? UIWindowScene }
|
|
99
|
+
.flatMap { $0.windows }
|
|
100
|
+
.first { $0.isKeyWindow }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private static func topViewController() -> UIViewController? {
|
|
104
|
+
var top = keyWindow()?.rootViewController
|
|
105
|
+
while let presented = top?.presentedViewController { top = presented }
|
|
106
|
+
return top
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -7,6 +7,7 @@ import UserNotifications
|
|
|
7
7
|
/// Limitations compared with a real alarm: sound plays for at most ~30 s, the
|
|
8
8
|
/// silent switch and Focus modes are respected, and `interval` repeats are
|
|
9
9
|
/// pre-scheduled as a batch of exact one-off notifications (see `schedule`).
|
|
10
|
+
@objc(RNAlarmNotificationBackend)
|
|
10
11
|
final class NotificationBackend: NSObject, UNUserNotificationCenterDelegate {
|
|
11
12
|
static let shared = NotificationBackend()
|
|
12
13
|
|
|
@@ -24,11 +25,23 @@ final class NotificationBackend: NSObject, UNUserNotificationCenterDelegate {
|
|
|
24
25
|
/// Wired by `AlarmModule` so native events reach JS.
|
|
25
26
|
var onEvent: ((String, AlarmData) -> Void)?
|
|
26
27
|
|
|
28
|
+
/// Delegate that was installed before us (e.g. Notifee); non-alarm notifications go to it.
|
|
29
|
+
private weak var previousDelegate: UNUserNotificationCenterDelegate?
|
|
30
|
+
private static var installed = false
|
|
31
|
+
|
|
27
32
|
/// Installs the delegate and actions. Call once at launch so a tap on a
|
|
28
33
|
/// notification that starts the app is delivered to the module.
|
|
29
|
-
static func install() {
|
|
34
|
+
@objc static func install() {
|
|
30
35
|
let center = UNUserNotificationCenter.current()
|
|
31
|
-
|
|
36
|
+
// Only once: a library that took over after us forwards to us, and taking the
|
|
37
|
+
// delegate back would make the two forward to each other forever.
|
|
38
|
+
if !installed {
|
|
39
|
+
installed = true
|
|
40
|
+
if center.delegate !== shared {
|
|
41
|
+
shared.previousDelegate = center.delegate
|
|
42
|
+
center.delegate = shared
|
|
43
|
+
}
|
|
44
|
+
}
|
|
32
45
|
let snooze = UNNotificationAction(identifier: actionSnooze, title: "Snooze", options: [])
|
|
33
46
|
let stop = UNNotificationAction(identifier: actionStop, title: "Stop", options: [.destructive])
|
|
34
47
|
center.setNotificationCategories([
|
|
@@ -196,11 +209,19 @@ final class NotificationBackend: NSObject, UNUserNotificationCenterDelegate {
|
|
|
196
209
|
willPresent notification: UNNotification,
|
|
197
210
|
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
|
|
198
211
|
) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
212
|
+
guard let a = alarm(from: notification) else {
|
|
213
|
+
if let previous = previousDelegate,
|
|
214
|
+
previous.responds(to: #selector(UNUserNotificationCenterDelegate.userNotificationCenter(_:willPresent:withCompletionHandler:))) {
|
|
215
|
+
previous.userNotificationCenter?(center, willPresent: notification, withCompletionHandler: completionHandler)
|
|
216
|
+
} else {
|
|
217
|
+
completionHandler([.banner, .list, .sound])
|
|
218
|
+
}
|
|
219
|
+
return
|
|
202
220
|
}
|
|
203
|
-
|
|
221
|
+
ringing = a
|
|
222
|
+
onEvent?(AlarmModule.eventFired, a)
|
|
223
|
+
// The ring screen takes over; keep the sound but skip the banner when it can show.
|
|
224
|
+
completionHandler(onEvent == nil ? [.banner, .list, .sound] : [.list, .sound])
|
|
204
225
|
}
|
|
205
226
|
|
|
206
227
|
func userNotificationCenter(
|
|
@@ -208,8 +229,16 @@ final class NotificationBackend: NSObject, UNUserNotificationCenterDelegate {
|
|
|
208
229
|
didReceive response: UNNotificationResponse,
|
|
209
230
|
withCompletionHandler completionHandler: @escaping () -> Void
|
|
210
231
|
) {
|
|
232
|
+
guard let a = alarm(from: response.notification) else {
|
|
233
|
+
if let previous = previousDelegate,
|
|
234
|
+
previous.responds(to: #selector(UNUserNotificationCenterDelegate.userNotificationCenter(_:didReceive:withCompletionHandler:))) {
|
|
235
|
+
previous.userNotificationCenter?(center, didReceive: response, withCompletionHandler: completionHandler)
|
|
236
|
+
} else {
|
|
237
|
+
completionHandler()
|
|
238
|
+
}
|
|
239
|
+
return
|
|
240
|
+
}
|
|
211
241
|
defer { completionHandler() }
|
|
212
|
-
guard let a = alarm(from: response.notification) else { return }
|
|
213
242
|
switch response.actionIdentifier {
|
|
214
243
|
case Self.actionSnooze:
|
|
215
244
|
snooze(a, minutes: a.snoozeMinutes)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-alarms",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "A powerful, cross-platform alarm management library for React Native",
|
|
5
5
|
"private": false,
|
|
6
6
|
"main": "index.ts",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"./service": {
|
|
14
14
|
"types": "./AlarmService.d.ts",
|
|
15
15
|
"default": "./AlarmService.ts"
|
|
16
|
-
}
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
17
18
|
},
|
|
18
19
|
"scripts": {
|
|
19
20
|
"android": "react-native run-android",
|
|
@@ -45,13 +46,10 @@
|
|
|
45
46
|
},
|
|
46
47
|
"license": "MIT",
|
|
47
48
|
"peerDependencies": {
|
|
48
|
-
"react": "
|
|
49
|
-
"react-native": "
|
|
49
|
+
"react": "*",
|
|
50
|
+
"react-native": "*"
|
|
50
51
|
},
|
|
51
52
|
"dependencies": {
|
|
52
|
-
"react": "19.3.0",
|
|
53
|
-
"react-native": "0.88.0",
|
|
54
|
-
"@react-native/new-app-screen": "0.88.0",
|
|
55
53
|
"react-native-safe-area-context": "^5.5.2"
|
|
56
54
|
},
|
|
57
55
|
"devDependencies": {
|
|
@@ -72,23 +70,12 @@
|
|
|
72
70
|
"eslint": "^8.19.0",
|
|
73
71
|
"jest": "^29.6.3",
|
|
74
72
|
"prettier": "2.8.8",
|
|
73
|
+
"react": "19.3.0",
|
|
74
|
+
"react-native": "0.88.0",
|
|
75
75
|
"react-test-renderer": "19.3.0",
|
|
76
76
|
"typescript": "^6.0.3"
|
|
77
77
|
},
|
|
78
78
|
"engines": {
|
|
79
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
80
|
}
|
|
94
81
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'react-native-alarms'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.license = package['license']
|
|
10
|
+
s.authors = package['author']['name']
|
|
11
|
+
s.homepage = package['repository']['url']
|
|
12
|
+
s.source = { :git => package['repository']['url'], :tag => "v#{s.version}" }
|
|
13
|
+
|
|
14
|
+
s.platforms = { :ios => '15.0' }
|
|
15
|
+
s.swift_version = '5.9'
|
|
16
|
+
s.source_files = 'ios/alarm/**/*.{h,m,mm,swift}'
|
|
17
|
+
s.frameworks = 'UserNotifications', 'UIKit'
|
|
18
|
+
|
|
19
|
+
install_modules_dependencies(s)
|
|
20
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Autolinking config for apps that install this package. The root android/ folder is
|
|
2
|
+
// the example app, so Android points at android-lib/ instead.
|
|
3
|
+
module.exports = {
|
|
4
|
+
dependency: {
|
|
5
|
+
platforms: {
|
|
6
|
+
android: {
|
|
7
|
+
sourceDir: './android-lib',
|
|
8
|
+
packageImportPath: 'import com.reactnativealarms.RNAlarmsPackage;',
|
|
9
|
+
packageInstance: 'new RNAlarmsPackage()',
|
|
10
|
+
},
|
|
11
|
+
ios: {
|
|
12
|
+
podspecPath: './react-native-alarms.podspec',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
};
|
package/specs/NativeAlarm.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TurboModule } from 'react-native';
|
|
2
|
-
import { TurboModuleRegistry } from 'react-native';
|
|
2
|
+
import { NativeModules, Platform, TurboModuleRegistry } from 'react-native';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Shared contract of the native `Alarm` module.
|
|
@@ -92,4 +92,22 @@ export interface Spec extends TurboModule {
|
|
|
92
92
|
removeListeners(count: number): void;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
const LINKING_ERROR =
|
|
96
|
+
"react-native-alarms: the native 'Alarm' module is not linked. " +
|
|
97
|
+
Platform.select({
|
|
98
|
+
ios: "Run 'pod install' in the ios folder, then rebuild the app.",
|
|
99
|
+
default: 'Rebuild the app after installing the package.',
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Both platforms register a legacy module, reachable through either registry
|
|
103
|
+
// depending on the architecture. Throw on first use, not on import.
|
|
104
|
+
const Alarm: Spec =
|
|
105
|
+
TurboModuleRegistry.get<Spec>('Alarm') ??
|
|
106
|
+
NativeModules.Alarm ??
|
|
107
|
+
new Proxy({} as Spec, {
|
|
108
|
+
get() {
|
|
109
|
+
throw new Error(LINKING_ERROR);
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
export default Alarm;
|
package/app.json
DELETED
package/index.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @format
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { AppRegistry } from 'react-native';
|
|
6
|
-
import App from './App';
|
|
7
|
-
import { name as appName } from './app.json';
|
|
8
|
-
import { AlarmRingScreen } from './AlarmRingScreen';
|
|
9
|
-
|
|
10
|
-
AppRegistry.registerComponent(appName, () => App);
|
|
11
|
-
|
|
12
|
-
AppRegistry.registerComponent('AlarmRing', () => AlarmRingScreen);
|