@symbiote-native/notifications 0.1.2 → 0.1.3

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 (100) hide show
  1. package/README.md +94 -58
  2. package/build/angular/index.d.ts +1 -0
  3. package/build/angular/index.js +1 -0
  4. package/build/angular/services/last-notification-response.service/index.d.ts +8 -0
  5. package/build/angular/services/last-notification-response.service/index.js +78 -0
  6. package/build/core/auto-registration.d.ts +5 -0
  7. package/build/core/auto-registration.js +65 -0
  8. package/build/core/backoff.d.ts +6 -0
  9. package/build/core/backoff.js +15 -0
  10. package/build/core/device-token.d.ts +7 -0
  11. package/build/core/device-token.js +35 -0
  12. package/build/core/emitter.d.ts +4 -0
  13. package/build/core/emitter.js +8 -0
  14. package/build/core/index.d.ts +3 -1
  15. package/build/core/index.js +3 -1
  16. package/build/core/last-notification-response.d.ts +2 -0
  17. package/build/core/last-notification-response.js +12 -0
  18. package/build/core/registration-abort.d.ts +2 -0
  19. package/build/core/registration-abort.js +10 -0
  20. package/build/core/token-metadata.d.ts +4 -0
  21. package/build/core/token-metadata.js +34 -0
  22. package/build/core/tokens.d.ts +4 -22
  23. package/build/core/tokens.js +41 -99
  24. package/build/core/update-device-push-token.d.ts +3 -0
  25. package/build/core/update-device-push-token.js +122 -0
  26. package/build/react/hooks/use-last-notification-response/index.d.ts +2 -0
  27. package/build/react/hooks/use-last-notification-response/index.js +22 -0
  28. package/build/react/index.d.ts +2 -0
  29. package/build/react/index.js +2 -0
  30. package/build/solid/index.d.ts +2 -0
  31. package/build/solid/index.js +2 -0
  32. package/build/solid/primitives/create-last-notification-response.d.ts +3 -0
  33. package/build/solid/primitives/create-last-notification-response.js +20 -0
  34. package/build/svelte/index.d.ts +2 -0
  35. package/build/svelte/index.js +2 -0
  36. package/build/svelte/use-last-notification-response.svelte.d.ts +4 -0
  37. package/build/svelte/use-last-notification-response.svelte.js +25 -0
  38. package/build/vue/composables/use-last-notification-response/index.d.ts +3 -0
  39. package/build/vue/composables/use-last-notification-response/index.js +20 -0
  40. package/build/vue/index.d.ts +2 -0
  41. package/build/vue/index.js +2 -0
  42. package/build-ngc/angular/index.d.ts +1 -0
  43. package/build-ngc/angular/index.js +1 -0
  44. package/build-ngc/angular/index.js.map +1 -1
  45. package/build-ngc/angular/services/last-notification-response.service/index.d.ts +11 -0
  46. package/build-ngc/angular/services/last-notification-response.service/index.js +37 -0
  47. package/build-ngc/angular/services/last-notification-response.service/index.js.map +1 -0
  48. package/build-ngc/core/auto-registration.d.ts +5 -0
  49. package/build-ngc/core/auto-registration.js +66 -0
  50. package/build-ngc/core/auto-registration.js.map +1 -0
  51. package/build-ngc/core/backoff.d.ts +6 -0
  52. package/build-ngc/core/backoff.js +16 -0
  53. package/build-ngc/core/backoff.js.map +1 -0
  54. package/build-ngc/core/device-token.d.ts +7 -0
  55. package/build-ngc/core/device-token.js +36 -0
  56. package/build-ngc/core/device-token.js.map +1 -0
  57. package/build-ngc/core/emitter.d.ts +4 -0
  58. package/build-ngc/core/emitter.js +8 -0
  59. package/build-ngc/core/emitter.js.map +1 -1
  60. package/build-ngc/core/index.d.ts +3 -1
  61. package/build-ngc/core/index.js +3 -1
  62. package/build-ngc/core/index.js.map +1 -1
  63. package/build-ngc/core/last-notification-response.d.ts +2 -0
  64. package/build-ngc/core/last-notification-response.js +13 -0
  65. package/build-ngc/core/last-notification-response.js.map +1 -0
  66. package/build-ngc/core/registration-abort.d.ts +2 -0
  67. package/build-ngc/core/registration-abort.js +11 -0
  68. package/build-ngc/core/registration-abort.js.map +1 -0
  69. package/build-ngc/core/token-metadata.d.ts +4 -0
  70. package/build-ngc/core/token-metadata.js +35 -0
  71. package/build-ngc/core/token-metadata.js.map +1 -0
  72. package/build-ngc/core/tokens.d.ts +4 -22
  73. package/build-ngc/core/tokens.js +41 -99
  74. package/build-ngc/core/tokens.js.map +1 -1
  75. package/build-ngc/core/update-device-push-token.d.ts +3 -0
  76. package/build-ngc/core/update-device-push-token.js +123 -0
  77. package/build-ngc/core/update-device-push-token.js.map +1 -0
  78. package/package.json +24 -23
  79. package/src/angular/index.ts +1 -0
  80. package/src/angular/services/last-notification-response.service/index.ts +54 -0
  81. package/src/core/auto-registration.ts +105 -0
  82. package/src/core/backoff.ts +24 -0
  83. package/src/core/device-token.ts +54 -0
  84. package/src/core/emitter.ts +10 -0
  85. package/src/core/index.ts +8 -0
  86. package/src/core/last-notification-response.ts +19 -0
  87. package/src/core/registration-abort.ts +12 -0
  88. package/src/core/token-metadata.ts +42 -0
  89. package/src/core/tokens.ts +71 -133
  90. package/src/core/update-device-push-token.ts +173 -0
  91. package/src/react/hooks/use-last-notification-response/index.ts +35 -0
  92. package/src/react/index.ts +2 -0
  93. package/src/solid/index.ts +2 -0
  94. package/src/solid/primitives/create-last-notification-response.ts +35 -0
  95. package/src/svelte/index.ts +2 -0
  96. package/src/svelte/svelte-ambient.d.ts +3 -0
  97. package/src/svelte/svelte-compile.test-helper.ts +7 -0
  98. package/src/svelte/use-last-notification-response.svelte.ts +45 -0
  99. package/src/vue/composables/use-last-notification-response/index.ts +35 -0
  100. package/src/vue/index.ts +2 -0
package/README.md CHANGED
@@ -1,18 +1,18 @@
1
1
  # @symbiote-native/notifications
2
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
3
+ Remind the user at the right moment, reach them with push when the app is closed, and know which
4
+ notification they tapped. One API for every [SymbioteNative](../../README.md) adapter (React, Vue,
5
+ Svelte, Solid and Angular).
6
+
7
+ It wraps [`expo-notifications`](https://github.com/expo/expo/tree/main/packages/expo-notifications):
8
+ permissions, device and Expo push tokens, scheduling, presentation, badges, Android channels, iOS
9
+ and Android categories, and the background notification task hook. Built the same way as
8
10
  [`@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).
11
+ `symbiote-expo-native-module` project skill for the full mechanism).
12
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`.
13
+ Every core export is a plain async function or a module-level listener registration. One
14
+ adapter-specific hook - `useLastNotificationResponse` - is ported to all five adapters (originally
15
+ dropped as React-only; since reversed, see "Push-token resync and defaults" below).
16
16
 
17
17
  ## Install
18
18
 
@@ -29,24 +29,24 @@ npx @symbiote-native/cli add --notifications
29
29
  ```
30
30
 
31
31
  Either way: installs `@symbiote-native/notifications` and wires the native autolinking
32
- automatically — see [`@symbiote-native/cli`](../cli). Read [past the wiring
32
+ automatically - see [`@symbiote-native/cli`](../cli). Read [past the wiring
33
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
34
+ shipping push, though - the CLI can't automate app-specific native config like a Firebase
35
35
  project.
36
36
 
37
37
  <details>
38
- <summary>Manual install (no CLI — installing and wiring native autolinking by hand)</summary>
38
+ <summary>Manual install (no CLI - installing and wiring native autolinking by hand)</summary>
39
39
 
40
40
  ```bash
41
41
  npm install @symbiote-native/notifications
42
42
  ```
43
43
 
44
- `expo-notifications` and `expo-modules-core` come along as regular, pinned dependencies — never
44
+ `expo-notifications` and `expo-modules-core` come along as regular, pinned dependencies - never
45
45
  install either yourself, and never add the `expo` meta-package to your project.
46
46
 
47
47
  ### Required one-time step: native autolinking wiring
48
48
 
49
- Same one-time step as every other `expo-modules-core` package this project ships — see
49
+ Same one-time step as every other `expo-modules-core` package this project ships - see
50
50
  [`@symbiote-native/local-auth`'s README](../local-auth/README.md#required-one-time-step-native-autolinking-wiring)
51
51
  and the `symbiote-expo-native-module` project skill. The per-package half of that table (the
52
52
  Gradle dependency and 13 native-module map entries) is generated by
@@ -55,28 +55,28 @@ on every install.
55
55
 
56
56
  </details>
57
57
 
58
- ### Required one-time steps this linker does NOT cover — read before shipping push
58
+ ### Required one-time steps this linker does NOT cover - read before shipping push
59
59
 
60
60
  Unlike every package this project has wrapped before it, `expo-notifications` needs real,
61
61
  app-specific native configuration that a thin JS wrapper cannot supply on its own. Read upstream's
62
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
63
+ optional - none of it is generated by `@symbiote-native/expo-modules-link`, because none of it fits
64
64
  that linker's "fixed value, same for every app" contract (§9 of the `symbiote-expo-native-module`
65
65
  skill):
66
66
 
67
67
  | Platform | What you must add yourself | Why |
68
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) |
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
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
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
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 |
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
75
  | iOS | Add any custom sound file as a bundle resource in Xcode | Same as Android's `res/raw/` step, for `INotificationContentInput.sound` |
76
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 |
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
78
 
79
- None of the above is required just to schedule and present **local** notifications — permissions,
79
+ None of the above is required just to schedule and present **local** notifications - permissions,
80
80
  scheduling, presentation, badges, channels, and categories all work with zero app-level native
81
81
  config beyond the linker step. Firebase, the icon/color meta-data, and the entitlement are only
82
82
  needed once you send a **remote/push** notification.
@@ -90,12 +90,19 @@ src/core/ permissions.ts, tokens.ts, presenter.ts, badge.ts, sch
90
90
  types.ts (I-prefixed, ported from upstream's Notifications.types.ts /
91
91
  Tokens.types.ts / NotificationChannel(Group)Manager.types.ts /
92
92
  NotificationScheduler.types.ts)
93
- src/angular/index.ts export * from '../core'
93
+ src/core/last-notification-response.ts determineNextResponse - the dedup rule every adapter's
94
+ useLastNotificationResponse shares
95
+ src/react/hooks/use-last-notification-response/
96
+ src/vue/composables/use-last-notification-response/
97
+ src/solid/primitives/create-last-notification-response.ts
98
+ src/svelte/use-last-notification-response.svelte.ts
99
+ src/angular/services/last-notification-response.service/ LastNotificationResponseService,
100
+ connect(): Signal<IMaybeNotificationResponse>
94
101
  ```
95
102
 
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/`).
103
+ Every adapter barrel (`src/{react,vue,solid,svelte}/index.ts`, `src/angular/index.ts`) re-exports
104
+ `../core` plus its own `useLastNotificationResponse`/`createLastNotificationResponse`/
105
+ `LastNotificationResponseService`.
99
106
 
100
107
  ## Use it
101
108
 
@@ -132,7 +139,7 @@ if (granted) {
132
139
  }
133
140
  ```
134
141
 
135
- Identical import surface on every adapter — `@symbiote-native/notifications/react`, `/vue`,
142
+ Identical import surface on every adapter - `@symbiote-native/notifications/react`, `/vue`,
136
143
  `/svelte`, `/solid`, `/angular` all re-export the same functions.
137
144
 
138
145
  ## API
@@ -172,7 +179,7 @@ getNotificationCategoriesAsync(): Promise<INotificationCategory[]>
172
179
  setNotificationCategoryAsync(identifier: string, actions: INotificationAction[], options?: INotificationCategoryOptions): Promise<INotificationCategory>
173
180
  deleteNotificationCategoryAsync(identifier: string): Promise<boolean>
174
181
 
175
- // channels — @platform android (no-op returning [] / null / void off Android)
182
+ // channels - @platform android (no-op returning [] / null / void off Android)
176
183
  getNotificationChannelsAsync(): Promise<INotificationChannel[]>
177
184
  getNotificationChannelAsync(channelId: string): Promise<INotificationChannel | null>
178
185
  setNotificationChannelAsync(channelId: string, channel: INotificationChannelInput): Promise<INotificationChannel | null>
@@ -197,35 +204,43 @@ registerTaskAsync(taskName: string): Promise<null>
197
204
  unregisterTaskAsync(taskName: string): Promise<null>
198
205
  ```
199
206
 
207
+ Per-adapter: `useLastNotificationResponse()` (React/Vue/Solid/Svelte) /
208
+ `inject(LastNotificationResponseService).connect()` (Angular) - the last tapped/received response,
209
+ deduped by notification identifier, kept in sync with `addNotificationResponseReceivedListener` and
210
+ `addNotificationResponseClearedListener`.
211
+
200
212
  Plus the enums `IosAlertStyle`, `IosAllowsPreviews`, `IosAuthorizationStatus`,
201
213
  `AndroidNotificationVisibility`, `AndroidAudioContentType`, `AndroidImportance`,
202
214
  `AndroidAudioUsage`, `AndroidNotificationPriority`, `SchedulableTriggerInputTypes`,
203
215
  `BackgroundNotificationTaskResult`, `PermissionStatus` (re-exported from `expo-modules-core`, same
204
216
  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.
217
+ triggers, and native-facing payloads - see `src/core/types.ts`.
218
+
219
+ ## Push-token resync and defaults
220
+
221
+ - **Token resync is ported, and explicit.** Upstream's `DevicePushTokenAutoRegistration.fx.ts`
222
+ re-POSTs a rolled device token to Expo's push backend with exponential backoff whenever
223
+ registration was left enabled. Here it is `installPushTokenAutoRegistration()` (`core/auto-registration.ts`,
224
+ with the upstream `backoff`/`updateDevicePushTokenAsync` helpers and their tests): idempotent, and
225
+ installed by `setAutoServerRegistrationEnabledAsync(true)` (so also by `getExpoPushTokenAsync`). Call it
226
+ once at app startup as well, so a token that rolled while the app was closed is re-sent too. It is
227
+ explicit rather than a module-load side effect because Metro's `inlineRequires` skips a barrel
228
+ re-export that nothing names as a value.
229
+ - **`applicationId` and `development` default like upstream**, from
230
+ [`@symbiote-native/application`](../application) (its `applicationId` and the iOS push-notification
231
+ service environment). `projectId` still has to be passed: upstream reads it from
232
+ `expo-constants`, and `@symbiote-native/constants` does not exist yet.
233
+ - **`useLastNotificationResponse` is ported to every adapter** (React, Vue, Solid, Svelte,
234
+ Angular) - each wraps `getLastNotificationResponse()` +
235
+ `addNotificationResponseClearedListener`/`addNotificationResponseReceivedListener` with the
236
+ shared `determineNextResponse` dedup rule from `core/last-notification-response.ts` behind its
237
+ own lifecycle (hook/composable/primitive/rune/service). Angular's `LastNotificationResponseService`
238
+ matches `PreventScreenCaptureService`'s `connect()` shape.
239
+ - **The deprecated `getLastNotificationResponseAsync`/`clearLastNotificationResponseAsync`** are ported
240
+ too, as thin wrappers over the sync forms, exactly like upstream.
241
+
242
+ ## Scope exclusions
243
+
229
244
  - **`SetBadgeCountOptions.web`** (a `badgin` options bag) is web-only; this project targets
230
245
  iOS/Android and `setBadgeCountAsync` takes no options here.
231
246
 
@@ -237,22 +252,43 @@ triggers, and native-facing payloads — see `src/core/types.ts`.
237
252
  separate build step per platform to exploit that split for, so each function branches on
238
253
  `Platform.OS === 'android'` at runtime instead. Off Android they resolve to the same documented
239
254
  no-op values (`[]`/`null`/void) without touching native.
240
- - **13 native modules autolink from one `expo-notifications` install** —
255
+ - **13 native modules autolink from one `expo-notifications` install** -
241
256
  `ExpoNotificationPermissionsModule`, `ExpoPushTokenManager`, `ExpoTopicSubscriptionModule`,
242
257
  `NotificationsServerRegistrationModule`, `ExpoNotificationPresenter`, `ExpoBadgeModule`,
243
258
  `ExpoNotificationScheduler`, `ExpoNotificationCategoriesModule`,
244
259
  `ExpoNotificationChannelManager`, `ExpoNotificationChannelGroupManager`,
245
260
  `ExpoNotificationsHandlerModule`, `ExpoNotificationsEmitter`,
246
- `ExpoBackgroundNotificationTasksModule` — see `native-link.json`.
247
- - **`setNotificationHandler`'s callback must respond within 3 seconds**, matching upstream — a
261
+ `ExpoBackgroundNotificationTasksModule` - see `native-link.json`.
262
+ - **Push does not work on emulators or simulators.** Test remote push on a physical device; local
263
+ notifications work everywhere.
264
+ - **`setNotificationHandler`'s callback must respond within 3 seconds**, matching upstream - a
248
265
  slow `handleNotification` implementation causes native to discard the notification and call
249
266
  `handleError` with a `NotificationTimeoutError` instead.
250
267
 
268
+ ## Common questions
269
+
270
+ - **No push token.** Pass `projectId`. On Android create a channel first (`setNotificationChannelAsync`
271
+ before `getExpoPushTokenAsync`; Android 13's prompt needs a channel) and set up FCM credentials and
272
+ `google-services.json`.
273
+ - **Nothing shows while the app is open.** Call `setNotificationHandler` with `shouldShowBanner` and
274
+ `shouldShowList` `true`.
275
+ - **Silent or late on Android.** Create the channel with high importance and send `priority: 'high'`.
276
+ - **Nothing when the app is closed.** Check the token is registered on the server, FCM V1 credentials
277
+ on Android, and test on a physical device.
278
+ - **Which notification was tapped?** `useLastNotificationResponse` or
279
+ `addNotificationResponseReceivedListener`.
280
+ - **Reminders need push?** No: scheduled local notifications need no Firebase or APNs.
281
+
282
+ Sources: [Expo docs: Notifications](https://docs.expo.dev/versions/latest/sdk/notifications/),
283
+ [Expo docs: push notifications setup](https://docs.expo.dev/push-notifications/push-notifications-setup/),
284
+ [expo/expo#49638](https://github.com/expo/expo/issues/49638),
285
+ [expo/expo#9745](https://github.com/expo/expo/issues/9745).
286
+
251
287
  ## Test it
252
288
 
253
- No Fabric/Descriptor angle at all — every function here is a pure async-function surface plus
289
+ No Fabric/Descriptor angle at all - every function here is a pure async-function surface plus
254
290
  event-emitter subscriptions, never a view or per-instance state. Tests inject a fake native-module
255
291
  object in place of the real `requireNativeModule` resolution (`vi.mock('./native-modules', …)`)
256
292
  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()`,
293
+ shared across instances so a test can fire an event the same way native would - no `installFabric()`,
258
294
  no ViewConfig. See `src/core/*.test.ts`.
@@ -1 +1,2 @@
1
1
  export * from '../core';
2
+ export { LastNotificationResponseService } from './services/last-notification-response.service';
@@ -1 +1,2 @@
1
1
  export * from '../core/index.js';
2
+ export { LastNotificationResponseService } from './services/last-notification-response.service/index.js';
@@ -0,0 +1,8 @@
1
+ import { type Signal } from '@angular/core';
2
+ import type { IMaybeNotificationResponse } from '../../../core';
3
+ export declare class LastNotificationResponseService {
4
+ private readonly injector;
5
+ private readonly lastResponse;
6
+ private isConnected;
7
+ connect(): Signal<IMaybeNotificationResponse>;
8
+ }
@@ -0,0 +1,78 @@
1
+ // Angular twin of `../../../react`'s `useLastNotificationResponse`, `connect()` shape combining
2
+ // `PermissionsService`'s signal return with `ScreenshotListenerService`'s effect+injector cleanup
3
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
4
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
5
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
6
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
7
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
8
+ var _, done = false;
9
+ for (var i = decorators.length - 1; i >= 0; i--) {
10
+ var context = {};
11
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
12
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
13
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
14
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
15
+ if (kind === "accessor") {
16
+ if (result === void 0) continue;
17
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
18
+ if (_ = accept(result.get)) descriptor.get = _;
19
+ if (_ = accept(result.set)) descriptor.set = _;
20
+ if (_ = accept(result.init)) initializers.unshift(_);
21
+ }
22
+ else if (_ = accept(result)) {
23
+ if (kind === "field") initializers.unshift(_);
24
+ else descriptor[key] = _;
25
+ }
26
+ }
27
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
28
+ done = true;
29
+ };
30
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
31
+ var useValue = arguments.length > 2;
32
+ for (var i = 0; i < initializers.length; i++) {
33
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
34
+ }
35
+ return useValue ? value : void 0;
36
+ };
37
+ import { effect, Injectable, Injector, signal, inject, } from '@angular/core';
38
+ import { addNotificationResponseClearedListener, addNotificationResponseReceivedListener, determineNextResponse, getLastNotificationResponse, } from '../../../core/index.js';
39
+ let LastNotificationResponseService = (() => {
40
+ let _classDecorators = [Injectable({ providedIn: 'root' })];
41
+ let _classDescriptor;
42
+ let _classExtraInitializers = [];
43
+ let _classThis;
44
+ var LastNotificationResponseService = class {
45
+ static { _classThis = this; }
46
+ static {
47
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
48
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
49
+ LastNotificationResponseService = _classThis = _classDescriptor.value;
50
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
51
+ __runInitializers(_classThis, _classExtraInitializers);
52
+ }
53
+ injector = inject(Injector);
54
+ lastResponse = signal(undefined);
55
+ isConnected = false;
56
+ connect() {
57
+ if (!this.isConnected) {
58
+ this.isConnected = true;
59
+ effect(onCleanup => {
60
+ this.lastResponse.update(prev => determineNextResponse(prev, getLastNotificationResponse()));
61
+ const subscription = addNotificationResponseReceivedListener(next => {
62
+ this.lastResponse.update(prev => determineNextResponse(prev, next));
63
+ });
64
+ const clearedSubscription = addNotificationResponseClearedListener(() => {
65
+ this.lastResponse.set(null);
66
+ });
67
+ onCleanup(() => {
68
+ subscription.remove();
69
+ clearedSubscription.remove();
70
+ });
71
+ }, { injector: this.injector });
72
+ }
73
+ return this.lastResponse.asReadonly();
74
+ }
75
+ };
76
+ return LastNotificationResponseService = _classThis;
77
+ })();
78
+ export { LastNotificationResponseService };
@@ -0,0 +1,5 @@
1
+ export type IDevicePushTokenRegistration = {
2
+ isEnabled: boolean;
3
+ };
4
+ export declare function handlePersistedRegistrationInfoAsync(registrationInfo: string | null | undefined): Promise<void>;
5
+ export declare function installPushTokenAutoRegistration(): () => void;
@@ -0,0 +1,65 @@
1
+ // Порт `DevicePushTokenAutoRegistration.fx.ts`, ресинк токена на сервер Expo
2
+ import { UnavailabilityError } from 'expo-modules-core';
3
+ import { addPushTokenListener, getDevicePushTokenAsync } from './device-token.js';
4
+ import { serverRegistrationModule } from './native-modules.js';
5
+ import { startAbortableRegistration } from './registration-abort.js';
6
+ import { hasDeviceTokenChangedAsync, updateDevicePushTokenAsync, } from './update-device-push-token.js';
7
+ async function updatePushTokenAsync(token) {
8
+ if (!(await hasDeviceTokenChangedAsync(token)))
9
+ return;
10
+ await updateDevicePushTokenAsync(startAbortableRegistration(), token);
11
+ }
12
+ function isEnabledRegistration(info) {
13
+ if (!info)
14
+ return false;
15
+ try {
16
+ const registration = JSON.parse(info);
17
+ return registration?.isEnabled === true;
18
+ }
19
+ catch (error) {
20
+ console.warn('[notifications] Error encountered while fetching registration information for auto token updates.', error);
21
+ return false;
22
+ }
23
+ }
24
+ // Досылает токен, если регистрация включена, а сервер его не получил
25
+ export async function handlePersistedRegistrationInfoAsync(registrationInfo) {
26
+ if (!isEnabledRegistration(registrationInfo))
27
+ return;
28
+ try {
29
+ // Регистрация включена, поэтому запрос "нового" токена безопасен
30
+ await updatePushTokenAsync(await getDevicePushTokenAsync());
31
+ }
32
+ catch (error) {
33
+ console.warn('[notifications] Error encountered while updating server registration with latest device push token.', error);
34
+ }
35
+ }
36
+ let removeListener = null;
37
+ // Upstream ставит подписку при загрузке модуля, здесь явно, т.к. под `inlineRequires` barrel
38
+ // не гарантирует выполнение побочного эффекта
39
+ export function installPushTokenAutoRegistration() {
40
+ if (removeListener)
41
+ return removeListener;
42
+ const getRegistrationInfoAsync = serverRegistrationModule.getRegistrationInfoAsync;
43
+ if (!getRegistrationInfoAsync) {
44
+ console.warn('[notifications] Error encountered while fetching auto-registration state, new tokens will not be automatically registered on server.', new UnavailabilityError('ServerRegistrationModule', 'getRegistrationInfoAsync'));
45
+ return () => undefined;
46
+ }
47
+ const subscription = addPushTokenListener(async (token) => {
48
+ try {
49
+ if (isEnabledRegistration(await serverRegistrationModule.getRegistrationInfoAsync?.())) {
50
+ await updatePushTokenAsync(token);
51
+ }
52
+ }
53
+ catch (error) {
54
+ console.warn('[notifications] Error encountered while updating server registration with latest device push token.', error);
55
+ }
56
+ });
57
+ getRegistrationInfoAsync().then(handlePersistedRegistrationInfoAsync, error => {
58
+ console.error('[notifications] Error reading persisted server registration info: ', error);
59
+ });
60
+ removeListener = () => {
61
+ subscription.remove();
62
+ removeListener = null;
63
+ };
64
+ return removeListener;
65
+ }
@@ -0,0 +1,6 @@
1
+ export declare function computeNextBackoffInterval(initialBackoff: number, previousRetryCount: number, { multiplier, randomizationFactor, minBackoff, maxBackoff, }?: {
2
+ multiplier?: number | undefined;
3
+ randomizationFactor?: number | undefined;
4
+ minBackoff?: number | undefined;
5
+ maxBackoff?: number | undefined;
6
+ }): number;
@@ -0,0 +1,15 @@
1
+ // Ported from expo-notifications sdk-57 utils/backoff.ts, itself from ide/backoff
2
+ export function computeNextBackoffInterval(initialBackoff, previousRetryCount, { multiplier = 1.5, randomizationFactor = 0.25, minBackoff = initialBackoff, maxBackoff = Infinity, } = {}) {
3
+ if (initialBackoff <= 0)
4
+ initialBackoff = 1;
5
+ if (previousRetryCount < 0)
6
+ previousRetryCount = 0;
7
+ if (multiplier < 1)
8
+ multiplier = 1;
9
+ if (randomizationFactor < 0 || randomizationFactor > 1)
10
+ randomizationFactor = 0;
11
+ const nextBackoff = initialBackoff * multiplier ** previousRetryCount;
12
+ // Jitter within the negative to positive range of the randomization factor
13
+ const jitterFactor = 1 - randomizationFactor + 2 * randomizationFactor * Math.random();
14
+ return Math.min(Math.max(nextBackoff * jitterFactor, minBackoff), maxBackoff);
15
+ }
@@ -0,0 +1,7 @@
1
+ import { type EventSubscription } from 'expo-modules-core';
2
+ import type { IDevicePushToken } from './types';
3
+ export type IPushTokenListener = (token: IDevicePushToken) => void;
4
+ /** Fires whenever the OS rolls the device push token while the app is running */
5
+ export declare function addPushTokenListener(listener: IPushTokenListener): EventSubscription;
6
+ /** Returns the native FCM (Android) or APNs (iOS) device push token */
7
+ export declare function getDevicePushTokenAsync(): Promise<IDevicePushToken>;
@@ -0,0 +1,35 @@
1
+ // Отдельно от `tokens.ts`, чтобы `auto-registration.ts` не зависел от него по кругу
2
+ import { Platform, UnavailabilityError, } from 'expo-modules-core';
3
+ import { dlog } from '@symbiote-native/engine';
4
+ import { pushTokenManager } from './native-modules.js';
5
+ // Пакет только под iOS/Android, поэтому `Platform.OS` сужаем один раз здесь, а не кастами
6
+ function nativePlatform() {
7
+ if (Platform.OS === 'ios' || Platform.OS === 'android') {
8
+ return Platform.OS;
9
+ }
10
+ throw new Error(`[notifications] unsupported platform: ${Platform.OS}`);
11
+ }
12
+ /** Fires whenever the OS rolls the device push token while the app is running */
13
+ export function addPushTokenListener(listener) {
14
+ return pushTokenManager.addListener('onDevicePushToken', ({ devicePushToken }) => {
15
+ listener({ data: devicePushToken, type: nativePlatform() });
16
+ });
17
+ }
18
+ let nativeTokenPromise = null;
19
+ /** Returns the native FCM (Android) or APNs (iOS) device push token */
20
+ export async function getDevicePushTokenAsync() {
21
+ if (!pushTokenManager.getDevicePushTokenAsync) {
22
+ throw new UnavailabilityError('Notifications', 'getDevicePushTokenAsync');
23
+ }
24
+ let devicePushToken;
25
+ if (nativeTokenPromise) {
26
+ devicePushToken = await nativeTokenPromise;
27
+ }
28
+ else {
29
+ nativeTokenPromise = pushTokenManager.getDevicePushTokenAsync();
30
+ devicePushToken = await nativeTokenPromise;
31
+ nativeTokenPromise = null;
32
+ }
33
+ dlog('[notifications] getDevicePushTokenAsync resolved');
34
+ return { type: nativePlatform(), data: devicePushToken };
35
+ }
@@ -20,5 +20,9 @@ export declare function getLastNotificationResponse(): INotificationResponse | n
20
20
  * be re-applied on the next read.
21
21
  */
22
22
  export declare function clearLastNotificationResponse(): void;
23
+ /** @deprecated Use `getLastNotificationResponse` instead */
24
+ export declare function getLastNotificationResponseAsync(): Promise<INotificationResponse | null>;
25
+ /** @deprecated Use `clearLastNotificationResponse` instead */
26
+ export declare function clearLastNotificationResponseAsync(): Promise<void>;
23
27
  /** Fires whenever `clearLastNotificationResponse` runs. */
24
28
  export declare function addNotificationResponseClearedListener(listener: () => void): EventSubscription;
@@ -52,6 +52,14 @@ export function clearLastNotificationResponse() {
52
52
  notificationsEmitterModule.clearLastNotificationResponse();
53
53
  emitter.emit(RESPONSE_CLEARED_EVENT, []);
54
54
  }
55
+ /** @deprecated Use `getLastNotificationResponse` instead */
56
+ export async function getLastNotificationResponseAsync() {
57
+ return getLastNotificationResponse();
58
+ }
59
+ /** @deprecated Use `clearLastNotificationResponse` instead */
60
+ export async function clearLastNotificationResponseAsync() {
61
+ clearLastNotificationResponse();
62
+ }
55
63
  /** Fires whenever `clearLastNotificationResponse` runs. */
56
64
  export function addNotificationResponseClearedListener(listener) {
57
65
  return emitter.addListener(RESPONSE_CLEARED_EVENT, listener);
@@ -1,12 +1,14 @@
1
1
  export { getPermissionsAsync, requestPermissionsAsync } from './permissions';
2
2
  export { addPushTokenListener, getDevicePushTokenAsync, getExpoPushTokenAsync, setAutoServerRegistrationEnabledAsync, subscribeToTopicAsync, unregisterForNotificationsAsync, unsubscribeFromTopicAsync, type IPushTokenListener, } from './tokens';
3
+ export { installPushTokenAutoRegistration, type IDevicePushTokenRegistration, } from './auto-registration';
3
4
  export { dismissAllNotificationsAsync, dismissNotificationAsync, getPresentedNotificationsAsync, } from './presenter';
4
5
  export { getBadgeCountAsync, setBadgeCountAsync } from './badge';
5
6
  export { cancelAllScheduledNotificationsAsync, cancelScheduledNotificationAsync, getAllScheduledNotificationsAsync, getNextTriggerDateAsync, scheduleNotificationAsync, } from './scheduler';
6
7
  export { deleteNotificationCategoryAsync, getNotificationCategoriesAsync, setNotificationCategoryAsync, } from './categories';
7
8
  export { deleteNotificationChannelAsync, deleteNotificationChannelGroupAsync, getNotificationChannelAsync, getNotificationChannelGroupAsync, getNotificationChannelGroupsAsync, getNotificationChannelsAsync, setNotificationChannelAsync, setNotificationChannelGroupAsync, } from './channels';
8
9
  export { NotificationTimeoutError, setNotificationHandler } from './handler';
9
- export { DEFAULT_ACTION_IDENTIFIER, addNotificationReceivedListener, addNotificationResponseClearedListener, addNotificationResponseReceivedListener, addNotificationsDroppedListener, clearLastNotificationResponse, getLastNotificationResponse, } from './emitter';
10
+ export { DEFAULT_ACTION_IDENTIFIER, addNotificationReceivedListener, addNotificationResponseClearedListener, addNotificationResponseReceivedListener, addNotificationsDroppedListener, clearLastNotificationResponse, clearLastNotificationResponseAsync, getLastNotificationResponse, getLastNotificationResponseAsync, } from './emitter';
10
11
  export { BackgroundNotificationTaskResult, registerTaskAsync, unregisterTaskAsync, } from './background-task';
12
+ export { determineNextResponse } from './last-notification-response';
11
13
  export type { IAudioAttributes, IAudioAttributesInput, IBeaconRegion, ICalendarNotificationTrigger, ICalendarTriggerInput, IChannelAwareTriggerInput, ICircularRegion, IDailyNotificationTrigger, IDailyTriggerInput, IDateTriggerInput, IDevicePushToken, IExpoPushToken, IExpoPushTokenOptions, IFirebaseRemoteMessage, IFirebaseRemoteMessageNotification, IInterruptionLevel, IIosNotificationPermissionsRequest, ILocationNotificationTrigger, IMaybeNotificationResponse, IMonthlyNotificationTrigger, IMonthlyTriggerInput, INativeCalendarTriggerInput, INativeChannelAwareTriggerInput, INativeDailyTriggerInput, INativeDateTriggerInput, INativeDevicePushToken, INativeMonthlyTriggerInput, INativeNotificationTriggerInput, INativeTimeIntervalTriggerInput, INativeWeeklyTriggerInput, INativeYearlyTriggerInput, INotification, INotificationAction, INotificationBehavior, INotificationCategory, INotificationCategoryOptions, INotificationChannel, INotificationChannelGroup, INotificationChannelGroupInput, INotificationChannelInput, INotificationContent, INotificationContentAndroid, INotificationContentAttachmentIos, INotificationContentInput, INotificationContentIos, INotificationHandler, INotificationPermissionsRequest, INotificationPermissionsStatus, INotificationRequest, INotificationRequestInput, INotificationResponse, INotificationTaskPayload, INotificationTrigger, INotificationTriggerInput, IPushNotificationTrigger, IRegion, ISchedulableNotificationTriggerInput, ITimeIntervalNotificationTrigger, ITimeIntervalTriggerInput, IUnknownNotificationTrigger, IWeeklyNotificationTrigger, IWeeklyTriggerInput, IYearlyNotificationTrigger, IYearlyTriggerInput, PermissionExpiration, PermissionResponse, } from './types';
12
14
  export { AndroidAudioContentType, AndroidAudioUsage, AndroidImportance, AndroidNotificationPriority, AndroidNotificationVisibility, IosAlertStyle, IosAllowsPreviews, IosAuthorizationStatus, PermissionStatus, SchedulableTriggerInputTypes, } from './types';
@@ -1,11 +1,13 @@
1
1
  export { getPermissionsAsync, requestPermissionsAsync } from './permissions.js';
2
2
  export { addPushTokenListener, getDevicePushTokenAsync, getExpoPushTokenAsync, setAutoServerRegistrationEnabledAsync, subscribeToTopicAsync, unregisterForNotificationsAsync, unsubscribeFromTopicAsync, } from './tokens.js';
3
+ export { installPushTokenAutoRegistration, } from './auto-registration.js';
3
4
  export { dismissAllNotificationsAsync, dismissNotificationAsync, getPresentedNotificationsAsync, } from './presenter.js';
4
5
  export { getBadgeCountAsync, setBadgeCountAsync } from './badge.js';
5
6
  export { cancelAllScheduledNotificationsAsync, cancelScheduledNotificationAsync, getAllScheduledNotificationsAsync, getNextTriggerDateAsync, scheduleNotificationAsync, } from './scheduler.js';
6
7
  export { deleteNotificationCategoryAsync, getNotificationCategoriesAsync, setNotificationCategoryAsync, } from './categories.js';
7
8
  export { deleteNotificationChannelAsync, deleteNotificationChannelGroupAsync, getNotificationChannelAsync, getNotificationChannelGroupAsync, getNotificationChannelGroupsAsync, getNotificationChannelsAsync, setNotificationChannelAsync, setNotificationChannelGroupAsync, } from './channels.js';
8
9
  export { NotificationTimeoutError, setNotificationHandler } from './handler.js';
9
- export { DEFAULT_ACTION_IDENTIFIER, addNotificationReceivedListener, addNotificationResponseClearedListener, addNotificationResponseReceivedListener, addNotificationsDroppedListener, clearLastNotificationResponse, getLastNotificationResponse, } from './emitter.js';
10
+ export { DEFAULT_ACTION_IDENTIFIER, addNotificationReceivedListener, addNotificationResponseClearedListener, addNotificationResponseReceivedListener, addNotificationsDroppedListener, clearLastNotificationResponse, clearLastNotificationResponseAsync, getLastNotificationResponse, getLastNotificationResponseAsync, } from './emitter.js';
10
11
  export { BackgroundNotificationTaskResult, registerTaskAsync, unregisterTaskAsync, } from './background-task.js';
12
+ export { determineNextResponse } from './last-notification-response.js';
11
13
  export { AndroidAudioContentType, AndroidAudioUsage, AndroidImportance, AndroidNotificationPriority, AndroidNotificationVisibility, IosAlertStyle, IosAllowsPreviews, IosAuthorizationStatus, PermissionStatus, SchedulableTriggerInputTypes, } from './types.js';
@@ -0,0 +1,2 @@
1
+ import type { IMaybeNotificationResponse, INotificationResponse } from './types';
2
+ export declare function determineNextResponse(prevResponse: IMaybeNotificationResponse, newResponse: INotificationResponse | null): IMaybeNotificationResponse;
@@ -0,0 +1,12 @@
1
+ // Framework-agnostic twin of expo-notifications' `determineNextResponse` - every adapter's
2
+ // `useLastNotificationResponse` shares this dedup rule instead of re-deriving it
3
+ export function determineNextResponse(prevResponse, newResponse) {
4
+ if (!newResponse)
5
+ return null;
6
+ if (!prevResponse)
7
+ return newResponse;
8
+ return prevResponse.notification.request.identifier !==
9
+ newResponse.notification.request.identifier
10
+ ? newResponse
11
+ : prevResponse;
12
+ }
@@ -0,0 +1,2 @@
1
+ export declare function abortPendingRegistration(): void;
2
+ export declare function startAbortableRegistration(): AbortSignal;
@@ -0,0 +1,10 @@
1
+ // Один активный запрос ресинка токена, новый прерывает предыдущий
2
+ let lastAbortController = null;
3
+ export function abortPendingRegistration() {
4
+ lastAbortController?.abort();
5
+ }
6
+ export function startAbortableRegistration() {
7
+ abortPendingRegistration();
8
+ lastAbortController = new AbortController();
9
+ return lastAbortController.signal;
10
+ }
@@ -0,0 +1,4 @@
1
+ import type { IDevicePushToken } from './types';
2
+ export declare function getDeviceIdAsync(): Promise<string>;
3
+ export declare function getTypeOfToken(devicePushToken: IDevicePushToken): string;
4
+ export declare function shouldUseDevelopmentNotificationService(): Promise<boolean>;