@symbiote-native/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.
Files changed (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +258 -0
  3. package/build/angular/index.d.ts +1 -0
  4. package/build/angular/index.js +1 -0
  5. package/build/core/background-task.d.ts +9 -0
  6. package/build/core/background-task.js +28 -0
  7. package/build/core/badge.d.ts +7 -0
  8. package/build/core/badge.js +23 -0
  9. package/build/core/categories.d.ts +10 -0
  10. package/build/core/categories.js +28 -0
  11. package/build/core/channels.d.ts +20 -0
  12. package/build/core/channels.js +106 -0
  13. package/build/core/emitter.d.ts +24 -0
  14. package/build/core/emitter.js +58 -0
  15. package/build/core/handler.d.ts +15 -0
  16. package/build/core/handler.js +50 -0
  17. package/build/core/index.d.ts +12 -0
  18. package/build/core/index.js +11 -0
  19. package/build/core/native-modules.d.ts +82 -0
  20. package/build/core/native-modules.js +33 -0
  21. package/build/core/permissions.d.ts +8 -0
  22. package/build/core/permissions.js +30 -0
  23. package/build/core/presenter.d.ts +7 -0
  24. package/build/core/presenter.js +27 -0
  25. package/build/core/scheduler.d.ts +13 -0
  26. package/build/core/scheduler.js +193 -0
  27. package/build/core/tokens.d.ts +30 -0
  28. package/build/core/tokens.js +192 -0
  29. package/build/core/types.d.ts +633 -0
  30. package/build/core/types.js +116 -0
  31. package/build/core/utils.d.ts +6 -0
  32. package/build/core/utils.js +53 -0
  33. package/build-ngc/angular/index.d.ts +1 -0
  34. package/build-ngc/angular/index.js +2 -0
  35. package/build-ngc/angular/index.js.map +1 -0
  36. package/build-ngc/core/background-task.d.ts +9 -0
  37. package/build-ngc/core/background-task.js +29 -0
  38. package/build-ngc/core/background-task.js.map +1 -0
  39. package/build-ngc/core/badge.d.ts +7 -0
  40. package/build-ngc/core/badge.js +24 -0
  41. package/build-ngc/core/badge.js.map +1 -0
  42. package/build-ngc/core/categories.d.ts +10 -0
  43. package/build-ngc/core/categories.js +29 -0
  44. package/build-ngc/core/categories.js.map +1 -0
  45. package/build-ngc/core/channels.d.ts +20 -0
  46. package/build-ngc/core/channels.js +107 -0
  47. package/build-ngc/core/channels.js.map +1 -0
  48. package/build-ngc/core/emitter.d.ts +24 -0
  49. package/build-ngc/core/emitter.js +59 -0
  50. package/build-ngc/core/emitter.js.map +1 -0
  51. package/build-ngc/core/handler.d.ts +15 -0
  52. package/build-ngc/core/handler.js +51 -0
  53. package/build-ngc/core/handler.js.map +1 -0
  54. package/build-ngc/core/index.d.ts +12 -0
  55. package/build-ngc/core/index.js +12 -0
  56. package/build-ngc/core/index.js.map +1 -0
  57. package/build-ngc/core/native-modules.d.ts +82 -0
  58. package/build-ngc/core/native-modules.js +34 -0
  59. package/build-ngc/core/native-modules.js.map +1 -0
  60. package/build-ngc/core/permissions.d.ts +8 -0
  61. package/build-ngc/core/permissions.js +31 -0
  62. package/build-ngc/core/permissions.js.map +1 -0
  63. package/build-ngc/core/presenter.d.ts +7 -0
  64. package/build-ngc/core/presenter.js +28 -0
  65. package/build-ngc/core/presenter.js.map +1 -0
  66. package/build-ngc/core/scheduler.d.ts +13 -0
  67. package/build-ngc/core/scheduler.js +194 -0
  68. package/build-ngc/core/scheduler.js.map +1 -0
  69. package/build-ngc/core/tokens.d.ts +30 -0
  70. package/build-ngc/core/tokens.js +193 -0
  71. package/build-ngc/core/tokens.js.map +1 -0
  72. package/build-ngc/core/types.d.ts +633 -0
  73. package/build-ngc/core/types.js +117 -0
  74. package/build-ngc/core/types.js.map +1 -0
  75. package/build-ngc/core/utils.d.ts +6 -0
  76. package/build-ngc/core/utils.js +54 -0
  77. package/build-ngc/core/utils.js.map +1 -0
  78. package/native-link.json +88 -0
  79. package/package.json +150 -0
  80. package/src/angular/index.ts +1 -0
  81. package/src/core/background-task.ts +32 -0
  82. package/src/core/badge.ts +26 -0
  83. package/src/core/categories.ts +60 -0
  84. package/src/core/channels.ts +183 -0
  85. package/src/core/emitter.ts +95 -0
  86. package/src/core/handler.ts +95 -0
  87. package/src/core/index.ts +144 -0
  88. package/src/core/native-modules.ts +209 -0
  89. package/src/core/permissions.ts +46 -0
  90. package/src/core/presenter.ts +45 -0
  91. package/src/core/scheduler.ts +265 -0
  92. package/src/core/tokens.ts +272 -0
  93. package/src/core/types.ts +767 -0
  94. package/src/core/utils.ts +82 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 A. Prokopenko
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,258 @@
1
+ # @symbiote-native/notifications
2
+
3
+ A wrapper package for [SymbioteNative](../../README.md) that makes
4
+ [`expo-notifications`](https://github.com/expo/expo/tree/main/packages/expo-notifications) —
5
+ permissions, device/Expo push tokens, scheduling, presentation, badges, Android channels, iOS/
6
+ Android categories, and the background notification task hook — usable from **every** adapter:
7
+ React, Vue, Svelte, Solid, and Angular. Built the same way as
8
+ [`@symbiote-native/local-auth`](../local-auth): an `expo-modules-core`-based wrapper (see the
9
+ `symbiote-expo-native-module` project skill for the full mechanism — why `expo-modules-core` is
10
+ depended on directly and never the `expo` meta-package, why the upstream JS is hand-ported into
11
+ `core/` rather than imported, and how autolinking picks up the native module).
12
+
13
+ Every export is a plain async function or a module-level listener registration — no hook/
14
+ composable/service to wrap, so the React, Vue, Svelte, Solid, and Angular entry points are plain
15
+ re-exports of the same `core`.
16
+
17
+ ## Install
18
+
19
+ **New app:**
20
+
21
+ ```bash
22
+ npx @symbiote-native/cli new my-app --notifications
23
+ ```
24
+
25
+ **Existing SymbioteNative app:**
26
+
27
+ ```bash
28
+ npx @symbiote-native/cli add --notifications
29
+ ```
30
+
31
+ Either way: installs `@symbiote-native/notifications` and wires the native autolinking
32
+ automatically — see [`@symbiote-native/cli`](../cli). Read [past the wiring
33
+ table](#required-one-time-steps-this-linker-does-not-cover--read-before-shipping-push) before
34
+ shipping push, though — the CLI can't automate app-specific native config like a Firebase
35
+ project.
36
+
37
+ <details>
38
+ <summary>Manual install (no CLI — installing and wiring native autolinking by hand)</summary>
39
+
40
+ ```bash
41
+ npm install @symbiote-native/notifications
42
+ ```
43
+
44
+ `expo-notifications` and `expo-modules-core` come along as regular, pinned dependencies — never
45
+ install either yourself, and never add the `expo` meta-package to your project.
46
+
47
+ ### Required one-time step: native autolinking wiring
48
+
49
+ Same one-time step as every other `expo-modules-core` package this project ships — see
50
+ [`@symbiote-native/local-auth`'s README](../local-auth/README.md#required-one-time-step-native-autolinking-wiring)
51
+ and the `symbiote-expo-native-module` project skill. The per-package half of that table (the
52
+ Gradle dependency and 13 native-module map entries) is generated by
53
+ [`@symbiote-native/expo-modules-link`](../expo-modules-link) from this package's `native-link.json`
54
+ on every install.
55
+
56
+ </details>
57
+
58
+ ### Required one-time steps this linker does NOT cover — read before shipping push
59
+
60
+ Unlike every package this project has wrapped before it, `expo-notifications` needs real,
61
+ app-specific native configuration that a thin JS wrapper cannot supply on its own. Read upstream's
62
+ own config plugin (`plugin/src/withNotifications{Android,IOS}.ts`) before assuming any of this is
63
+ optional — none of it is generated by `@symbiote-native/expo-modules-link`, because none of it fits
64
+ that linker's "fixed value, same for every app" contract (§9 of the `symbiote-expo-native-module`
65
+ skill):
66
+
67
+ | Platform | What you must add yourself | Why |
68
+ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
69
+ | Android | A real Firebase project + `google-services.json`, plus the Google Services Gradle plugin | `android/build.gradle` inside `expo-notifications` depends directly on `com.google.firebase:firebase-messaging` — push delivery is FCM, not a generic APNs-style relay, and this is a real exception to "expo-modules-core only" (see the `symbiote-expo-native-module` skill §10d) |
70
+ | Android | A 96×96 all-white PNG copied into `res/drawable-*dpi/notification_icon.png`, plus two `<meta-data>` entries on `<application>`: `com.google.firebase.messaging.default_notification_icon` and `expo.modules.notifications.default_notification_icon`, both `@drawable/notification_icon` | The status-bar icon Android draws for a notification your app did not explicitly build with a custom small icon |
71
+ | Android | Optionally, `android:color` via `@color/notification_icon_color` and the two matching `default_notification_color` meta-data entries | Tint applied to the small icon above |
72
+ | Android | Optionally, `com.google.firebase.messaging.default_notification_channel_id` meta-data | Which channel an FCM-delivered notification lands in when the payload does not name one |
73
+ | Android | Copy any custom sound file into `res/raw/` | `INotificationContentInput.sound` accepts a custom filename on iOS; Android channels carry their own `sound` field instead — see `setNotificationChannelAsync` |
74
+ | iOS | The **Push Notifications** capability + `aps-environment` entitlement (`development` or `production`) in Xcode | `getDevicePushTokenAsync`/`getExpoPushTokenAsync` need APNs registration, which needs this entitlement present — omitting it is a silent registration failure, not a thrown error |
75
+ | iOS | Add any custom sound file as a bundle resource in Xcode | Same as Android's `res/raw/` step, for `INotificationContentInput.sound` |
76
+ | iOS | Optionally, `UIBackgroundModes` including `remote-notification` in `Info.plist` | Lets a background/silent push wake the app to run a task registered via `registerTaskAsync` |
77
+ | Both | [`@symbiote-native/task-manager`](../task-manager) installed, with a task `defineTask`'d at module scope before `registerTaskAsync` runs | `registerTaskAsync`/`unregisterTaskAsync` here are thin wrappers over expo-notifications' own `BackgroundNotificationTasksModule`, which upstream itself only works when `expo-task-manager` (this project: `@symbiote-native/task-manager`) is linked — see the `symbiote-expo-native-module` skill §10d |
78
+
79
+ None of the above is required just to schedule and present **local** notifications — permissions,
80
+ scheduling, presentation, badges, channels, and categories all work with zero app-level native
81
+ config beyond the linker step. Firebase, the icon/color meta-data, and the entitlement are only
82
+ needed once you send a **remote/push** notification.
83
+
84
+ ## Shape
85
+
86
+ ```
87
+ src/core/ permissions.ts, tokens.ts, presenter.ts, badge.ts, scheduler.ts,
88
+ categories.ts, channels.ts, handler.ts, emitter.ts, background-task.ts,
89
+ native-modules.ts (13 requireNativeModule() resolutions), utils.ts,
90
+ types.ts (I-prefixed, ported from upstream's Notifications.types.ts /
91
+ Tokens.types.ts / NotificationChannel(Group)Manager.types.ts /
92
+ NotificationScheduler.types.ts)
93
+ src/angular/index.ts export * from '../core'
94
+ ```
95
+
96
+ `./react`, `./vue`, `./svelte`, and `./solid` are `exports`-map aliases straight onto `src/core/`.
97
+ `./angular` stays a physical file/subpath since Angular ships through a separate `ngc`/AOT build
98
+ (`build-ngc/`).
99
+
100
+ ## Use it
101
+
102
+ ```ts
103
+ import * as Notifications from '@symbiote-native/notifications';
104
+
105
+ Notifications.setNotificationHandler({
106
+ handleNotification: async () => ({
107
+ shouldShowBanner: true,
108
+ shouldShowList: true,
109
+ shouldPlaySound: false,
110
+ shouldSetBadge: false,
111
+ }),
112
+ });
113
+
114
+ const { granted } = await Notifications.requestPermissionsAsync();
115
+ if (granted) {
116
+ const identifier = await Notifications.scheduleNotificationAsync({
117
+ content: { title: "Time's up!", body: 'Change sides!' },
118
+ trigger: {
119
+ type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL,
120
+ seconds: 60,
121
+ },
122
+ });
123
+
124
+ const subscription = Notifications.addNotificationReceivedListener(
125
+ notification => {
126
+ console.log(notification.request.content.title);
127
+ },
128
+ );
129
+ // later: subscription.remove();
130
+
131
+ await Notifications.cancelScheduledNotificationAsync(identifier);
132
+ }
133
+ ```
134
+
135
+ Identical import surface on every adapter — `@symbiote-native/notifications/react`, `/vue`,
136
+ `/svelte`, `/solid`, `/angular` all re-export the same functions.
137
+
138
+ ## API
139
+
140
+ ```ts
141
+ // permissions
142
+ getPermissionsAsync(): Promise<INotificationPermissionsStatus>
143
+ requestPermissionsAsync(permissions?: INotificationPermissionsRequest): Promise<INotificationPermissionsStatus>
144
+
145
+ // tokens
146
+ addPushTokenListener(listener: IPushTokenListener): EventSubscription
147
+ getDevicePushTokenAsync(): Promise<IDevicePushToken>
148
+ getExpoPushTokenAsync(options?: IExpoPushTokenOptions): Promise<IExpoPushToken>
149
+ setAutoServerRegistrationEnabledAsync(enabled: boolean): Promise<void>
150
+ unregisterForNotificationsAsync(): Promise<void>
151
+ subscribeToTopicAsync(topic: string): Promise<null> // @platform android
152
+ unsubscribeFromTopicAsync(topic: string): Promise<null> // @platform android
153
+
154
+ // presentation
155
+ getPresentedNotificationsAsync(): Promise<INotification[]>
156
+ dismissNotificationAsync(notificationIdentifier: string): Promise<void>
157
+ dismissAllNotificationsAsync(): Promise<void>
158
+
159
+ // badge
160
+ getBadgeCountAsync(): Promise<number>
161
+ setBadgeCountAsync(badgeCount: number): Promise<boolean>
162
+
163
+ // scheduling
164
+ scheduleNotificationAsync(request: INotificationRequestInput): Promise<string>
165
+ getAllScheduledNotificationsAsync(): Promise<INotificationRequest[]>
166
+ cancelScheduledNotificationAsync(identifier: string): Promise<void>
167
+ cancelAllScheduledNotificationsAsync(): Promise<void>
168
+ getNextTriggerDateAsync(trigger: ISchedulableNotificationTriggerInput): Promise<number | null>
169
+
170
+ // categories
171
+ getNotificationCategoriesAsync(): Promise<INotificationCategory[]>
172
+ setNotificationCategoryAsync(identifier: string, actions: INotificationAction[], options?: INotificationCategoryOptions): Promise<INotificationCategory>
173
+ deleteNotificationCategoryAsync(identifier: string): Promise<boolean>
174
+
175
+ // channels — @platform android (no-op returning [] / null / void off Android)
176
+ getNotificationChannelsAsync(): Promise<INotificationChannel[]>
177
+ getNotificationChannelAsync(channelId: string): Promise<INotificationChannel | null>
178
+ setNotificationChannelAsync(channelId: string, channel: INotificationChannelInput): Promise<INotificationChannel | null>
179
+ deleteNotificationChannelAsync(channelId: string): Promise<void>
180
+ getNotificationChannelGroupsAsync(): Promise<INotificationChannelGroup[]>
181
+ getNotificationChannelGroupAsync(groupId: string): Promise<INotificationChannelGroup | null>
182
+ setNotificationChannelGroupAsync(groupId: string, group: INotificationChannelGroupInput): Promise<INotificationChannelGroup | null>
183
+ deleteNotificationChannelGroupAsync(groupId: string): Promise<void>
184
+
185
+ // handler / emitter
186
+ setNotificationHandler(handler: INotificationHandler | null): void
187
+ addNotificationReceivedListener(listener: (event: INotification) => void): EventSubscription
188
+ addNotificationsDroppedListener(listener: () => void): EventSubscription // @platform android
189
+ addNotificationResponseReceivedListener(listener: (event: INotificationResponse) => void): EventSubscription
190
+ getLastNotificationResponse(): INotificationResponse | null
191
+ clearLastNotificationResponse(): void
192
+ addNotificationResponseClearedListener(listener: () => void): EventSubscription
193
+ DEFAULT_ACTION_IDENTIFIER: string
194
+
195
+ // background task (needs @symbiote-native/task-manager, see "Required one-time steps" above)
196
+ registerTaskAsync(taskName: string): Promise<null>
197
+ unregisterTaskAsync(taskName: string): Promise<null>
198
+ ```
199
+
200
+ Plus the enums `IosAlertStyle`, `IosAllowsPreviews`, `IosAuthorizationStatus`,
201
+ `AndroidNotificationVisibility`, `AndroidAudioContentType`, `AndroidImportance`,
202
+ `AndroidAudioUsage`, `AndroidNotificationPriority`, `SchedulableTriggerInputTypes`,
203
+ `BackgroundNotificationTaskResult`, `PermissionStatus` (re-exported from `expo-modules-core`, same
204
+ precedent `@symbiote-native/sensors` uses), and the full `I`-prefixed type surface for content,
205
+ triggers, and native-facing payloads — see `src/core/types.ts`.
206
+
207
+ ## What this port deliberately does not carry over from upstream
208
+
209
+ - **No automatic push-token server-resync daemon.** Upstream's `DevicePushTokenAutoRegistration.fx.ts`
210
+ wires a permanent, module-load background listener that re-POSTs a rolled device token to Expo's
211
+ push backend with exponential retry/backoff (via the `abort-controller` package), whenever
212
+ registration was left enabled. This port ships `setAutoServerRegistrationEnabledAsync` (used
213
+ internally by `getExpoPushTokenAsync`) but not the daemon itself — call `getExpoPushTokenAsync()`
214
+ again from your own `addPushTokenListener` callback for the same effect, without an unconditional
215
+ background retry loop and its extra dependency.
216
+ - **No `expo-constants`/`expo-application` defaults.** Upstream's `getExpoPushTokenAsync` fills in
217
+ `projectId` from `Constants.expoConfig.extra.eas.projectId` and `applicationId` from
218
+ `Application.applicationId` when omitted. Neither package is part of this port (per the
219
+ `symbiote-expo-native-module` skill, this project depends on `expo-modules-core` only) — pass
220
+ both explicitly. [`@symbiote-native/application`](../application)'s own `applicationId` export
221
+ covers the second one.
222
+ - **No `useLastNotificationResponse` React hook.** This package's whole surface is
223
+ adapter-agnostic plain functions by design — read the last response with
224
+ `getLastNotificationResponse()` and subscribe to changes with
225
+ `addNotificationResponseClearedListener`/`addNotificationResponseReceivedListener` directly in
226
+ whichever lifecycle your framework prefers.
227
+ - **The deprecated `*Async` aliases for `getLastNotificationResponse`/`clearLastNotificationResponse`**
228
+ are not ported — call the non-`Async` forms upstream itself now recommends.
229
+ - **`SetBadgeCountOptions.web`** (a `badgin` options bag) is web-only; this project targets
230
+ iOS/Android and `setBadgeCountAsync` takes no options here.
231
+
232
+ ## Notes
233
+
234
+ - **Android channel/channel-group functions are Android-only, like upstream, but collapsed into
235
+ one function each.** Upstream splits every one of these into a `.ts` (iOS no-op) and a
236
+ `.android.ts` (real) file picked by Metro's platform-extension resolution; this port has no
237
+ separate build step per platform to exploit that split for, so each function branches on
238
+ `Platform.OS === 'android'` at runtime instead. Off Android they resolve to the same documented
239
+ no-op values (`[]`/`null`/void) without touching native.
240
+ - **13 native modules autolink from one `expo-notifications` install** —
241
+ `ExpoNotificationPermissionsModule`, `ExpoPushTokenManager`, `ExpoTopicSubscriptionModule`,
242
+ `NotificationsServerRegistrationModule`, `ExpoNotificationPresenter`, `ExpoBadgeModule`,
243
+ `ExpoNotificationScheduler`, `ExpoNotificationCategoriesModule`,
244
+ `ExpoNotificationChannelManager`, `ExpoNotificationChannelGroupManager`,
245
+ `ExpoNotificationsHandlerModule`, `ExpoNotificationsEmitter`,
246
+ `ExpoBackgroundNotificationTasksModule` — see `native-link.json`.
247
+ - **`setNotificationHandler`'s callback must respond within 3 seconds**, matching upstream — a
248
+ slow `handleNotification` implementation causes native to discard the notification and call
249
+ `handleError` with a `NotificationTimeoutError` instead.
250
+
251
+ ## Test it
252
+
253
+ No Fabric/Descriptor angle at all — every function here is a pure async-function surface plus
254
+ event-emitter subscriptions, never a view or per-instance state. Tests inject a fake native-module
255
+ object in place of the real `requireNativeModule` resolution (`vi.mock('./native-modules', …)`)
256
+ and, for the emitter/handler modules, a fake `LegacyEventEmitter` whose listener registry is
257
+ shared across instances so a test can fire an event the same way native would — no `installFabric()`,
258
+ no ViewConfig. See `src/core/*.test.ts`.
@@ -0,0 +1 @@
1
+ export * from '../core';
@@ -0,0 +1 @@
1
+ export * from '../core/index.js';
@@ -0,0 +1,9 @@
1
+ export { BackgroundNotificationTaskResult } from './types';
2
+ /**
3
+ * Starts delivering notification-received/response events (and, when the app is terminated,
4
+ * headless background notifications) into a task already defined with
5
+ * `TaskManager.defineTask(taskName, …)`.
6
+ */
7
+ export declare function registerTaskAsync(taskName: string): Promise<null>;
8
+ /** Stops delivering into a task registered with `registerTaskAsync`. */
9
+ export declare function unregisterTaskAsync(taskName: string): Promise<null>;
@@ -0,0 +1,28 @@
1
+ // Ported from expo-notifications @ sdk-57's registerTaskAsync.ts / unregisterTaskAsync.ts.
2
+ //
3
+ // Runs on `@symbiote-native/task-manager` under the hood, exactly like upstream runs on
4
+ // `expo-task-manager` — `TaskManager.defineTask(taskName, executor)` must be called first, at
5
+ // the top of the JS bundle (outside any component), then this function tells native to start
6
+ // delivering into that task. See this package's README for the full wiring, including why the
7
+ // task also needs `@symbiote-native/task-manager` installed.
8
+ import { UnavailabilityError } from 'expo-modules-core';
9
+ import { backgroundNotificationTasksModule } from './native-modules.js';
10
+ export { BackgroundNotificationTaskResult } from './types.js';
11
+ /**
12
+ * Starts delivering notification-received/response events (and, when the app is terminated,
13
+ * headless background notifications) into a task already defined with
14
+ * `TaskManager.defineTask(taskName, …)`.
15
+ */
16
+ export async function registerTaskAsync(taskName) {
17
+ if (!backgroundNotificationTasksModule.registerTaskAsync) {
18
+ throw new UnavailabilityError('Notifications', 'registerTaskAsync');
19
+ }
20
+ return backgroundNotificationTasksModule.registerTaskAsync(taskName);
21
+ }
22
+ /** Stops delivering into a task registered with `registerTaskAsync`. */
23
+ export async function unregisterTaskAsync(taskName) {
24
+ if (!backgroundNotificationTasksModule.unregisterTaskAsync) {
25
+ throw new UnavailabilityError('Notifications', 'unregisterTaskAsync');
26
+ }
27
+ return backgroundNotificationTasksModule.unregisterTaskAsync(taskName);
28
+ }
@@ -0,0 +1,7 @@
1
+ /** Current app-icon badge count. `0` if unset — not every Android launcher supports badges. */
2
+ export declare function getBadgeCountAsync(): Promise<number>;
3
+ /**
4
+ * Sets the app-icon badge. `0` clears it. On iOS this needs the `allowBadge` permission
5
+ * (`requestPermissionsAsync`) or it resolves to `false`.
6
+ */
7
+ export declare function setBadgeCountAsync(badgeCount: number): Promise<boolean>;
@@ -0,0 +1,23 @@
1
+ // Ported from expo-notifications @ sdk-57's getBadgeCountAsync.ts / setBadgeCountAsync.ts.
2
+ //
3
+ // ponytail: upstream's `SetBadgeCountOptions.web` (a `badgin` options bag) is web-only and this
4
+ // project targets iOS/Android — not ported.
5
+ import { UnavailabilityError } from 'expo-modules-core';
6
+ import { badgeModule } from './native-modules.js';
7
+ /** Current app-icon badge count. `0` if unset — not every Android launcher supports badges. */
8
+ export async function getBadgeCountAsync() {
9
+ if (!badgeModule.getBadgeCountAsync) {
10
+ throw new UnavailabilityError('Notifications', 'getBadgeCountAsync');
11
+ }
12
+ return badgeModule.getBadgeCountAsync();
13
+ }
14
+ /**
15
+ * Sets the app-icon badge. `0` clears it. On iOS this needs the `allowBadge` permission
16
+ * (`requestPermissionsAsync`) or it resolves to `false`.
17
+ */
18
+ export async function setBadgeCountAsync(badgeCount) {
19
+ if (!badgeModule.setBadgeCountAsync) {
20
+ throw new UnavailabilityError('Notifications', 'setBadgeCountAsync');
21
+ }
22
+ return badgeModule.setBadgeCountAsync(badgeCount);
23
+ }
@@ -0,0 +1,10 @@
1
+ import type { INotificationAction, INotificationCategory, INotificationCategoryOptions } from './types';
2
+ /** Every registered notification category. */
3
+ export declare function getNotificationCategoriesAsync(): Promise<INotificationCategory[]>;
4
+ /**
5
+ * Registers a category of action buttons under `identifier`, referenced later via
6
+ * `INotificationContentInput.categoryIdentifier`. Avoid `:`/`-` in `identifier`.
7
+ */
8
+ export declare function setNotificationCategoryAsync(identifier: string, actions: INotificationAction[], options?: INotificationCategoryOptions): Promise<INotificationCategory>;
9
+ /** Deletes a category. Resolves `false` if it did not exist. */
10
+ export declare function deleteNotificationCategoryAsync(identifier: string): Promise<boolean>;
@@ -0,0 +1,28 @@
1
+ // Ported from expo-notifications @ sdk-57's getNotificationCategoriesAsync.ts,
2
+ // setNotificationCategoryAsync.ts, deleteNotificationCategoryAsync.ts.
3
+ import { UnavailabilityError } from 'expo-modules-core';
4
+ import { notificationCategoriesModule } from './native-modules.js';
5
+ /** Every registered notification category. */
6
+ export async function getNotificationCategoriesAsync() {
7
+ if (!notificationCategoriesModule.getNotificationCategoriesAsync) {
8
+ throw new UnavailabilityError('Notifications', 'getNotificationCategoriesAsync');
9
+ }
10
+ return notificationCategoriesModule.getNotificationCategoriesAsync();
11
+ }
12
+ /**
13
+ * Registers a category of action buttons under `identifier`, referenced later via
14
+ * `INotificationContentInput.categoryIdentifier`. Avoid `:`/`-` in `identifier`.
15
+ */
16
+ export async function setNotificationCategoryAsync(identifier, actions, options) {
17
+ if (!notificationCategoriesModule.setNotificationCategoryAsync) {
18
+ throw new UnavailabilityError('Notifications', 'setNotificationCategoryAsync');
19
+ }
20
+ return notificationCategoriesModule.setNotificationCategoryAsync(identifier, actions, options);
21
+ }
22
+ /** Deletes a category. Resolves `false` if it did not exist. */
23
+ export async function deleteNotificationCategoryAsync(identifier) {
24
+ if (!notificationCategoriesModule.deleteNotificationCategoryAsync) {
25
+ throw new UnavailabilityError('Notifications', 'deleteNotificationCategoryAsync');
26
+ }
27
+ return notificationCategoriesModule.deleteNotificationCategoryAsync(identifier);
28
+ }
@@ -0,0 +1,20 @@
1
+ import { type INotificationChannel, type INotificationChannelGroup, type INotificationChannelGroupInput, type INotificationChannelInput } from './types';
2
+ /** @platform android — empty array on every other platform. */
3
+ export declare function getNotificationChannelsAsync(): Promise<INotificationChannel[]>;
4
+ /** @platform android — `null` on every other platform. */
5
+ export declare function getNotificationChannelAsync(channelId: string): Promise<INotificationChannel | null>;
6
+ /**
7
+ * Creates or updates a channel. Only the name and description may change once a channel
8
+ * exists — an Android OS limitation. @platform android — `null` on every other platform.
9
+ */
10
+ export declare function setNotificationChannelAsync(channelId: string, channel: INotificationChannelInput): Promise<INotificationChannel | null>;
11
+ /** @platform android — no-op on every other platform. */
12
+ export declare function deleteNotificationChannelAsync(channelId: string): Promise<void>;
13
+ /** @platform android — empty array on every other platform. */
14
+ export declare function getNotificationChannelGroupsAsync(): Promise<INotificationChannelGroup[]>;
15
+ /** @platform android — `null` on every other platform. */
16
+ export declare function getNotificationChannelGroupAsync(groupId: string): Promise<INotificationChannelGroup | null>;
17
+ /** @platform android — `null` on every other platform. */
18
+ export declare function setNotificationChannelGroupAsync(groupId: string, group: INotificationChannelGroupInput): Promise<INotificationChannelGroup | null>;
19
+ /** @platform android — no-op on every other platform. */
20
+ export declare function deleteNotificationChannelGroupAsync(groupId: string): Promise<void>;
@@ -0,0 +1,106 @@
1
+ // Ported from expo-notifications @ sdk-57's get/set/deleteNotificationChannel(Group)Async.ts —
2
+ // upstream splits each of these into a `.ts` (iOS no-op) and a `.android.ts` (real) file picked
3
+ // by Metro's platform extension resolution; this port collapses each pair into one function with
4
+ // a runtime `Platform.OS === 'android'` branch, since this package has no separate build step
5
+ // per platform to exploit the file-split for.
6
+ import { Platform, UnavailabilityError } from 'expo-modules-core';
7
+ import { notificationChannelGroupManager, notificationChannelManager, } from './native-modules.js';
8
+ import { AndroidImportance, } from './types.js';
9
+ const ANDROID_ONLY_NOTICE = 'Notification channels are only supported on Android.';
10
+ function isAndroid() {
11
+ return Platform.OS === 'android';
12
+ }
13
+ /** @platform android — empty array on every other platform. */
14
+ export async function getNotificationChannelsAsync() {
15
+ if (!isAndroid()) {
16
+ console.debug(ANDROID_ONLY_NOTICE);
17
+ return [];
18
+ }
19
+ if (!notificationChannelManager.getNotificationChannelsAsync) {
20
+ throw new UnavailabilityError('Notifications', 'getNotificationChannelsAsync');
21
+ }
22
+ return ((await notificationChannelManager.getNotificationChannelsAsync()) ?? []);
23
+ }
24
+ /** @platform android — `null` on every other platform. */
25
+ export async function getNotificationChannelAsync(channelId) {
26
+ if (!isAndroid()) {
27
+ console.debug(ANDROID_ONLY_NOTICE);
28
+ return null;
29
+ }
30
+ if (!notificationChannelManager.getNotificationChannelAsync) {
31
+ throw new UnavailabilityError('Notifications', 'getNotificationChannelAsync');
32
+ }
33
+ return notificationChannelManager.getNotificationChannelAsync(channelId);
34
+ }
35
+ /**
36
+ * Creates or updates a channel. Only the name and description may change once a channel
37
+ * exists — an Android OS limitation. @platform android — `null` on every other platform.
38
+ */
39
+ export async function setNotificationChannelAsync(channelId, channel) {
40
+ if (!isAndroid()) {
41
+ console.debug(ANDROID_ONLY_NOTICE);
42
+ return null;
43
+ }
44
+ if (!notificationChannelManager.setNotificationChannelAsync) {
45
+ throw new UnavailabilityError('Notifications', 'setNotificationChannelAsync');
46
+ }
47
+ if (channel.importance === AndroidImportance.UNSPECIFIED) {
48
+ console.warn(`[notifications] Channel "${channelId}" importance is UNSPECIFIED, which can error on some Android versions — use AndroidImportance.DEFAULT instead.`);
49
+ }
50
+ return notificationChannelManager.setNotificationChannelAsync(channelId, channel);
51
+ }
52
+ /** @platform android — no-op on every other platform. */
53
+ export async function deleteNotificationChannelAsync(channelId) {
54
+ if (!isAndroid()) {
55
+ console.debug(ANDROID_ONLY_NOTICE);
56
+ return;
57
+ }
58
+ if (!notificationChannelManager.deleteNotificationChannelAsync) {
59
+ throw new UnavailabilityError('Notifications', 'deleteNotificationChannelAsync');
60
+ }
61
+ return notificationChannelManager.deleteNotificationChannelAsync(channelId);
62
+ }
63
+ /** @platform android — empty array on every other platform. */
64
+ export async function getNotificationChannelGroupsAsync() {
65
+ if (!isAndroid()) {
66
+ console.debug(ANDROID_ONLY_NOTICE);
67
+ return [];
68
+ }
69
+ if (!notificationChannelGroupManager.getNotificationChannelGroupsAsync) {
70
+ throw new UnavailabilityError('Notifications', 'getNotificationChannelGroupsAsync');
71
+ }
72
+ return notificationChannelGroupManager.getNotificationChannelGroupsAsync();
73
+ }
74
+ /** @platform android — `null` on every other platform. */
75
+ export async function getNotificationChannelGroupAsync(groupId) {
76
+ if (!isAndroid()) {
77
+ console.debug(ANDROID_ONLY_NOTICE);
78
+ return null;
79
+ }
80
+ if (!notificationChannelGroupManager.getNotificationChannelGroupAsync) {
81
+ throw new UnavailabilityError('Notifications', 'getNotificationChannelGroupAsync');
82
+ }
83
+ return notificationChannelGroupManager.getNotificationChannelGroupAsync(groupId);
84
+ }
85
+ /** @platform android — `null` on every other platform. */
86
+ export async function setNotificationChannelGroupAsync(groupId, group) {
87
+ if (!isAndroid()) {
88
+ console.debug(ANDROID_ONLY_NOTICE);
89
+ return null;
90
+ }
91
+ if (!notificationChannelGroupManager.setNotificationChannelGroupAsync) {
92
+ throw new UnavailabilityError('Notifications', 'setNotificationChannelGroupAsync');
93
+ }
94
+ return notificationChannelGroupManager.setNotificationChannelGroupAsync(groupId, group);
95
+ }
96
+ /** @platform android — no-op on every other platform. */
97
+ export async function deleteNotificationChannelGroupAsync(groupId) {
98
+ if (!isAndroid()) {
99
+ console.debug(ANDROID_ONLY_NOTICE);
100
+ return;
101
+ }
102
+ if (!notificationChannelGroupManager.deleteNotificationChannelGroupAsync) {
103
+ throw new UnavailabilityError('Notifications', 'deleteNotificationChannelGroupAsync');
104
+ }
105
+ return notificationChannelGroupManager.deleteNotificationChannelGroupAsync(groupId);
106
+ }
@@ -0,0 +1,24 @@
1
+ import { type EventSubscription } from 'expo-modules-core';
2
+ import type { INotification, INotificationResponse } from './types';
3
+ export declare const DEFAULT_ACTION_IDENTIFIER = "expo.modules.notifications.actions.DEFAULT";
4
+ /** Fires whenever a notification is received while the app is running. */
5
+ export declare function addNotificationReceivedListener(listener: (event: INotification) => void): EventSubscription;
6
+ /**
7
+ * Fires whenever the server (Firebase Cloud Messaging) dropped queued notifications.
8
+ * @platform android
9
+ */
10
+ export declare function addNotificationsDroppedListener(listener: () => void): EventSubscription;
11
+ /** Fires whenever the user interacts with a notification (e.g. taps it). */
12
+ export declare function addNotificationResponseReceivedListener(listener: (event: INotificationResponse) => void): EventSubscription;
13
+ /**
14
+ * The most recently received notification response, or `null` if the app was not opened by
15
+ * one.
16
+ */
17
+ export declare function getLastNotificationResponse(): INotificationResponse | null;
18
+ /**
19
+ * Clears the last notification response, e.g. once its route has been handled and should not
20
+ * be re-applied on the next read.
21
+ */
22
+ export declare function clearLastNotificationResponse(): void;
23
+ /** Fires whenever `clearLastNotificationResponse` runs. */
24
+ export declare function addNotificationResponseClearedListener(listener: () => void): EventSubscription;
@@ -0,0 +1,58 @@
1
+ // Ported from expo-notifications @ sdk-57's NotificationsEmitter.ts.
2
+ import { LegacyEventEmitter, UnavailabilityError, } from 'expo-modules-core';
3
+ import { dlog } from '@symbiote-native/engine';
4
+ import { notificationsEmitterModule } from './native-modules.js';
5
+ import { mapNotification, mapNotificationResponse } from './utils.js';
6
+ const emitter = new LegacyEventEmitter(notificationsEmitterModule);
7
+ const RECEIVED_EVENT = 'onDidReceiveNotification';
8
+ const DROPPED_EVENT = 'onNotificationsDeleted';
9
+ const RESPONSE_RECEIVED_EVENT = 'onDidReceiveNotificationResponse';
10
+ const RESPONSE_CLEARED_EVENT = 'onDidClearNotificationResponse';
11
+ export const DEFAULT_ACTION_IDENTIFIER = 'expo.modules.notifications.actions.DEFAULT';
12
+ /** Fires whenever a notification is received while the app is running. */
13
+ export function addNotificationReceivedListener(listener) {
14
+ return emitter.addListener(RECEIVED_EVENT, notification => {
15
+ dlog(() => `[notifications] received ${notification.request.identifier}`);
16
+ listener(mapNotification(notification));
17
+ });
18
+ }
19
+ /**
20
+ * Fires whenever the server (Firebase Cloud Messaging) dropped queued notifications.
21
+ * @platform android
22
+ */
23
+ export function addNotificationsDroppedListener(listener) {
24
+ return emitter.addListener(DROPPED_EVENT, listener);
25
+ }
26
+ /** Fires whenever the user interacts with a notification (e.g. taps it). */
27
+ export function addNotificationResponseReceivedListener(listener) {
28
+ return emitter.addListener(RESPONSE_RECEIVED_EVENT, response => {
29
+ dlog(() => `[notifications] response ${response.actionIdentifier} for ${response.notification.request.identifier}`);
30
+ listener(mapNotificationResponse(response));
31
+ });
32
+ }
33
+ /**
34
+ * The most recently received notification response, or `null` if the app was not opened by
35
+ * one.
36
+ */
37
+ export function getLastNotificationResponse() {
38
+ if (!notificationsEmitterModule.getLastNotificationResponse) {
39
+ throw new UnavailabilityError('Notifications', 'getLastNotificationResponse');
40
+ }
41
+ const response = notificationsEmitterModule.getLastNotificationResponse();
42
+ return response ? mapNotificationResponse(response) : response;
43
+ }
44
+ /**
45
+ * Clears the last notification response, e.g. once its route has been handled and should not
46
+ * be re-applied on the next read.
47
+ */
48
+ export function clearLastNotificationResponse() {
49
+ if (!notificationsEmitterModule.clearLastNotificationResponse) {
50
+ throw new UnavailabilityError('Notifications', 'clearLastNotificationResponse');
51
+ }
52
+ notificationsEmitterModule.clearLastNotificationResponse();
53
+ emitter.emit(RESPONSE_CLEARED_EVENT, []);
54
+ }
55
+ /** Fires whenever `clearLastNotificationResponse` runs. */
56
+ export function addNotificationResponseClearedListener(listener) {
57
+ return emitter.addListener(RESPONSE_CLEARED_EVENT, listener);
58
+ }
@@ -0,0 +1,15 @@
1
+ import { CodedError } from 'expo-modules-core';
2
+ import type { INotification, INotificationHandler } from './types';
3
+ export declare class NotificationTimeoutError extends CodedError {
4
+ info: {
5
+ notification: INotification;
6
+ id: string;
7
+ };
8
+ constructor(notificationId: string, notification: INotification);
9
+ }
10
+ /**
11
+ * Sets the callback deciding whether/how a notification received while the app is running
12
+ * should be shown. Must respond within 3 seconds or the notification is discarded. Passing
13
+ * `null` clears the handler — the default (no handler) is to not show anything.
14
+ */
15
+ export declare function setNotificationHandler(handler: INotificationHandler | null): void;