@12-apps/notifications 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@12-apps/notifications",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "type": "module",
5
5
  "description": "Plug-and-play notification system (12-15): an always-on in-app inbox, per-user × per-category channel preferences, and email / SMS / WhatsApp / web-push transports behind vendor DRIVERS so a second provider is a config entry. Framework-free core (.), host-mounted backend surface (./server: inbox / preferences / push-subscription endpoints, the channel router with delivery records + retry sweep, the permission fan-out, duck-typed Prisma seam), Hono adapter (./hono), React surface (./react: bell + badge, inbox drawer, preferences screen), VAPID sender (./web-push) and the package-owned Prisma partial + migrations. Standardized adoption contract in ADOPTING.md.",
6
6
  "exports": {
@@ -11,7 +11,7 @@ import { Box } from '@12-apps/ui/mui/Box';
11
11
  import type { NotificationMessages } from '../messages';
12
12
 
13
13
  import { BellIcon } from './bell-icon';
14
- import { useUnreadCount, type NotificationsSubscribe } from './hooks';
14
+ import { useUnreadCount, type NotificationsSignalHook, type NotificationsSubscribe } from './hooks';
15
15
  import type { InboxStore } from './inbox-state';
16
16
 
17
17
  const triggerSx = {
@@ -46,14 +46,17 @@ export function BellButton({
46
46
  store,
47
47
  messages,
48
48
  subscribe,
49
+ useSignal,
49
50
  }: BellButtonProps & {
50
51
  store: InboxStore;
51
52
  messages: NotificationMessages;
52
53
  subscribe?: NotificationsSubscribe;
54
+ useSignal?: NotificationsSignalHook;
53
55
  }): JSX.Element {
54
56
  const count = useUnreadCount(store, {
55
57
  enabled,
56
58
  ...(subscribe ? { subscribe } : {}),
59
+ ...(useSignal ? { useSignal } : {}),
57
60
  });
58
61
  return (
59
62
  <Box
@@ -4,7 +4,11 @@ import { messagesOf, type NotificationMessages } from '../messages';
4
4
 
5
5
  import { createNotificationsApiClient, type NotificationsApiClient } from './api';
6
6
  import { BellButton, type BellButtonProps } from './bell-button';
7
- import { useUnreadCount, type NotificationsSubscribe } from './hooks';
7
+ import {
8
+ useUnreadCount,
9
+ type NotificationsSignalHook,
10
+ type NotificationsSubscribe,
11
+ } from './hooks';
8
12
  import { createInboxStore, type InboxStore } from './inbox-state';
9
13
  import { NotificationsPanel, type NotificationsPanelProps } from './panel';
10
14
  import { PreferencesScreen, type PreferencesScreenProps } from './preferences-screen';
@@ -41,6 +45,12 @@ export interface NotificationsWebConfig {
41
45
  * correctness.
42
46
  */
43
47
  subscribe?: NotificationsSubscribe;
48
+ /**
49
+ * The same wiring as a HOOK, for a host whose realtime connection lives in
50
+ * React context — see `NotificationsSignalHook`. `subscribe` is read at
51
+ * factory time, which such a host cannot reach.
52
+ */
53
+ useSignal?: NotificationsSignalHook;
44
54
  /** The browser push enable step's host seams (SW path, platform hint). */
45
55
  webPush?: WebPushSetupConfig;
46
56
  }
@@ -79,7 +89,10 @@ export function createWebNotifications(config: NotificationsWebConfig): WebNotif
79
89
  const store = createInboxStore(api);
80
90
  const webPush = config.webPush ?? {};
81
91
  const subscribe = config.subscribe;
82
- const subscribeOption = subscribe ? { subscribe } : {};
92
+ const subscribeOption = {
93
+ ...(subscribe ? { subscribe } : {}),
94
+ ...(config.useSignal ? { useSignal: config.useSignal } : {}),
95
+ };
83
96
 
84
97
  const Bell: ComponentType<BellButtonProps> = (props) => (
85
98
  <BellButton {...props} store={store} messages={messages} {...subscribeOption} />
@@ -24,6 +24,26 @@ import {
24
24
  */
25
25
  export type NotificationsSubscribe = (onHint: () => void) => () => void;
26
26
 
27
+ /**
28
+ * The same wiring, as a HOOK — for a host whose realtime connection lives in
29
+ * React context rather than in a module.
30
+ *
31
+ * `subscribe` above is supplied at FACTORY time, which is module scope, and a
32
+ * context-bound connection cannot be reached from there: the provider holding
33
+ * it is inside the tree. A host in that shape (a `<UserRealtimeProvider>` and a
34
+ * `useUserTopics` hook, which is the common one) had no way to pass anything at
35
+ * all, and the badge simply never heard an event.
36
+ *
37
+ * So this is the second door, and it is the one `@12-apps/app-shell` already
38
+ * uses for the same problem — its consent dialog takes a `useSignal` hook for
39
+ * exactly this reason. Two packages solving one problem two ways is how an
40
+ * adopter ends up believing the feature is unavailable to it.
41
+ *
42
+ * Called during render, so it may use context and hooks freely. Pass one or
43
+ * the other; passing both runs both, which is a host's business.
44
+ */
45
+ export type NotificationsSignalHook = (onHint: () => void) => void;
46
+
27
47
  export function useInboxState(store: InboxStore): InboxState {
28
48
  return useSyncExternalStore(store.subscribe, store.getState, store.getState);
29
49
  }
@@ -36,12 +56,22 @@ export function useInboxState(store: InboxStore): InboxState {
36
56
  */
37
57
  export function useUnreadCount(
38
58
  store: InboxStore,
39
- options: { enabled?: boolean; subscribe?: NotificationsSubscribe } = {},
59
+ options: {
60
+ enabled?: boolean;
61
+ subscribe?: NotificationsSubscribe;
62
+ useSignal?: NotificationsSignalHook;
63
+ } = {},
40
64
  ): number {
41
65
  const enabled = options.enabled ?? true;
42
66
  const subscribe = options.subscribe;
43
67
  const { unread } = useInboxState(store);
44
68
 
69
+ // Called unconditionally — it is a hook, so it cannot sit behind `enabled`.
70
+ // The host's own hook decides what to do when there is nothing to hear.
71
+ options.useSignal?.(() => {
72
+ if (enabled) store.invalidate();
73
+ });
74
+
45
75
  useEffect(() => {
46
76
  if (!enabled) return;
47
77
  store.refreshBadge();
@@ -40,6 +40,7 @@ export {
40
40
  useInboxList,
41
41
  useInboxState,
42
42
  useUnreadCount,
43
+ type NotificationsSignalHook,
43
44
  type NotificationsSubscribe,
44
45
  } from './hooks';
45
46