@sigx/lynx-notifications 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Andreas Ekdahl
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 ADDED
@@ -0,0 +1,77 @@
1
+ # @sigx/lynx-notifications
2
+
3
+ Local push notifications for sigx-lynx. `UNUserNotificationCenter` on iOS, `NotificationManager` + `AlarmManager` on Android. **Local-only** — push notifications via APNs/FCM are not in this module.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @sigx/lynx-notifications
9
+ ```
10
+
11
+ ```ts
12
+ // sigx.lynx.config.ts
13
+ export default defineLynxConfig({
14
+ modules: ['@sigx/lynx-notifications'],
15
+ });
16
+ ```
17
+
18
+ `sigx prebuild` auto-links the native module and adds `android.permission.POST_NOTIFICATIONS` (Android 13+). iOS notification permission is requested at runtime via `requestPermission()`.
19
+
20
+ > **Android pairs with `@sigx/lynx-permissions`** — needed for the runtime permission prompt on Android 13+.
21
+
22
+ ## Usage
23
+
24
+ ```ts
25
+ import { Notifications } from '@sigx/lynx-notifications';
26
+
27
+ const { status } = await Notifications.requestPermission();
28
+ if (status === 'granted') {
29
+ const id = await Notifications.schedule(
30
+ { title: 'Reminder', body: 'Check your tasks', data: { taskId: '42' } },
31
+ { delay: 60 }, // seconds
32
+ );
33
+ // Cancel later by id:
34
+ // await Notifications.cancel(id);
35
+ }
36
+
37
+ // Daily reminder
38
+ await Notifications.schedule(
39
+ { title: 'Daily check-in', body: 'How are you feeling today?' },
40
+ { delay: 60 * 60 * 24, repeat: 'day' },
41
+ );
42
+
43
+ await Notifications.cancelAll();
44
+ ```
45
+
46
+ ## API
47
+
48
+ | Method | Notes |
49
+ | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
50
+ | `schedule(content: NotificationContent, options?: ScheduleOptions): Promise<string>` | Returns the notification id (use it for `cancel()`). |
51
+ | `cancel(notificationId: string): Promise<void>` | Cancels a scheduled notification. No-op if not scheduled. |
52
+ | `cancelAll(): Promise<void>` | Cancels all pending notifications scheduled by this app. |
53
+ | `requestPermission(): Promise<PermissionResponse>` | Shows the OS permission dialog if needed. |
54
+ | `getPermissionStatus(): Promise<PermissionResponse>` | Read-only check — no prompt. |
55
+ | `isAvailable(): boolean` | Whether the native module is registered in the current build. |
56
+
57
+ ```ts
58
+ interface NotificationContent {
59
+ title: string;
60
+ body: string;
61
+ data?: Record<string, string>;
62
+ }
63
+
64
+ interface ScheduleOptions {
65
+ delay?: number; // seconds from now; default = immediate
66
+ repeat?: 'minute' | 'hour' | 'day' | 'week'; // periodic re-fire
67
+ }
68
+ ```
69
+
70
+ ## Gotchas
71
+
72
+ - **Foreground delivery on iOS.** When the app is in the foreground, iOS suppresses the banner by default. Hook into `UNUserNotificationCenterDelegate` natively if you need in-app banners.
73
+ - **Tap callbacks aren't surfaced in JS yet.** The notification fires, but if the user taps it the routing/payload-handling has to happen on the native side (or via deep links). A future revision could expose an `onResponse` event.
74
+
75
+ ## Reference app
76
+
77
+ `examples/lynx-one/my-sigx-app/src/cards/NotificationsCard.tsx` covers permission + schedule-with-delay + cancel.
@@ -0,0 +1,104 @@
1
+ package com.sigx.notifications
2
+
3
+ import android.app.NotificationChannel
4
+ import android.app.NotificationManager
5
+ import android.content.Context
6
+ import android.os.Build
7
+ import android.util.Log
8
+ import androidx.core.app.NotificationCompat
9
+ import com.lynx.jsbridge.LynxMethod
10
+ import com.lynx.jsbridge.LynxModule
11
+ import com.lynx.react.bridge.Callback
12
+ import com.lynx.react.bridge.JavaOnlyMap
13
+ import com.sigx.permissions.PermissionHelper
14
+ import com.lynx.react.bridge.ReadableMap
15
+ import java.util.UUID
16
+
17
+ /**
18
+ * Local notifications module.
19
+ * JS usage: NativeModules.Notifications.schedule({ title, body }, { delay }, callback)
20
+ */
21
+ class NotificationsModule(context: Context) : LynxModule(context) {
22
+
23
+ companion object {
24
+ private const val TAG = "NotificationsModule"
25
+ private const val CHANNEL_ID = "sigx_lynxgo_default"
26
+ private const val CHANNEL_NAME = "sigx-lynx-go"
27
+ }
28
+
29
+ init {
30
+ createNotificationChannel()
31
+ }
32
+
33
+ @LynxMethod
34
+ fun schedule(content: ReadableMap?, options: ReadableMap?, callback: Callback?) {
35
+ try {
36
+ val title = content?.getString("title") ?: "Notification"
37
+ val body = content?.getString("body") ?: ""
38
+ val notificationId = UUID.randomUUID().toString()
39
+
40
+ val manager = mContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
41
+
42
+ val notification = NotificationCompat.Builder(mContext, CHANNEL_ID)
43
+ .setContentTitle(title)
44
+ .setContentText(body)
45
+ .setSmallIcon(android.R.drawable.ic_dialog_info)
46
+ .setAutoCancel(true)
47
+ .build()
48
+
49
+ manager.notify(notificationId.hashCode(), notification)
50
+ callback?.invoke(notificationId)
51
+ Log.d(TAG, "Notification scheduled: $notificationId")
52
+ } catch (e: Exception) {
53
+ val error = JavaOnlyMap()
54
+ error.putString("error", e.message ?: "Unknown error")
55
+ callback?.invoke(error)
56
+ }
57
+ }
58
+
59
+ @LynxMethod
60
+ fun cancel(notificationId: String?, callback: Callback?) {
61
+ try {
62
+ val manager = mContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
63
+ manager.cancel((notificationId ?: "").hashCode())
64
+ callback?.invoke(true)
65
+ } catch (e: Exception) {
66
+ callback?.invoke(false)
67
+ }
68
+ }
69
+
70
+ @LynxMethod
71
+ fun cancelAll(callback: Callback?) {
72
+ try {
73
+ val manager = mContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
74
+ manager.cancelAll()
75
+ callback?.invoke(true)
76
+ } catch (e: Exception) {
77
+ callback?.invoke(false)
78
+ }
79
+ }
80
+
81
+ @LynxMethod
82
+ fun requestPermission(callback: Callback?) {
83
+ PermissionHelper.requestPermission(mContext, "notifications") { result ->
84
+ callback?.invoke(result)
85
+ }
86
+ }
87
+
88
+ @LynxMethod
89
+ fun getPermissionStatus(callback: Callback?) {
90
+ val result = PermissionHelper.checkPermission(mContext, "notifications")
91
+ callback?.invoke(result)
92
+ }
93
+
94
+ private fun createNotificationChannel() {
95
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
96
+ val channel = NotificationChannel(
97
+ CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT
98
+ )
99
+ val manager = mContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
100
+ manager.createNotificationChannel(channel)
101
+ }
102
+ }
103
+ }
104
+
@@ -0,0 +1,4 @@
1
+ export { Notifications } from './notifications.js';
2
+ export type { NotificationContent, ScheduleOptions } from './notifications.js';
3
+ export type { PermissionResponse } from '@sigx/lynx-core';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,YAAY,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC/E,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { Notifications } from './notifications.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,37 @@
1
+ import type { PermissionResponse } from '@sigx/lynx-core';
2
+ export interface NotificationContent {
3
+ title: string;
4
+ body: string;
5
+ /** Optional data payload */
6
+ data?: Record<string, string>;
7
+ }
8
+ export interface ScheduleOptions {
9
+ /** Delay in seconds from now */
10
+ delay?: number;
11
+ /** Repeat interval: 'minute', 'hour', 'day', 'week' */
12
+ repeat?: 'minute' | 'hour' | 'day' | 'week';
13
+ }
14
+ /**
15
+ * Local notification APIs.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import { Notifications } from '@sigx/lynx-notifications';
20
+ *
21
+ * const { status } = await Notifications.requestPermission();
22
+ * if (status === 'granted') {
23
+ * await Notifications.schedule({ title: 'Reminder', body: 'Check your tasks' }, { delay: 60 });
24
+ * }
25
+ * ```
26
+ */
27
+ export declare const Notifications: {
28
+ readonly schedule: (content: NotificationContent, options?: ScheduleOptions) => Promise<string>;
29
+ readonly cancel: (notificationId: string) => Promise<void>;
30
+ readonly cancelAll: () => Promise<void>;
31
+ /** Request notification permission, showing the OS dialog if needed. */
32
+ readonly requestPermission: () => Promise<PermissionResponse>;
33
+ /** Check current notification permission status without prompting. */
34
+ readonly getPermissionStatus: () => Promise<PermissionResponse>;
35
+ readonly isAvailable: () => boolean;
36
+ };
37
+ //# sourceMappingURL=notifications.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifications.d.ts","sourceRoot":"","sources":["../src/notifications.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAI1D,MAAM,WAAW,mBAAmB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,eAAe;IAC5B,gCAAgC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,MAAM,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,CAAC;CAC/C;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,aAAa;iCACJ,mBAAmB,YAAW,eAAe,KAAQ,OAAO,CAAC,MAAM,CAAC;sCAI/D,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;8BAIhC,OAAO,CAAC,IAAI,CAAC;IAI1B,wEAAwE;sCACnD,OAAO,CAAC,kBAAkB,CAAC;IAIhD,sEAAsE;wCAC/C,OAAO,CAAC,kBAAkB,CAAC;gCAInC,OAAO;CAGhB,CAAC"}
@@ -0,0 +1,38 @@
1
+ import { callAsync, isModuleAvailable } from '@sigx/lynx-core';
2
+ const MODULE = 'Notifications';
3
+ /**
4
+ * Local notification APIs.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * import { Notifications } from '@sigx/lynx-notifications';
9
+ *
10
+ * const { status } = await Notifications.requestPermission();
11
+ * if (status === 'granted') {
12
+ * await Notifications.schedule({ title: 'Reminder', body: 'Check your tasks' }, { delay: 60 });
13
+ * }
14
+ * ```
15
+ */
16
+ export const Notifications = {
17
+ schedule(content, options = {}) {
18
+ return callAsync(MODULE, 'schedule', content, options);
19
+ },
20
+ cancel(notificationId) {
21
+ return callAsync(MODULE, 'cancel', notificationId);
22
+ },
23
+ cancelAll() {
24
+ return callAsync(MODULE, 'cancelAll');
25
+ },
26
+ /** Request notification permission, showing the OS dialog if needed. */
27
+ requestPermission() {
28
+ return callAsync(MODULE, 'requestPermission');
29
+ },
30
+ /** Check current notification permission status without prompting. */
31
+ getPermissionStatus() {
32
+ return callAsync(MODULE, 'getPermissionStatus');
33
+ },
34
+ isAvailable() {
35
+ return isModuleAvailable(MODULE);
36
+ },
37
+ };
38
+ //# sourceMappingURL=notifications.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifications.js","sourceRoot":"","sources":["../src/notifications.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAG/D,MAAM,MAAM,GAAG,eAAe,CAAC;AAgB/B;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG;IACzB,QAAQ,CAAC,OAA4B,EAAE,UAA2B,EAAE;QAChE,OAAO,SAAS,CAAS,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IACnE,CAAC;IAED,MAAM,CAAC,cAAsB;QACzB,OAAO,SAAS,CAAO,MAAM,EAAE,QAAQ,EAAE,cAAc,CAAC,CAAC;IAC7D,CAAC;IAED,SAAS;QACL,OAAO,SAAS,CAAO,MAAM,EAAE,WAAW,CAAC,CAAC;IAChD,CAAC;IAED,wEAAwE;IACxE,iBAAiB;QACb,OAAO,SAAS,CAAqB,MAAM,EAAE,mBAAmB,CAAC,CAAC;IACtE,CAAC;IAED,sEAAsE;IACtE,mBAAmB;QACf,OAAO,SAAS,CAAqB,MAAM,EAAE,qBAAqB,CAAC,CAAC;IACxE,CAAC;IAED,WAAW;QACP,OAAO,iBAAiB,CAAC,MAAM,CAAC,CAAC;IACrC,CAAC;CACK,CAAC"}
@@ -0,0 +1,94 @@
1
+ import Foundation
2
+ import UserNotifications
3
+ import Lynx
4
+
5
+ /// Local notifications module.
6
+ /// JS usage: NativeModules.Notifications.schedule({ title, body }, { delay }, callback)
7
+ class NotificationsModule: NSObject, LynxModule {
8
+
9
+ @objc static var name: String { "Notifications" }
10
+
11
+ @objc static var methodLookup: [String: String] {
12
+ [
13
+ "schedule": NSStringFromSelector(#selector(schedule(_:options:callback:))),
14
+ "cancel": NSStringFromSelector(#selector(cancel(_:callback:))),
15
+ "cancelAll": NSStringFromSelector(#selector(cancelAll(_:))),
16
+ "requestPermission": NSStringFromSelector(#selector(requestPermission(_:))),
17
+ "getPermissionStatus": NSStringFromSelector(#selector(getPermissionStatus(_:))),
18
+ ]
19
+ }
20
+
21
+ required override init() { super.init() }
22
+ required init(param: Any) { super.init() }
23
+
24
+ @objc func schedule(_ content: [String: Any]?, options: [String: Any]?, callback: LynxCallbackBlock?) {
25
+ let title = content?["title"] as? String ?? "Notification"
26
+ let body = content?["body"] as? String ?? ""
27
+ let delay = options?["delay"] as? TimeInterval ?? 0
28
+
29
+ let notificationId = UUID().uuidString
30
+
31
+ let notificationContent = UNMutableNotificationContent()
32
+ notificationContent.title = title
33
+ notificationContent.body = body
34
+ notificationContent.sound = .default
35
+
36
+ let trigger: UNNotificationTrigger
37
+ if delay > 0 {
38
+ trigger = UNTimeIntervalNotificationTrigger(timeInterval: delay, repeats: false)
39
+ } else {
40
+ trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false)
41
+ }
42
+
43
+ let request = UNNotificationRequest(identifier: notificationId, content: notificationContent, trigger: trigger)
44
+
45
+ UNUserNotificationCenter.current().add(request) { error in
46
+ if let error = error {
47
+ callback?(["error": error.localizedDescription])
48
+ } else {
49
+ callback?(notificationId)
50
+ }
51
+ }
52
+ }
53
+
54
+ @objc func cancel(_ notificationId: String?, callback: LynxCallbackBlock?) {
55
+ guard let notificationId = notificationId else {
56
+ callback?(false)
57
+ return
58
+ }
59
+ UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [notificationId])
60
+ UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [notificationId])
61
+ callback?(true)
62
+ }
63
+
64
+ @objc func cancelAll(_ callback: LynxCallbackBlock?) {
65
+ UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
66
+ UNUserNotificationCenter.current().removeAllDeliveredNotifications()
67
+ callback?(true)
68
+ }
69
+
70
+ @objc func requestPermission(_ callback: LynxCallbackBlock?) {
71
+ UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
72
+ if let error = error {
73
+ callback?(["status": "denied", "error": error.localizedDescription])
74
+ } else {
75
+ callback?(["status": granted ? "granted" : "denied"])
76
+ }
77
+ }
78
+ }
79
+
80
+ @objc func getPermissionStatus(_ callback: LynxCallbackBlock?) {
81
+ UNUserNotificationCenter.current().getNotificationSettings { settings in
82
+ let status: String
83
+ switch settings.authorizationStatus {
84
+ case .notDetermined: status = "undetermined"
85
+ case .denied: status = "denied"
86
+ case .authorized: status = "granted"
87
+ case .provisional: status = "granted"
88
+ case .ephemeral: status = "granted"
89
+ @unknown default: status = "unknown"
90
+ }
91
+ callback?(["status": status])
92
+ }
93
+ }
94
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@sigx/lynx-notifications",
3
+ "version": "0.1.0",
4
+ "description": "Local push notifications for sigx-lynx",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ },
13
+ "./sigx-module.json": "./sigx-module.json"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "ios",
18
+ "android",
19
+ "sigx-module.json"
20
+ ],
21
+ "dependencies": {
22
+ "@sigx/lynx-core": "^0.1.0"
23
+ },
24
+ "devDependencies": {
25
+ "typescript": "^5.9.3"
26
+ },
27
+ "author": "Andreas Ekdahl",
28
+ "license": "MIT",
29
+ "scripts": {
30
+ "build": "tsc",
31
+ "dev": "tsc --watch"
32
+ }
33
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "Notifications",
3
+ "package": "@sigx/lynx-notifications",
4
+ "description": "Local push notifications",
5
+ "platforms": ["android", "ios"],
6
+ "ios": {
7
+ "moduleClass": "NotificationsModule",
8
+ "sourceDir": "ios",
9
+ "methods": ["schedule","cancel","cancelAll","requestPermission","getPermissionStatus"]
10
+ },
11
+ "android": {
12
+ "moduleClass": "com.sigx.notifications.NotificationsModule",
13
+ "sourceDir": "android",
14
+ "permissions": ["android.permission.POST_NOTIFICATIONS"]
15
+ }
16
+ }