@sigx/lynx-notifications 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,115 @@
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 androidx.core.app.NotificationCompat
8
+ import com.google.firebase.messaging.FirebaseMessagingService
9
+ import com.google.firebase.messaging.RemoteMessage
10
+
11
+ /**
12
+ * FCM entry point. Registered in AndroidManifest via the auto-linker
13
+ * (`android.services` in `signalx-module.json`).
14
+ *
15
+ * Handles two callbacks:
16
+ * - [onNewToken]: token rotated. Forwarded to JS via [PushEventBus] so the
17
+ * app can re-register with its backend (Azure Notification Hubs, etc.).
18
+ * - [onMessageReceived]: incoming push. Forwarded to JS regardless of app
19
+ * state. When the app is BACKGROUNDED and the payload carries a
20
+ * title/body, we additionally pop a system notification so the user has
21
+ * a visible entry point back into the app — without this, FCM data-only
22
+ * messages received while backgrounded silently land in JS but never
23
+ * surface in the system tray. When the app is foreground, we skip the
24
+ * system notif: the JS heap is alive and apps are expected to render
25
+ * their own in-app UI from `addPushListener`.
26
+ */
27
+ class SigxFirebaseMessagingService : FirebaseMessagingService() {
28
+
29
+ override fun onNewToken(token: String) {
30
+ super.onNewToken(token)
31
+ PushEventBus.publishToken(token, platform = "fcm")
32
+ }
33
+
34
+ override fun onMessageReceived(message: RemoteMessage) {
35
+ super.onMessageReceived(message)
36
+ val notification = message.notification
37
+ val title = notification?.title
38
+ val body = notification?.body
39
+ val data = message.data
40
+ // foreground=true here is "the JS heap is alive". FCM service runs
41
+ // regardless of activity foreground state, but the JS shim and apps
42
+ // typically treat `foreground` as "received while app was in use" —
43
+ // we approximate by checking the process importance.
44
+ val foreground = isAppInForeground()
45
+ PushEventBus.publishMessage(
46
+ title = title,
47
+ body = body,
48
+ data = data,
49
+ foreground = foreground,
50
+ )
51
+
52
+ if (!foreground && title != null) {
53
+ showSystemNotification(title, body ?: "", data)
54
+ }
55
+ }
56
+
57
+ private fun isAppInForeground(): Boolean {
58
+ val appProcessInfo = android.app.ActivityManager.RunningAppProcessInfo()
59
+ android.app.ActivityManager.getMyMemoryState(appProcessInfo)
60
+ return appProcessInfo.importance == android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND ||
61
+ appProcessInfo.importance == android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE
62
+ }
63
+
64
+ private fun showSystemNotification(title: String, body: String, data: Map<String, String>) {
65
+ val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
66
+ // The FCM service can fire BEFORE NotificationsModule is ever
67
+ // instantiated (the JS bridge may not have loaded yet — particularly
68
+ // for data-only messages that wake the app from a fully terminated
69
+ // state). Ensure the notification channel exists here too so
70
+ // `notify()` on O+ doesn't silently drop the post.
71
+ ensureChannel(manager)
72
+ // Prefer the app's launcher icon over the stock dialog icon — Android
73
+ // renders small-icons as monochrome silhouettes, so a generic system
74
+ // shape would show up as a plain square. `applicationInfo.icon` is
75
+ // the manifest <application android:icon>, which apps always set.
76
+ val smallIcon = applicationInfo.icon.takeIf { it != 0 } ?: android.R.drawable.ic_dialog_info
77
+ val builder = NotificationCompat.Builder(this, NotificationsModule.CHANNEL_ID)
78
+ .setContentTitle(title)
79
+ .setContentText(body)
80
+ .setSmallIcon(smallIcon)
81
+ .setAutoCancel(true)
82
+
83
+ // Stash data on the launch intent so [PushActivityHook.onNewIntent]
84
+ // can route the tap to [PushEventBus.publishResponse] / capture as
85
+ // the cold-start payload.
86
+ val pm = packageManager
87
+ val launchIntent = pm.getLaunchIntentForPackage(packageName)?.apply {
88
+ putExtra(PushActivityHook.EXTRA_NOTIFICATION_TAP, true)
89
+ for ((k, v) in data) putExtra("sigx_push_data_$k", v)
90
+ }
91
+ val notificationId = (data["notification_id"] ?: data["notificationId"] ?: System.currentTimeMillis().toString())
92
+ if (launchIntent != null) {
93
+ val pending = android.app.PendingIntent.getActivity(
94
+ this,
95
+ notificationId.hashCode(),
96
+ launchIntent,
97
+ android.app.PendingIntent.FLAG_UPDATE_CURRENT or android.app.PendingIntent.FLAG_IMMUTABLE,
98
+ )
99
+ builder.setContentIntent(pending)
100
+ }
101
+
102
+ manager.notify(notificationId.hashCode(), builder.build())
103
+ }
104
+
105
+ private fun ensureChannel(manager: NotificationManager) {
106
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
107
+ if (manager.getNotificationChannel(NotificationsModule.CHANNEL_ID) != null) return
108
+ val channel = NotificationChannel(
109
+ NotificationsModule.CHANNEL_ID,
110
+ "sigx-lynx-go",
111
+ NotificationManager.IMPORTANCE_DEFAULT,
112
+ )
113
+ manager.createNotificationChannel(channel)
114
+ }
115
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { Notifications } from './notifications.js';
2
- export type { NotificationContent, ScheduleOptions } from './notifications.js';
2
+ export type { NotificationContent, ScheduleOptions, RegisterPushResult, UnregisterPushResult, NotificationResponse, PushTokenEvent, PushTokenError, RemoteMessage, } from './notifications.js';
3
+ export { addTokenListener, addTokenErrorListener, addPushListener, addNotificationResponseListener, } from './push.js';
3
4
  export type { PermissionResponse } from '@sigx/lynx-core';
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,YAAY,EACR,mBAAmB,EACnB,eAAe,EACf,kBAAkB,EAClB,oBAAoB,EACpB,oBAAoB,EACpB,cAAc,EACd,cAAc,EACd,aAAa,GAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACH,gBAAgB,EAChB,qBAAqB,EACrB,eAAe,EACf,+BAA+B,GAClC,MAAM,WAAW,CAAC;AACnB,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { Notifications } from './notifications.js';
2
+ export { addTokenListener, addTokenErrorListener, addPushListener, addNotificationResponseListener, } from './push.js';
2
3
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAWnD,OAAO,EACH,gBAAgB,EAChB,qBAAqB,EACrB,eAAe,EACf,+BAA+B,GAClC,MAAM,WAAW,CAAC"}
@@ -1,8 +1,9 @@
1
1
  import type { PermissionResponse } from '@sigx/lynx-core';
2
+ import { addTokenListener, addTokenErrorListener, addPushListener, addNotificationResponseListener, type NotificationResponse, type PushTokenEvent, type PushTokenError, type RemoteMessage } from './push.js';
2
3
  export interface NotificationContent {
3
4
  title: string;
4
5
  body: string;
5
- /** Optional data payload */
6
+ /** Optional data payload. Round-tripped to JS on tap responses. */
6
7
  data?: Record<string, string>;
7
8
  }
8
9
  export interface ScheduleOptions {
@@ -11,27 +12,100 @@ export interface ScheduleOptions {
11
12
  /** Repeat interval: 'minute', 'hour', 'day', 'week' */
12
13
  repeat?: 'minute' | 'hour' | 'day' | 'week';
13
14
  }
15
+ export interface RegisterPushResult {
16
+ token?: string;
17
+ platform?: 'apns' | 'fcm';
18
+ /** iOS resolves with `{ dispatched: true }` — the real token arrives via `addTokenListener`. */
19
+ dispatched?: boolean;
20
+ error?: string;
21
+ }
22
+ export interface UnregisterPushResult {
23
+ ok: boolean;
24
+ error?: string;
25
+ }
14
26
  /**
15
- * Local notification APIs.
27
+ * Local + remote notification APIs.
16
28
  *
17
29
  * @example
18
30
  * ```ts
19
31
  * import { Notifications } from '@sigx/lynx-notifications';
20
32
  *
21
33
  * const { status } = await Notifications.requestPermission();
22
- * if (status === 'granted') {
23
- * await Notifications.schedule({ title: 'Reminder', body: 'Check your tasks' }, { delay: 60 });
24
- * }
34
+ * if (status !== 'granted') return;
35
+ *
36
+ * // Remote push registration. On iOS the real token arrives via addTokenListener;
37
+ * // on Android the token is in the promise result.
38
+ * Notifications.addTokenListener(({ token, platform }) => {
39
+ * // POST token + platform to your backend (Azure Notification Hubs / Firebase Admin / …)
40
+ * });
41
+ * Notifications.addPushListener((msg) => {
42
+ * console.log('push received', msg);
43
+ * });
44
+ * Notifications.addNotificationResponseListener((resp) => {
45
+ * // user tapped the notification — route them somewhere
46
+ * });
47
+ *
48
+ * await Notifications.registerForPushNotifications();
49
+ *
50
+ * // Cold-start tap (if any)
51
+ * const initial = await Notifications.getInitialNotification();
25
52
  * ```
26
53
  */
27
54
  export declare const Notifications: {
28
- schedule(content: NotificationContent, options?: ScheduleOptions): Promise<string>;
29
- cancel(notificationId: string): Promise<void>;
30
- cancelAll(): Promise<void>;
55
+ readonly schedule: (content: NotificationContent, options?: ScheduleOptions) => Promise<string>;
56
+ readonly cancel: (notificationId: string) => Promise<void>;
57
+ readonly cancelAll: () => Promise<void>;
31
58
  /** Request notification permission, showing the OS dialog if needed. */
32
- requestPermission(): Promise<PermissionResponse>;
59
+ readonly requestPermission: () => Promise<PermissionResponse>;
33
60
  /** Check current notification permission status without prompting. */
34
- getPermissionStatus(): Promise<PermissionResponse>;
35
- isAvailable(): boolean;
61
+ readonly getPermissionStatus: () => Promise<PermissionResponse>;
62
+ /**
63
+ * Trigger remote-push registration.
64
+ *
65
+ * iOS: dispatches `application.registerForRemoteNotifications()`. The
66
+ * token (or error) arrives asynchronously via `addTokenListener` /
67
+ * `addTokenErrorListener`. The promise resolves with `{ dispatched: true }`
68
+ * once the call has been made.
69
+ *
70
+ * Android: resolves directly with the FCM token. Also publishes the token
71
+ * via the listener channel so JS code that wires both paths sees one
72
+ * canonical event.
73
+ */
74
+ readonly registerForPushNotifications: () => Promise<RegisterPushResult>;
75
+ /**
76
+ * Detach from APNs / FCM.
77
+ *
78
+ * iOS: synchronous — resolves with `{ ok: true }` after
79
+ * `unregisterForRemoteNotifications` is called.
80
+ * Android: awaits the FCM `deleteToken()` Task; resolves with
81
+ * `{ ok: false, error }` if the network call fails so the JS caller
82
+ * doesn't believe it's unregistered while the server keeps pushing.
83
+ * Failures also publish on the `addTokenErrorListener` channel.
84
+ */
85
+ readonly unregisterForPushNotifications: () => Promise<UnregisterPushResult>;
86
+ /**
87
+ * iOS: app-icon badge count. iOS 16+ uses
88
+ * `UNUserNotificationCenter.setBadgeCount`; older falls back to
89
+ * `applicationIconBadgeNumber`.
90
+ *
91
+ * Android: no-op. Stock Android has no portable badging API (it's
92
+ * vendor-specific — Samsung's `ShortcutBadger`, etc.) and this call
93
+ * does NOT clear pending notifications — callers wanting that should
94
+ * use `cancelAll()` directly.
95
+ */
96
+ readonly setBadgeCount: (count: number) => Promise<void>;
97
+ /** iOS: current badge number. Android: always 0 (no portable read API). */
98
+ readonly getBadgeCount: () => Promise<number>;
99
+ /**
100
+ * If the app was launched by a notification tap, returns the payload.
101
+ * One-shot: subsequent calls return null. Call exactly once during startup.
102
+ */
103
+ readonly getInitialNotification: () => Promise<NotificationResponse | null>;
104
+ readonly addTokenListener: typeof addTokenListener;
105
+ readonly addTokenErrorListener: typeof addTokenErrorListener;
106
+ readonly addPushListener: typeof addPushListener;
107
+ readonly addNotificationResponseListener: typeof addNotificationResponseListener;
108
+ readonly isAvailable: () => boolean;
36
109
  };
110
+ export type { NotificationResponse, PushTokenEvent, PushTokenError, RemoteMessage };
37
111
  //# sourceMappingURL=notifications.d.ts.map
@@ -1 +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;IACtB,QAAQ,UAAU,mBAAmB,YAAW,eAAe,GAAQ,OAAO,CAAC,MAAM,CAAC;IAItF,MAAM,iBAAiB,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI7C,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAI1B,wEAAwE;IACxE,iBAAiB,IAAI,OAAO,CAAC,kBAAkB,CAAC;IAIhD,sEAAsE;IACtE,mBAAmB,IAAI,OAAO,CAAC,kBAAkB,CAAC;IAIlD,WAAW,IAAI,OAAO;CAGhB,CAAC"}
1
+ {"version":3,"file":"notifications.d.ts","sourceRoot":"","sources":["../src/notifications.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,EACH,gBAAgB,EAChB,qBAAqB,EACrB,eAAe,EACf,+BAA+B,EAC/B,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,aAAa,EACrB,MAAM,WAAW,CAAC;AAInB,MAAM,WAAW,mBAAmB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,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,MAAM,WAAW,kBAAkB;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC1B,gGAAgG;IAChG,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACjC,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;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;IAIlD;;;;;;;;;;;OAWG;iDAC6B,OAAO,CAAC,kBAAkB,CAAC;IAI3D;;;;;;;;;OASG;mDAC+B,OAAO,CAAC,oBAAoB,CAAC;IAI/D;;;;;;;;;OASG;oCACkB,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;IAI3C,2EAA2E;kCAC1D,OAAO,CAAC,MAAM,CAAC;IAIhC;;;OAGG;2CACuB,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;;;;;gCAa/C,OAAO;CAGhB,CAAC;AAEX,YAAY,EAAE,oBAAoB,EAAE,cAAc,EAAE,cAAc,EAAE,aAAa,EAAE,CAAC"}
@@ -1,16 +1,32 @@
1
1
  import { callAsync, isModuleAvailable } from '@sigx/lynx-core';
2
+ import { addTokenListener, addTokenErrorListener, addPushListener, addNotificationResponseListener, } from './push.js';
2
3
  const MODULE = 'Notifications';
3
4
  /**
4
- * Local notification APIs.
5
+ * Local + remote notification APIs.
5
6
  *
6
7
  * @example
7
8
  * ```ts
8
9
  * import { Notifications } from '@sigx/lynx-notifications';
9
10
  *
10
11
  * const { status } = await Notifications.requestPermission();
11
- * if (status === 'granted') {
12
- * await Notifications.schedule({ title: 'Reminder', body: 'Check your tasks' }, { delay: 60 });
13
- * }
12
+ * if (status !== 'granted') return;
13
+ *
14
+ * // Remote push registration. On iOS the real token arrives via addTokenListener;
15
+ * // on Android the token is in the promise result.
16
+ * Notifications.addTokenListener(({ token, platform }) => {
17
+ * // POST token + platform to your backend (Azure Notification Hubs / Firebase Admin / …)
18
+ * });
19
+ * Notifications.addPushListener((msg) => {
20
+ * console.log('push received', msg);
21
+ * });
22
+ * Notifications.addNotificationResponseListener((resp) => {
23
+ * // user tapped the notification — route them somewhere
24
+ * });
25
+ *
26
+ * await Notifications.registerForPushNotifications();
27
+ *
28
+ * // Cold-start tap (if any)
29
+ * const initial = await Notifications.getInitialNotification();
14
30
  * ```
15
31
  */
16
32
  export const Notifications = {
@@ -31,8 +47,68 @@ export const Notifications = {
31
47
  getPermissionStatus() {
32
48
  return callAsync(MODULE, 'getPermissionStatus');
33
49
  },
50
+ /**
51
+ * Trigger remote-push registration.
52
+ *
53
+ * iOS: dispatches `application.registerForRemoteNotifications()`. The
54
+ * token (or error) arrives asynchronously via `addTokenListener` /
55
+ * `addTokenErrorListener`. The promise resolves with `{ dispatched: true }`
56
+ * once the call has been made.
57
+ *
58
+ * Android: resolves directly with the FCM token. Also publishes the token
59
+ * via the listener channel so JS code that wires both paths sees one
60
+ * canonical event.
61
+ */
62
+ registerForPushNotifications() {
63
+ return callAsync(MODULE, 'registerForPushNotifications');
64
+ },
65
+ /**
66
+ * Detach from APNs / FCM.
67
+ *
68
+ * iOS: synchronous — resolves with `{ ok: true }` after
69
+ * `unregisterForRemoteNotifications` is called.
70
+ * Android: awaits the FCM `deleteToken()` Task; resolves with
71
+ * `{ ok: false, error }` if the network call fails so the JS caller
72
+ * doesn't believe it's unregistered while the server keeps pushing.
73
+ * Failures also publish on the `addTokenErrorListener` channel.
74
+ */
75
+ unregisterForPushNotifications() {
76
+ return callAsync(MODULE, 'unregisterForPushNotifications');
77
+ },
78
+ /**
79
+ * iOS: app-icon badge count. iOS 16+ uses
80
+ * `UNUserNotificationCenter.setBadgeCount`; older falls back to
81
+ * `applicationIconBadgeNumber`.
82
+ *
83
+ * Android: no-op. Stock Android has no portable badging API (it's
84
+ * vendor-specific — Samsung's `ShortcutBadger`, etc.) and this call
85
+ * does NOT clear pending notifications — callers wanting that should
86
+ * use `cancelAll()` directly.
87
+ */
88
+ setBadgeCount(count) {
89
+ return callAsync(MODULE, 'setBadgeCount', count);
90
+ },
91
+ /** iOS: current badge number. Android: always 0 (no portable read API). */
92
+ getBadgeCount() {
93
+ return callAsync(MODULE, 'getBadgeCount');
94
+ },
95
+ /**
96
+ * If the app was launched by a notification tap, returns the payload.
97
+ * One-shot: subsequent calls return null. Call exactly once during startup.
98
+ */
99
+ getInitialNotification() {
100
+ return callAsync(MODULE, 'getInitialNotification');
101
+ },
102
+ // ── Event subscriptions ─────────────────────────────────────────────────
103
+ // Re-exported so consumers can chain `Notifications.addTokenListener(...)`
104
+ // without a second import. Each returns an unsubscribe function.
105
+ addTokenListener,
106
+ addTokenErrorListener,
107
+ addPushListener,
108
+ addNotificationResponseListener,
34
109
  isAvailable() {
35
110
  return isModuleAvailable(MODULE);
36
111
  },
37
112
  };
113
+ // `UnregisterPushResult` is declared above; re-export via index.ts.
38
114
  //# sourceMappingURL=notifications.js.map
@@ -1 +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,OAAO,GAAoB,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"}
1
+ {"version":3,"file":"notifications.js","sourceRoot":"","sources":["../src/notifications.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAE/D,OAAO,EACH,gBAAgB,EAChB,qBAAqB,EACrB,eAAe,EACf,+BAA+B,GAKlC,MAAM,WAAW,CAAC;AAEnB,MAAM,MAAM,GAAG,eAAe,CAAC;AA6B/B;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG;IACzB,QAAQ,CAAC,OAA4B,EAAE,OAAO,GAAoB,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;;;;;;;;;;;OAWG;IACH,4BAA4B;QACxB,OAAO,SAAS,CAAqB,MAAM,EAAE,8BAA8B,CAAC,CAAC;IACjF,CAAC;IAED;;;;;;;;;OASG;IACH,8BAA8B;QAC1B,OAAO,SAAS,CAAuB,MAAM,EAAE,gCAAgC,CAAC,CAAC;IACrF,CAAC;IAED;;;;;;;;;OASG;IACH,aAAa,CAAC,KAAa;QACvB,OAAO,SAAS,CAAO,MAAM,EAAE,eAAe,EAAE,KAAK,CAAC,CAAC;IAC3D,CAAC;IAED,2EAA2E;IAC3E,aAAa;QACT,OAAO,SAAS,CAAS,MAAM,EAAE,eAAe,CAAC,CAAC;IACtD,CAAC;IAED;;;OAGG;IACH,sBAAsB;QAClB,OAAO,SAAS,CAA8B,MAAM,EAAE,wBAAwB,CAAC,CAAC;IACpF,CAAC;IAED,2EAA2E;IAC3E,2EAA2E;IAC3E,iEAAiE;IAEjE,gBAAgB;IAChB,qBAAqB;IACrB,eAAe;IACf,+BAA+B;IAE/B,WAAW;QACP,OAAO,iBAAiB,CAAC,MAAM,CAAC,CAAC;IACrC,CAAC;CACK,CAAC;AAGX,oEAAoE"}
package/dist/push.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Remote-push event subscriptions. Backed by `GlobalEventEmitter` on four
3
+ * native channels: `__sigxPushToken`, `__sigxPushTokenError`,
4
+ * `__sigxPushMessage`, `__sigxNotificationResponse`. The native side carries
5
+ * the same channel names — JS shims here just adapt the listener-bag API.
6
+ */
7
+ export interface PushTokenEvent {
8
+ token: string;
9
+ platform: 'apns' | 'fcm';
10
+ }
11
+ export interface PushTokenError {
12
+ error: string;
13
+ }
14
+ export interface RemoteMessage {
15
+ title?: string;
16
+ body?: string;
17
+ data: Record<string, string>;
18
+ foreground: boolean;
19
+ }
20
+ export interface NotificationResponse {
21
+ notificationId: string;
22
+ data: Record<string, string>;
23
+ /** 'default' for the standard tap; custom action ids when categories ship. */
24
+ actionIdentifier: string;
25
+ }
26
+ export declare function addTokenListener(cb: (event: PushTokenEvent) => void): () => void;
27
+ export declare function addTokenErrorListener(cb: (event: PushTokenError) => void): () => void;
28
+ export declare function addPushListener(cb: (event: RemoteMessage) => void): () => void;
29
+ export declare function addNotificationResponseListener(cb: (event: NotificationResponse) => void): () => void;
30
+ //# sourceMappingURL=push.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push.d.ts","sourceRoot":"","sources":["../src/push.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,MAAM,WAAW,cAAc;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,GAAG,KAAK,CAAC;CAC5B;AAED,MAAM,WAAW,cAAc;IAC3B,KAAK,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,UAAU,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACjC,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,8EAA8E;IAC9E,gBAAgB,EAAE,MAAM,CAAC;CAC5B;AA+CD,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,MAAM,IAAI,CAEhF;AAED,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,MAAM,IAAI,CAErF;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,GAAG,MAAM,IAAI,CAE9E;AAED,wBAAgB,+BAA+B,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI,CAErG"}
package/dist/push.js ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Remote-push event subscriptions. Backed by `GlobalEventEmitter` on four
3
+ * native channels: `__sigxPushToken`, `__sigxPushTokenError`,
4
+ * `__sigxPushMessage`, `__sigxNotificationResponse`. The native side carries
5
+ * the same channel names — JS shims here just adapt the listener-bag API.
6
+ */
7
+ const TOKEN_CHANNEL = '__sigxPushToken';
8
+ const TOKEN_ERROR_CHANNEL = '__sigxPushTokenError';
9
+ const MESSAGE_CHANNEL = '__sigxPushMessage';
10
+ const RESPONSE_CHANNEL = '__sigxNotificationResponse';
11
+ function emitter() {
12
+ if (typeof lynx === 'undefined')
13
+ return undefined;
14
+ const obj = lynx;
15
+ return obj.getJSModule?.('GlobalEventEmitter');
16
+ }
17
+ function subscribe(channel, cb) {
18
+ const e = emitter();
19
+ if (!e) {
20
+ // Web / SSR / test fallback: no native bridge, just return a no-op
21
+ // unsubscribe. The shim is a one-way data path — there's nothing
22
+ // useful to emulate.
23
+ return () => { };
24
+ }
25
+ const wrapped = (raw) => {
26
+ const event = (typeof raw === 'string' ? safeParse(raw) : raw);
27
+ if (event === undefined)
28
+ return;
29
+ try {
30
+ cb(event);
31
+ }
32
+ catch (err) {
33
+ console.warn(`[notifications] listener for ${channel} threw:`, err);
34
+ }
35
+ };
36
+ e.addListener(channel, wrapped);
37
+ return () => e.removeListener(channel, wrapped);
38
+ }
39
+ function safeParse(s) {
40
+ try {
41
+ return JSON.parse(s);
42
+ }
43
+ catch {
44
+ return undefined;
45
+ }
46
+ }
47
+ export function addTokenListener(cb) {
48
+ return subscribe(TOKEN_CHANNEL, cb);
49
+ }
50
+ export function addTokenErrorListener(cb) {
51
+ return subscribe(TOKEN_ERROR_CHANNEL, cb);
52
+ }
53
+ export function addPushListener(cb) {
54
+ return subscribe(MESSAGE_CHANNEL, cb);
55
+ }
56
+ export function addNotificationResponseListener(cb) {
57
+ return subscribe(RESPONSE_CHANNEL, cb);
58
+ }
59
+ //# sourceMappingURL=push.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push.js","sourceRoot":"","sources":["../src/push.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,aAAa,GAAG,iBAAiB,CAAC;AACxC,MAAM,mBAAmB,GAAG,sBAAsB,CAAC;AACnD,MAAM,eAAe,GAAG,mBAAmB,CAAC;AAC5C,MAAM,gBAAgB,GAAG,4BAA4B,CAAC;AAoCtD,SAAS,OAAO;IACZ,IAAI,OAAO,IAAI,KAAK,WAAW;QAAE,OAAO,SAAS,CAAC;IAClD,MAAM,GAAG,GAAG,IAA2B,CAAC;IACxC,OAAO,GAAG,CAAC,WAAW,EAAE,CAAC,oBAAoB,CAAC,CAAC;AACnD,CAAC;AAED,SAAS,SAAS,CACd,OAAe,EACf,EAAsB;IAEtB,MAAM,CAAC,GAAG,OAAO,EAAE,CAAC;IACpB,IAAI,CAAC,CAAC,EAAE,CAAC;QACL,mEAAmE;QACnE,iEAAiE;QACjE,qBAAqB;QACrB,OAAO,GAAG,EAAE,GAAE,CAAC,CAAC;IACpB,CAAC;IACD,MAAM,OAAO,GAAG,CAAC,GAAY,EAAE,EAAE;QAC7B,MAAM,KAAK,GAAG,CAAC,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAkB,CAAC;QAChF,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO;QAChC,IAAI,CAAC;YACD,EAAE,CAAC,KAAK,CAAC,CAAC;QACd,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACX,OAAO,CAAC,IAAI,CAAC,gCAAgC,OAAO,SAAS,EAAE,GAAG,CAAC,CAAC;QACxE,CAAC;IACL,CAAC,CAAC;IACF,CAAC,CAAC,WAAW,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAChC,OAAO,GAAG,EAAE,CAAC,CAAC,CAAC,cAAc,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;AACpD,CAAC;AAED,SAAS,SAAS,CAAC,CAAS;IACxB,IAAI,CAAC;QAAC,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,SAAS,CAAC;IAAC,CAAC;AAC7D,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,EAAmC;IAChE,OAAO,SAAS,CAAiB,aAAa,EAAE,EAAE,CAAC,CAAC;AACxD,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,EAAmC;IACrE,OAAO,SAAS,CAAiB,mBAAmB,EAAE,EAAE,CAAC,CAAC;AAC9D,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,EAAkC;IAC9D,OAAO,SAAS,CAAgB,eAAe,EAAE,EAAE,CAAC,CAAC;AACzD,CAAC;AAED,MAAM,UAAU,+BAA+B,CAAC,EAAyC;IACrF,OAAO,SAAS,CAAuB,gBAAgB,EAAE,EAAE,CAAC,CAAC;AACjE,CAAC"}
@@ -1,9 +1,10 @@
1
1
  import Foundation
2
+ import UIKit
2
3
  import UserNotifications
3
4
  import Lynx
4
5
 
5
- /// Local notifications module.
6
- /// JS usage: NativeModules.Notifications.schedule({ title, body }, { delay }, callback)
6
+ /// Local + remote notifications module.
7
+ /// JS usage: NativeModules.Notifications.<method>(...)
7
8
  class NotificationsModule: NSObject, LynxModule {
8
9
 
9
10
  @objc static var name: String { "Notifications" }
@@ -15,6 +16,11 @@ class NotificationsModule: NSObject, LynxModule {
15
16
  "cancelAll": NSStringFromSelector(#selector(cancelAll(_:))),
16
17
  "requestPermission": NSStringFromSelector(#selector(requestPermission(_:))),
17
18
  "getPermissionStatus": NSStringFromSelector(#selector(getPermissionStatus(_:))),
19
+ "registerForPushNotifications": NSStringFromSelector(#selector(registerForPushNotifications(_:))),
20
+ "unregisterForPushNotifications": NSStringFromSelector(#selector(unregisterForPushNotifications(_:))),
21
+ "setBadgeCount": NSStringFromSelector(#selector(setBadgeCount(_:callback:))),
22
+ "getBadgeCount": NSStringFromSelector(#selector(getBadgeCount(_:))),
23
+ "getInitialNotification": NSStringFromSelector(#selector(getInitialNotification(_:))),
18
24
  ]
19
25
  }
20
26
 
@@ -25,6 +31,7 @@ class NotificationsModule: NSObject, LynxModule {
25
31
  let title = content?["title"] as? String ?? "Notification"
26
32
  let body = content?["body"] as? String ?? ""
27
33
  let delay = options?["delay"] as? TimeInterval ?? 0
34
+ let data = content?["data"] as? [String: String] ?? [:]
28
35
 
29
36
  let notificationId = UUID().uuidString
30
37
 
@@ -32,6 +39,9 @@ class NotificationsModule: NSObject, LynxModule {
32
39
  notificationContent.title = title
33
40
  notificationContent.body = body
34
41
  notificationContent.sound = .default
42
+ // Round-trip the data dict so a local tap surfaces it on
43
+ // didReceiveResponse → publishResponse — same wire shape as remote.
44
+ notificationContent.userInfo = data
35
45
 
36
46
  let trigger: UNNotificationTrigger
37
47
  if delay > 0 {
@@ -91,4 +101,60 @@ class NotificationsModule: NSObject, LynxModule {
91
101
  callback?(["status": status])
92
102
  }
93
103
  }
104
+
105
+ // MARK: - Remote push
106
+
107
+ /// Ask iOS to contact APNs. The token (or registration failure) arrives
108
+ /// asynchronously via the AppDelegate hook → `PushEventBus` → JS event.
109
+ /// The callback only confirms that `registerForRemoteNotifications` was
110
+ /// dispatched — it does NOT block on token receipt.
111
+ @objc func registerForPushNotifications(_ callback: LynxCallbackBlock?) {
112
+ DispatchQueue.main.async {
113
+ UIApplication.shared.registerForRemoteNotifications()
114
+ callback?(["dispatched": true])
115
+ }
116
+ }
117
+
118
+ @objc func unregisterForPushNotifications(_ callback: LynxCallbackBlock?) {
119
+ DispatchQueue.main.async {
120
+ UIApplication.shared.unregisterForRemoteNotifications()
121
+ callback?(["ok": true])
122
+ }
123
+ }
124
+
125
+ @objc func setBadgeCount(_ count: NSNumber?, callback: LynxCallbackBlock?) {
126
+ let value = count?.intValue ?? 0
127
+ if #available(iOS 16.0, *) {
128
+ UNUserNotificationCenter.current().setBadgeCount(value) { error in
129
+ if let error = error {
130
+ callback?(["error": error.localizedDescription])
131
+ } else {
132
+ callback?(true)
133
+ }
134
+ }
135
+ } else {
136
+ DispatchQueue.main.async {
137
+ UIApplication.shared.applicationIconBadgeNumber = value
138
+ callback?(true)
139
+ }
140
+ }
141
+ }
142
+
143
+ @objc func getBadgeCount(_ callback: LynxCallbackBlock?) {
144
+ DispatchQueue.main.async {
145
+ callback?(NSNumber(value: UIApplication.shared.applicationIconBadgeNumber))
146
+ }
147
+ }
148
+
149
+ /// One-shot: return the payload that launched the app from a cold start,
150
+ /// or nil. Subsequent calls return nil even if the original payload was
151
+ /// non-nil — JS code that wants to react to launch payloads should call
152
+ /// this exactly once during startup.
153
+ @objc func getInitialNotification(_ callback: LynxCallbackBlock?) {
154
+ if let payload = PushEventBus.shared.consumeInitialResponse() {
155
+ callback?(payload)
156
+ } else {
157
+ callback?(NSNull())
158
+ }
159
+ }
94
160
  }