@12-apps/notifications 4.7.0 → 4.7.1
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/dist/chunk-BW723CX2.js +214 -0
- package/dist/chunk-BW723CX2.js.map +1 -0
- package/dist/chunk-CQZMTFPY.js +76 -0
- package/dist/chunk-CQZMTFPY.js.map +1 -0
- package/dist/{chunk-QRAXX3GR.js → chunk-CUZW62JS.js} +2 -2
- package/dist/{chunk-HQU4R4SG.js → chunk-HHMRCMQU.js} +2 -2
- package/dist/chunk-M2TVBVH2.js +15 -0
- package/dist/chunk-M2TVBVH2.js.map +1 -0
- package/dist/chunk-MMLV4EZT.js +263 -0
- package/dist/chunk-MMLV4EZT.js.map +1 -0
- package/dist/chunk-O5BVUXPO.js +22 -0
- package/dist/chunk-O5BVUXPO.js.map +1 -0
- package/dist/{chunk-TIGTBSAQ.js → chunk-WHBMPHQE.js} +6 -4
- package/dist/{chunk-TIGTBSAQ.js.map → chunk-WHBMPHQE.js.map} +1 -1
- package/dist/{chunk-Y34FX24X.js → chunk-XE7HZVMH.js} +2 -10
- package/dist/chunk-XE7HZVMH.js.map +1 -0
- package/dist/{create-web-notifications-BpNR8qH3.d.ts → create-web-notifications-BHCzaU2y.d.ts} +13 -2
- package/dist/hono/index.js +4 -3
- package/dist/hono/index.js.map +1 -1
- package/dist/index.js +5 -3
- package/dist/manifest/server.js +5 -4
- package/dist/manifest/server.js.map +1 -1
- package/dist/manifest/web.d.ts +1 -1
- package/dist/manifest/web.js +3 -2
- package/dist/manifest/web.js.map +1 -1
- package/dist/panel-UFXNO4AF.js +243 -0
- package/dist/panel-UFXNO4AF.js.map +1 -0
- package/dist/preferences-screen-IOW6Y2H2.js +294 -0
- package/dist/preferences-screen-IOW6Y2H2.js.map +1 -0
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +17 -11
- package/dist/server/index.js +5 -4
- package/package.json +2 -2
- package/src/react/create-web-notifications.tsx +19 -10
- package/src/react/page-lazy.tsx +73 -0
- package/src/react/panel-lazy.tsx +74 -0
- package/dist/chunk-6HLHQDKS.js +0 -1022
- package/dist/chunk-6HLHQDKS.js.map +0 -1
- package/dist/chunk-Y34FX24X.js.map +0 -1
- /package/dist/{chunk-QRAXX3GR.js.map → chunk-CUZW62JS.js.map} +0 -0
- /package/dist/{chunk-HQU4R4SG.js.map → chunk-HHMRCMQU.js.map} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/react/api.ts","../src/react/bell-icon.tsx","../src/react/inbox-state.ts","../src/react/hooks.ts","../src/react/relative-time.ts","../src/react/web-push-client.ts","../src/react/transport.ts","../src/react/create-web-notifications.tsx","../src/react/bell-button.tsx","../src/react/panel.tsx","../src/react/row.tsx","../src/react/preferences-screen.tsx","../src/react/web-push-setup.tsx"],"sourcesContent":["import type { ChannelRow } from '../preferences-core';\nimport type { NotificationChannel } from '../types';\nimport type { ListNotificationsResult } from '../wire';\n\nimport type { NotificationsResult, NotificationsTransport } from './transport';\n\n/**\n * The wire client, bound to one mount (12-15).\n *\n * Every path this package's screens can call, in one place — which is what\n * makes the api half's route table and the web half's URLs one contract instead\n * of two lists that drift.\n */\n\n/** `GET <mount>/notification-preferences` and the PUT's answer. */\nexport interface PreferencesPayload {\n preferences: Record<string, ChannelRow>;\n availability: Record<NotificationChannel, boolean>;\n /** The host's taxonomy, so the screen renders it without being told twice. */\n categories: string[];\n}\n\n/** `GET <mount>/push-subscriptions`. */\nexport interface PushRegistrationPayload {\n /** null = web push is not configured on this deployment. */\n vapidPublicKey: string | null;\n count: number;\n /**\n * Whether the endpoint asked about is still registered to the caller. Present\n * only when one was passed — see {@link NotificationsApiClient.getPushRegistration}.\n */\n registered?: boolean;\n}\n\nexport interface NotificationsApiClient {\n listNotifications(input: {\n cursor?: string | null;\n limit?: number;\n filter?: 'all' | 'unread';\n }): Promise<ListNotificationsResult>;\n unreadCount(): Promise<number>;\n markRead(ids: readonly string[]): Promise<NotificationsResult<{ updated: number }>>;\n markAllRead(): Promise<NotificationsResult<{ updated: number }>>;\n remove(ids: readonly string[]): Promise<NotificationsResult<{ deleted: number }>>;\n getPreferences(): Promise<PreferencesPayload>;\n savePreference(\n category: string,\n channel: NotificationChannel,\n enabled: boolean,\n ): Promise<NotificationsResult<PreferencesPayload>>;\n /**\n * The deployment's VAPID key and the caller's device count — and, when an\n * `endpoint` is passed, whether the SERVER still has that exact subscription\n * under the caller's id. The browser holding a subscription object is not\n * evidence of that: a re-own or a 404/410 prune drops the row and leaves the\n * browser's object in place.\n */\n getPushRegistration(input?: { endpoint?: string }): Promise<PushRegistrationPayload>;\n savePushSubscription(input: {\n endpoint: string;\n keys: { p256dh: string; auth: string };\n }): Promise<NotificationsResult<{ count: number }>>;\n removePushSubscription(endpoint: string): Promise<NotificationsResult<{ count: number }>>;\n}\n\nexport function createNotificationsApiClient(\n apiBase: string,\n transport: NotificationsTransport,\n): NotificationsApiClient {\n const base = apiBase.replace(/\\/$/, '');\n const url = (path: string): string => `${base}${path}`;\n\n return {\n listNotifications({ cursor, limit, filter }) {\n const params = new URLSearchParams();\n if (limit !== undefined) params.set('limit', String(limit));\n if (cursor) params.set('cursor', cursor);\n if (filter) params.set('filter', filter);\n const query = params.toString();\n return transport.get<ListNotificationsResult>(\n url(`/notifications${query ? `?${query}` : ''}`),\n );\n },\n async unreadCount() {\n const { count } = await transport.get<{ count: number }>(\n url('/notifications/unread-count'),\n );\n return count;\n },\n markRead: (ids) =>\n transport.send(url('/notifications/mark-read'), 'POST', { ids: [...ids] }),\n markAllRead: () => transport.send(url('/notifications/mark-read'), 'POST', { all: true }),\n remove: (ids) => transport.send(url('/notifications/delete'), 'POST', { ids: [...ids] }),\n getPreferences: () => transport.get<PreferencesPayload>(url('/notification-preferences')),\n savePreference: (category, channel, enabled) =>\n transport.send(url('/notification-preferences'), 'PUT', {\n [category]: { [channel]: enabled },\n }),\n getPushRegistration: ({ endpoint } = {}) =>\n transport.get<PushRegistrationPayload>(\n url(\n endpoint\n ? `/push-subscriptions?endpoint=${encodeURIComponent(endpoint)}`\n : '/push-subscriptions',\n ),\n ),\n savePushSubscription: (input) => transport.send(url('/push-subscriptions'), 'POST', input),\n removePushSubscription: (endpoint) =>\n transport.send(url('/push-subscriptions'), 'DELETE', { endpoint }),\n };\n}\n","/** Inline SVG bell (no icon-library dependency in this package). */\nimport type { JSX } from 'react';\n\nimport { Box } from '@12-apps/ui/mui/Box';\n\nexport function BellIcon({\n size = 28,\n dim = false,\n}: {\n size?: number;\n dim?: boolean;\n}): JSX.Element {\n return (\n <Box\n component=\"svg\"\n viewBox=\"0 0 24 24\"\n aria-hidden\n sx={{\n width: size,\n height: size,\n fill: 'none',\n stroke: 'currentColor',\n opacity: dim ? 0.4 : 1,\n }}\n strokeWidth={1.8}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M6 9a6 6 0 0 1 12 0c0 5 2 6 2 6H4s2-1 2-6\" />\n <path d=\"M10 20a2 2 0 0 0 4 0\" />\n </Box>\n );\n}\n","import type { InboxNotification } from '../wire';\n\nimport type { NotificationsApiClient } from './api';\n\n/**\n * The inbox's client state, as ONE store shared by the bell and the panel.\n *\n * They have to share it: marking a row read in the panel must move the badge in\n * the same tick, and an arrival must add a row to the list AND to the count.\n * the origin got that for free from a react-query cache the host had already\n * mounted; a published package cannot assume one — a query client is a host\n * decision, and requiring a particular one (or a particular version of one) is\n * the kind of dependency that keeps a package out of a host that made the other\n * choice. So the sharing is explicit and dependency-free: one subscribable\n * store, read through `useSyncExternalStore`.\n *\n * Optimistic on every write, with invalidate-on-error: the badge and the list\n * update instantly, and a failed write refetches the server truth rather than\n * leaving the screen asserting something the database does not say.\n */\n\nexport const PAGE_SIZE = 20;\n\n/** The badge's poll while nothing is pushing to us. */\nexport const BADGE_POLL_MS = 60_000;\n\n/**\n * The badge's interval while a realtime connection is live.\n *\n * Five minutes, not \"never\": this is the reconcile that catches an event the bus\n * dropped, and it costs one COUNT per open tab per five minutes. Deliberately\n * far slower than an operational screen's — a bell badge is ambient, and the\n * arrival that matters is pushed within milliseconds anyway. The poll does NOT\n * stop, which is the standing contract: a dropped event must cost latency and\n * never correctness.\n */\nexport const BADGE_RECONCILE_MS = 300_000;\n\nexport type InboxListStatus = 'idle' | 'pending' | 'ready' | 'error';\n\nexport interface InboxState {\n unread: number;\n items: InboxNotification[];\n status: InboxListStatus;\n /** A cursor means there is another page. */\n nextCursor: string | null;\n loadingMore: boolean;\n}\n\nexport interface InboxStore {\n getState(): InboxState;\n subscribe(listener: () => void): () => void;\n /** Load the first page (idempotent while one is in flight). */\n open(): void;\n /** Refetch the badge count. */\n refreshBadge(): void;\n /** Refetch both — what a realtime hint or a failed write triggers. */\n invalidate(): void;\n loadMore(): void;\n markRead(ids: readonly string[]): void;\n markAllRead(): void;\n remove(id: string): void;\n}\n\nconst EMPTY: InboxState = {\n unread: 0,\n items: [],\n status: 'idle',\n nextCursor: null,\n loadingMore: false,\n};\n\n/** The mutable cell the functions below share, so each one stays small. */\ninterface Cell {\n state: InboxState;\n listeners: Set<() => void>;\n /** Fences a stale reload: a newer one must always win. */\n request: number;\n}\n\nfunction patch(cell: Cell, next: Partial<InboxState>): void {\n cell.state = { ...cell.state, ...next };\n for (const listener of cell.listeners) listener();\n}\n\n/** Refetch the badge count. The number is always one the server just gave us. */\nfunction refreshBadge(cell: Cell, api: NotificationsApiClient): void {\n void api\n .unreadCount()\n .then((unread) => patch(cell, { unread }))\n .catch(() => undefined);\n}\n\n/**\n * Reload page one, discarding whatever the optimistic path had produced.\n * `request` fences it: a reload that started before a newer one must not land\n * after it and reinstate stale rows.\n */\nfunction reloadList(cell: Cell, api: NotificationsApiClient): void {\n const token = (cell.request += 1);\n patch(cell, { status: cell.state.items.length > 0 ? cell.state.status : 'pending' });\n void api\n .listNotifications({ limit: PAGE_SIZE })\n .then((page) => {\n if (token !== cell.request) return;\n patch(cell, { items: page.items, nextCursor: page.nextCursor, status: 'ready' });\n })\n .catch(() => {\n if (token !== cell.request) return;\n patch(cell, { status: 'error' });\n });\n}\n\nfunction invalidate(cell: Cell, api: NotificationsApiClient): void {\n refreshBadge(cell, api);\n if (cell.state.status !== 'idle') reloadList(cell, api);\n}\n\n/** Apply an optimistic edit; on failure, take the server's word instead. */\nfunction write(\n cell: Cell,\n api: NotificationsApiClient,\n apply: () => void,\n send: () => Promise<{ ok: boolean }>,\n): void {\n apply();\n void send()\n .then((result) => {\n if (!result.ok) invalidate(cell, api);\n })\n .catch(() => invalidate(cell, api));\n}\n\nfunction bumpUnread(cell: Cell, delta: number): void {\n patch(cell, { unread: Math.max(0, cell.state.unread + delta) });\n}\n\nfunction loadMore(cell: Cell, api: NotificationsApiClient): void {\n const cursor = cell.state.nextCursor;\n if (!cursor || cell.state.loadingMore) return;\n patch(cell, { loadingMore: true });\n void api\n .listNotifications({ cursor, limit: PAGE_SIZE })\n .then((page) => {\n patch(cell, {\n items: [...cell.state.items, ...page.items],\n nextCursor: page.nextCursor,\n loadingMore: false,\n });\n })\n .catch(() => patch(cell, { loadingMore: false }));\n}\n\nfunction markRead(cell: Cell, api: NotificationsApiClient, ids: readonly string[]): void {\n const readAt = new Date().toISOString();\n let flipped = 0;\n const items = cell.state.items.map((item) => {\n if (!ids.includes(item.id) || item.readAt !== null) return item;\n flipped += 1;\n return { ...item, readAt };\n });\n if (flipped === 0) return;\n write(\n cell,\n api,\n () => {\n patch(cell, { items });\n bumpUnread(cell, -flipped);\n },\n () => api.markRead(ids),\n );\n}\n\nfunction remove(cell: Cell, api: NotificationsApiClient, id: string): void {\n const target = cell.state.items.find((item) => item.id === id);\n if (!target) return;\n const items = cell.state.items.filter((item) => item.id !== id);\n write(\n cell,\n api,\n () => {\n patch(cell, { items });\n if (target.readAt === null) bumpUnread(cell, -1);\n },\n () => api.remove([id]),\n );\n}\n\nexport function createInboxStore(api: NotificationsApiClient): InboxStore {\n const cell: Cell = { state: EMPTY, listeners: new Set(), request: 0 };\n return {\n getState: () => cell.state,\n subscribe(listener) {\n cell.listeners.add(listener);\n return () => cell.listeners.delete(listener);\n },\n open() {\n if (cell.state.status === 'idle') reloadList(cell, api);\n },\n refreshBadge: () => refreshBadge(cell, api),\n invalidate: () => invalidate(cell, api),\n loadMore: () => loadMore(cell, api),\n markRead: (ids) => markRead(cell, api, ids),\n markAllRead() {\n const readAt = new Date().toISOString();\n const items = cell.state.items.map((item) => ({ ...item, readAt: item.readAt ?? readAt }));\n write(\n cell,\n api,\n () => patch(cell, { items, unread: 0 }),\n () => api.markAllRead(),\n );\n },\n remove: (id) => remove(cell, api, id),\n };\n}\n","import { useEffect, useSyncExternalStore } from 'react';\n\nimport {\n BADGE_POLL_MS,\n BADGE_RECONCILE_MS,\n type InboxState,\n type InboxStore,\n} from './inbox-state';\n\n/**\n * The two hooks the bell and the panel use, and the realtime seam between them.\n *\n * A host that has a message bus passes `subscribe`; one that has not passes\n * nothing and keeps the 60 s poll. The bell ships in this package and mounts in\n * whatever embeds it, so it must not require the host to have adopted anything.\n */\n\n/**\n * How the surface learns an inbox changed without asking.\n *\n * Called once per mounted bell with a callback that means only \"ask again\" — no\n * payload, so the number on screen is always one the server just gave us.\n * Returns its own teardown. A host wires this to whatever it already has.\n */\nexport type NotificationsSubscribe = (onHint: () => void) => () => void;\n\n/**\n * The same wiring, as a HOOK — for a host whose realtime connection lives in\n * React context rather than in a module.\n *\n * `subscribe` above is supplied at FACTORY time, which is module scope, and a\n * context-bound connection cannot be reached from there: the provider holding\n * it is inside the tree. A host in that shape (a `<UserRealtimeProvider>` and a\n * `useUserTopics` hook, which is the common one) had no way to pass anything at\n * all, and the badge simply never heard an event.\n *\n * So this is the second door, and it is the one `@12-apps/app-shell` already\n * uses for the same problem — its consent dialog takes a `useSignal` hook for\n * exactly this reason. Two packages solving one problem two ways is how an\n * adopter ends up believing the feature is unavailable to it.\n *\n * Called during render, so it may use context and hooks freely. Pass one or\n * the other; passing both runs both, which is a host's business.\n */\nexport type NotificationsSignalHook = (onHint: () => void) => void;\n\nexport function useInboxState(store: InboxStore): InboxState {\n return useSyncExternalStore(store.subscribe, store.getState, store.getState);\n}\n\n/**\n * The bell badge number: pushed while a subscription is live, polled otherwise.\n *\n * `enabled` gates the poll AND the subscription. A signed-out header still\n * mounts the bell, and there is nothing for it to hear.\n */\nexport function useUnreadCount(\n store: InboxStore,\n options: {\n enabled?: boolean;\n subscribe?: NotificationsSubscribe;\n useSignal?: NotificationsSignalHook;\n } = {},\n): number {\n const enabled = options.enabled ?? true;\n const subscribe = options.subscribe;\n const { unread } = useInboxState(store);\n\n // Called unconditionally — it is a hook, so it cannot sit behind `enabled`.\n // The host's own hook decides what to do when there is nothing to hear.\n options.useSignal?.(() => {\n if (enabled) store.invalidate();\n });\n\n useEffect(() => {\n if (!enabled) return;\n store.refreshBadge();\n const unsubscribe = subscribe?.(() => store.invalidate());\n // A live subscription relaxes the poll to the reconcile interval; without\n // one it stays the 60 s poll.\n const interval = setInterval(\n () => store.refreshBadge(),\n subscribe ? BADGE_RECONCILE_MS : BADGE_POLL_MS,\n );\n const onFocus = (): void => store.refreshBadge();\n globalThis.addEventListener?.('focus', onFocus);\n return () => {\n clearInterval(interval);\n globalThis.removeEventListener?.('focus', onFocus);\n unsubscribe?.();\n };\n }, [store, enabled, subscribe]);\n\n return enabled ? unread : 0;\n}\n\n/** The panel's list — only fetches while the panel is open. */\nexport function useInboxList(store: InboxStore, open: boolean): InboxState {\n const state = useInboxState(store);\n useEffect(() => {\n if (open) store.open();\n }, [store, open]);\n return state;\n}\n","import type { NotificationMessages } from '../messages';\n\n/**\n * \"há 5 min\"-style relative timestamp, falling back to an absolute date for\n * anything older than a week. Every word comes from the messages table, so a\n * host in another locale changes the copy and the locale together.\n */\nexport function relativeTime(iso: string, messages: NotificationMessages): string {\n const elapsedMs = Date.now() - new Date(iso).getTime();\n const minutes = Math.round(elapsedMs / 60_000);\n if (minutes < 1) return messages.justNow;\n if (minutes < 60) return messages.minutesAgo(minutes);\n const hours = Math.round(minutes / 60);\n if (hours < 24) return messages.hoursAgo(hours);\n const days = Math.round(hours / 24);\n if (days < 7) return messages.daysAgo(days);\n return new Date(iso).toLocaleDateString(messages.dateLocale);\n}\n","import type { NotificationsApiClient } from './api';\n\n/**\n * The browser half of Web Push: register the service worker, ask permission,\n * subscribe with the deployment's VAPID public key (read from the packaged\n * `GET <mount>/push-subscriptions`) and persist the subscription so the\n * WEB_PUSH transport can reach this browser.\n *\n * A preference alone cannot reach a device that never subscribed, which is why\n * this ships with the preferences screen rather than being left to the host.\n * The one thing that IS the host's is the service-worker path — path-routed SPAs\n * each control their own scope, and the file itself lives in the host's public\n * directory.\n */\n\n/** Why enabling push failed, mapped to a user-facing hint by the caller. */\nexport type PushSetupResult =\n | { ok: true }\n | { ok: false; reason: 'unsupported' | 'unconfigured' | 'permission-denied' | 'error' };\n\n/** `PushManager.subscribe` needs the VAPID key as a Uint8Array. */\nfunction base64UrlToUint8Array(base64Url: string): Uint8Array {\n const padding = '='.repeat((4 - (base64Url.length % 4)) % 4);\n const base64 = (base64Url + padding).replaceAll('-', '+').replaceAll('_', '/');\n const raw = atob(base64);\n return Uint8Array.from(raw, (char) => char.charCodeAt(0));\n}\n\nexport function pushSupported(): boolean {\n return (\n typeof navigator !== 'undefined' &&\n 'serviceWorker' in navigator &&\n typeof window !== 'undefined' &&\n 'PushManager' in window &&\n 'Notification' in window\n );\n}\n\n/** Whether this browser currently holds an active push subscription. */\nexport async function getExistingPushSubscription(): Promise<PushSubscription | null> {\n if (!pushSupported()) return null;\n const registration = await navigator.serviceWorker.getRegistration();\n if (!registration) return null;\n return registration.pushManager.getSubscription();\n}\n\n/** Register the SW and return this browser's (possibly new) subscription. */\nasync function obtainSubscription(\n swPath: string,\n vapidPublicKey: string,\n): Promise<PushSubscription> {\n const registration = await navigator.serviceWorker.register(swPath);\n await navigator.serviceWorker.ready;\n return (\n (await registration.pushManager.getSubscription()) ??\n registration.pushManager.subscribe({\n userVisibleOnly: true,\n applicationServerKey: base64UrlToUint8Array(vapidPublicKey) as BufferSource,\n })\n );\n}\n\n/**\n * Full enable flow: configured? → permission → SW registration → subscribe →\n * persist. Idempotent — an existing subscription is simply re-persisted (the\n * server upserts on the endpoint).\n */\n/** Persist the browser's subscription server-side (upsert on the endpoint). */\nasync function persist(\n api: NotificationsApiClient,\n subscription: PushSubscription,\n): Promise<PushSetupResult> {\n const json = subscription.toJSON();\n if (!json.endpoint || !json.keys?.p256dh || !json.keys.auth) {\n return { ok: false, reason: 'error' };\n }\n const saved = await api.savePushSubscription({\n endpoint: json.endpoint,\n keys: { p256dh: json.keys.p256dh, auth: json.keys.auth },\n });\n return saved.ok ? { ok: true } : { ok: false, reason: 'error' };\n}\n\nexport async function enableWebPush(\n api: NotificationsApiClient,\n swPath = '/sw.js',\n): Promise<PushSetupResult> {\n if (!pushSupported()) return { ok: false, reason: 'unsupported' };\n\n const registration = await api.getPushRegistration().catch(() => null);\n if (!registration?.vapidPublicKey) return { ok: false, reason: 'unconfigured' };\n\n const permission = await Notification.requestPermission();\n if (permission !== 'granted') return { ok: false, reason: 'permission-denied' };\n\n try {\n return await persist(\n api,\n await obtainSubscription(swPath, registration.vapidPublicKey),\n );\n } catch {\n return { ok: false, reason: 'error' };\n }\n}\n\n/** Disable flow: unsubscribe the browser and drop the server-side row. */\nexport async function disableWebPush(api: NotificationsApiClient): Promise<void> {\n const subscription = await getExistingPushSubscription();\n if (!subscription) return;\n const endpoint = subscription.endpoint;\n await subscription.unsubscribe().catch(() => false);\n await api.removePushSubscription(endpoint).catch(() => undefined);\n}\n","/**\n * How the notification screens reach their data (12-15) — the report-builder\n * transport doctrine: this is the ONLY way the surface performs I/O, so a\n * caller supplying one has substituted the entire backend without stubbing a\n * global. The default is same-origin `fetch` riding the browser's cookies.\n */\n\n/** A write outcome the screens branch on — never a thrown mutation. */\nexport type NotificationsResult<T> = { ok: true; data: T } | { ok: false; error: string };\n\n/** A failed read, carrying the status the screens branch on (401 = signed out). */\nexport class NotificationsHttpError extends Error {\n readonly status: number;\n constructor(status: number, message: string) {\n super(message);\n this.name = 'NotificationsHttpError';\n this.status = status;\n Object.setPrototypeOf(this, NotificationsHttpError.prototype);\n }\n}\n\nexport interface NotificationsTransport {\n /** A read. Returns the payload INSIDE the `{ data }` envelope. */\n get<T>(path: string): Promise<T>;\n /** A write. Returns a {@link NotificationsResult} rather than rejecting. */\n send<T>(path: string, method: string, body?: unknown): Promise<NotificationsResult<T>>;\n}\n\n/**\n * @param fallbackError What a failed write says when the server sent no\n * sentence of its own — REQUIRED, the host's words. `createWebNotifications`\n * already passes its (equally required) `messages.operationFailed`; only a\n * host constructing the transport directly writes it here. The old default\n * was one application's Portuguese, and the only string in this package the\n * required-messages port did not cover.\n */\nexport function httpNotificationsTransport(fallbackError: string): NotificationsTransport {\n return {\n async get<T>(path: string): Promise<T> {\n const response = await fetch(path, {\n credentials: 'same-origin',\n headers: { Accept: 'application/json' },\n });\n const payload = (await response.json().catch(() => null)) as\n | { data?: T; error?: string }\n | null;\n if (!response.ok) {\n throw new NotificationsHttpError(\n response.status,\n payload?.error ?? `HTTP ${response.status} for ${path}`,\n );\n }\n return (payload?.data ?? payload) as T;\n },\n\n async send<T>(path: string, method: string, body?: unknown): Promise<NotificationsResult<T>> {\n try {\n const response = await fetch(path, {\n method,\n credentials: 'same-origin',\n headers: {\n Accept: 'application/json',\n ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),\n },\n ...(body === undefined ? {} : { body: JSON.stringify(body) }),\n });\n if (response.status === 204) return { ok: true, data: undefined as T };\n const payload = (await response.json().catch(() => null)) as\n | { data?: T; error?: string }\n | null;\n if (!response.ok) return { ok: false, error: payload?.error ?? fallbackError };\n return { ok: true, data: (payload?.data ?? payload) as T };\n } catch {\n return { ok: false, error: fallbackError };\n }\n },\n };\n}\n","import { useState, type ComponentType, type JSX } from 'react';\n\nimport { messagesOf, type NotificationMessages } from '../messages';\n\nimport { createNotificationsApiClient, type NotificationsApiClient } from './api';\nimport { BellButton, type BellButtonProps } from './bell-button';\nimport {\n useUnreadCount,\n type NotificationsSignalHook,\n type NotificationsSubscribe,\n} from './hooks';\nimport { createInboxStore, type InboxStore } from './inbox-state';\nimport { NotificationsPanel, type NotificationsPanelProps } from './panel';\nimport { PreferencesScreen, type PreferencesScreenProps } from './preferences-screen';\nimport { httpNotificationsTransport, type NotificationsTransport } from './transport';\nimport type { WebPushSetupConfig } from './web-push-setup';\n\n/**\n * The one thing this package exposes to a FRONTEND host (12-15).\n *\n * Everything the notification centre IS — the bell with its live badge, the\n * slide-over inbox with its optimistic mark-read / delete / mark-all and its\n * cursor pager, the preferences matrix with its availability hints and the\n * per-browser push enable step, and every wire call between them — lives inside\n * this package. The host names where the API is mounted, and that is the whole\n * wiring.\n *\n * `page` is the standalone surface (the preferences screen), which is the one\n * thing a host routes to. The bell and the panel are a PAIR a host drops into\n * its own chrome, and they share one store, so a read in the panel moves the\n * badge in the same tick.\n */\n\nexport interface NotificationsWebConfig {\n /** The account mount the routes live under, e.g. `/api/account`. */\n apiBase: string;\n /** How the surface reaches its data. Default: same-origin fetch. */\n transport?: NotificationsTransport;\n /** User-facing copy overrides (pt-BR product copy by default). */\n messages: NotificationMessages;\n /**\n * How the surface learns an inbox changed without asking — the host's message\n * bus. Without it the badge keeps its 60 s poll, which is the standing\n * contract rather than a fallback: a dropped event must cost latency, never\n * correctness.\n */\n subscribe?: NotificationsSubscribe;\n /**\n * The same wiring as a HOOK, for a host whose realtime connection lives in\n * React context — see `NotificationsSignalHook`. `subscribe` is read at\n * factory time, which such a host cannot reach.\n */\n useSignal?: NotificationsSignalHook;\n /** The browser push enable step's host seams (SW path, platform hint). */\n webPush?: WebPushSetupConfig;\n}\n\nexport interface WebNotifications {\n /** The routed surface: the preferences screen. */\n page: ComponentType<PreferencesScreenProps>;\n /** The bell, already bound to the shared store. */\n BellButton: ComponentType<BellButtonProps>;\n /** The inbox slide-over, sharing that store. */\n Panel: ComponentType<NotificationsPanelProps>;\n /**\n * Bell + panel as ONE element, for a host that just wants the feature in its\n * header and does not want to own the open/closed state.\n */\n BellWithPanel: ComponentType<{\n enabled?: boolean;\n onNavigate?: (link: string) => void;\n }>;\n /** The badge number, for a host with its own trigger chrome. */\n useUnreadCount: (options?: { enabled?: boolean }) => number;\n /** The shared client state, for host glue. */\n store: InboxStore;\n /** The bound wire client. */\n api: NotificationsApiClient;\n /** The copy in force, so a host's own chrome can reuse a sentence. */\n messages: NotificationMessages;\n}\n\nexport function createWebNotifications(config: NotificationsWebConfig): WebNotifications {\n const messages = messagesOf(config);\n const api = createNotificationsApiClient(\n config.apiBase,\n config.transport ?? httpNotificationsTransport(messages.operationFailed),\n );\n const store = createInboxStore(api);\n const webPush = config.webPush ?? {};\n const subscribe = config.subscribe;\n const subscribeOption = {\n ...(subscribe ? { subscribe } : {}),\n ...(config.useSignal ? { useSignal: config.useSignal } : {}),\n };\n\n const Bell: ComponentType<BellButtonProps> = (props) => (\n <BellButton {...props} store={store} messages={messages} {...subscribeOption} />\n );\n const Panel: ComponentType<NotificationsPanelProps> = (props) => (\n <NotificationsPanel {...props} store={store} messages={messages} />\n );\n\n function useBoundUnreadCount(options: { enabled?: boolean } = {}): number {\n return useUnreadCount(store, { ...options, ...subscribeOption });\n }\n\n function BellWithPanel({\n enabled = true,\n onNavigate,\n }: {\n enabled?: boolean;\n onNavigate?: (link: string) => void;\n }): JSX.Element {\n const [open, setOpen] = useState(false);\n return (\n <>\n <Bell enabled={enabled} onClick={() => setOpen(true)} />\n <Panel\n open={open}\n onClose={() => setOpen(false)}\n {...(onNavigate ? { onNavigate } : {})}\n />\n </>\n );\n }\n\n return {\n page: (props) => (\n <PreferencesScreen {...props} api={api} messages={messages} webPush={webPush} />\n ),\n BellButton: Bell,\n Panel,\n BellWithPanel,\n useUnreadCount: useBoundUnreadCount,\n store,\n api,\n messages,\n };\n}\n","/**\n * Bare bell trigger with the live unread badge — for hosts that do not already\n * have a styled icon-button slot. A host with its own trigger chrome uses\n * `useUnreadCount` + `Panel` directly.\n */\nimport type { JSX } from 'react';\n\nimport { Badge } from '@12-apps/ui/data-display/Badge';\nimport { Box } from '@12-apps/ui/mui/Box';\n\nimport type { NotificationMessages } from '../messages';\n\nimport { BellIcon } from './bell-icon';\nimport { useUnreadCount, type NotificationsSignalHook, type NotificationsSubscribe } from './hooks';\nimport type { InboxStore } from './inbox-state';\n\nconst triggerSx = {\n display: 'inline-flex',\n alignItems: 'center',\n justifyContent: 'center',\n p: 0.5,\n border: 'none',\n background: 'none',\n cursor: 'pointer',\n color: 'text.primary',\n lineHeight: 0,\n '& *': { cursor: 'pointer' },\n '&:hover': { color: 'primary.main' },\n '&:focus-visible': {\n outline: '2px solid',\n outlineColor: 'primary.main',\n outlineOffset: '2px',\n borderRadius: '50%',\n },\n} as const;\n\nexport interface BellButtonProps {\n onClick: () => void;\n /** Signed-out hosts still mount the bell; `false` silences it. */\n enabled?: boolean;\n}\n\nexport function BellButton({\n onClick,\n enabled = true,\n store,\n messages,\n subscribe,\n useSignal,\n}: BellButtonProps & {\n store: InboxStore;\n messages: NotificationMessages;\n subscribe?: NotificationsSubscribe;\n useSignal?: NotificationsSignalHook;\n}): JSX.Element {\n const count = useUnreadCount(store, {\n enabled,\n ...(subscribe ? { subscribe } : {}),\n ...(useSignal ? { useSignal } : {}),\n });\n return (\n <Box\n component=\"button\"\n type=\"button\"\n onClick={onClick}\n aria-label={count > 0 ? messages.openBellWithUnread(count) : messages.openBell}\n data-testid=\"notifications-bell\"\n sx={triggerSx}\n >\n <Badge\n content={count > 0 ? count : undefined}\n color=\"primary\"\n variant=\"count\"\n max={99}\n data-testid=\"notifications-badge\"\n >\n <BellIcon size={28} />\n </Badge>\n </Box>\n );\n}\n","/**\n * The notification-centre slide-over: newest-first list with unread styling,\n * per-item open (marks read + deep-links), soft delete, mark-all, empty /\n * loading / error states and a \"load more\" cursor pager.\n *\n * Rendering is app-agnostic — the host passes `onNavigate` (its router's\n * navigate) for deep links. Without one a link is simply not followed, which is\n * what lets the panel mount in a host that has no router at all.\n */\nimport { useCallback, type JSX } from 'react';\n\nimport { EmptyState } from '@12-apps/ui/data-display/EmptyState';\nimport { LoadingState } from '@12-apps/ui/data-display/LoadingState';\nimport { Button } from '@12-apps/ui/form/Button';\nimport { Drawer, DrawerContent, DrawerHeader } from '@12-apps/ui/layout/Drawer';\nimport { Box } from '@12-apps/ui/mui/Box';\nimport { useMediaQuery } from '@12-apps/ui/mui/useMediaQuery';\nimport { useTheme } from '@12-apps/ui/mui/styles';\n\nimport type { NotificationMessages } from '../messages';\nimport type { InboxNotification } from '../wire';\n\nimport { BellIcon } from './bell-icon';\nimport { useInboxList } from './hooks';\nimport type { InboxState, InboxStore } from './inbox-state';\nimport { NotificationRow } from './row';\n\ninterface PanelBodyProps {\n state: InboxState;\n messages: NotificationMessages;\n onRetry: () => void;\n onLoadMore: () => void;\n onOpen: (notification: InboxNotification) => void;\n onDelete: (id: string) => void;\n}\n\n/** The scrollable panel body: loading / error / empty / the list + pager. */\nfunction PanelBody({\n state,\n messages,\n onRetry,\n onLoadMore,\n onOpen,\n onDelete,\n}: PanelBodyProps): JSX.Element {\n if (state.status === 'pending' || state.status === 'idle') {\n return (\n <LoadingState\n variant=\"spinner\"\n message={messages.loading}\n size=\"md\"\n dataTestId=\"notifications-loading\"\n />\n );\n }\n if (state.status === 'error') {\n return (\n <EmptyState\n variant=\"minimal\"\n title={messages.loadFailedTitle}\n description={messages.loadFailedBody}\n onRefresh={onRetry}\n refreshLabel={messages.retry}\n dataTestId=\"notifications-error\"\n />\n );\n }\n if (state.items.length === 0) {\n return (\n <EmptyState\n variant=\"illustrated\"\n illustration={<BellIcon size={44} dim />}\n title={messages.emptyTitle}\n description={messages.emptyBody}\n dataTestId=\"notifications-empty\"\n />\n );\n }\n return (\n <Box>\n {state.items.map((notification) => (\n <NotificationRow\n key={notification.id}\n notification={notification}\n messages={messages}\n onOpen={onOpen}\n onDelete={onDelete}\n />\n ))}\n {state.nextCursor ? (\n <Box sx={{ display: 'flex', justifyContent: 'center', py: 1.5 }}>\n <Button\n variant=\"outline\"\n color=\"neutral\"\n size=\"sm\"\n disabled={state.loadingMore}\n onClick={onLoadMore}\n dataTestId=\"notifications-load-more\"\n >\n {state.loadingMore ? messages.loadingMore : messages.loadMore}\n </Button>\n </Box>\n ) : null}\n </Box>\n );\n}\n\nexport interface NotificationsPanelProps {\n open: boolean;\n onClose: () => void;\n /** Navigate to a notification's in-app link (the host's router). */\n onNavigate?: (link: string) => void;\n}\n\nexport function NotificationsPanel({\n open,\n onClose,\n onNavigate,\n store,\n messages,\n}: NotificationsPanelProps & {\n store: InboxStore;\n messages: NotificationMessages;\n}): JSX.Element {\n // `useTheme` from @mui/material/styles falls back to the DEFAULT theme when\n // no provider is mounted, where the callback form of `useMediaQuery` would\n // hand the callback a null theme and throw. A published component must render\n // in a host that has not wrapped it yet.\n const theme = useTheme();\n const isMobile = useMediaQuery(theme.breakpoints.down('sm'));\n const state = useInboxList(store, open);\n\n const openNotification = useCallback(\n (notification: InboxNotification) => {\n if (notification.readAt === null) store.markRead([notification.id]);\n if (notification.link && onNavigate) {\n onClose();\n onNavigate(notification.link);\n }\n },\n [store, onClose, onNavigate],\n );\n\n const hasUnread = state.items.some((item) => item.readAt === null);\n\n return (\n <Drawer\n open={open}\n onClose={onClose}\n anchor=\"right\"\n variant=\"right\"\n width={isMobile ? '100vw' : 400}\n dataTestId=\"notifications-panel\"\n >\n <DrawerHeader onClose={onClose}>{messages.panelTitle}</DrawerHeader>\n <DrawerContent>\n {hasUnread ? (\n <Box sx={{ display: 'flex', justifyContent: 'flex-end', pb: 1 }}>\n <Button\n variant=\"ghost\"\n color=\"primary\"\n size=\"xs\"\n onClick={() => store.markAllRead()}\n dataTestId=\"notifications-mark-all-read\"\n >\n {messages.markAllRead}\n </Button>\n </Box>\n ) : null}\n <PanelBody\n state={state}\n messages={messages}\n onRetry={() => store.invalidate()}\n onLoadMore={() => store.loadMore()}\n onOpen={openNotification}\n onDelete={(id) => store.remove(id)}\n />\n </DrawerContent>\n </Drawer>\n );\n}\n","/** One inbox row: unread accent, content (opens/marks read), timestamp, delete. */\nimport type { JSX } from 'react';\n\nimport { Button } from '@12-apps/ui/form/Button';\nimport { Box } from '@12-apps/ui/mui/Box';\nimport { alpha, type Theme } from '@12-apps/ui/mui/styles';\nimport { Text } from '@12-apps/ui/typography/Text';\n\nimport type { NotificationMessages } from '../messages';\nimport type { InboxNotification } from '../wire';\n\nimport { relativeTime } from './relative-time';\n\nconst contentButtonSx = {\n flex: 1,\n minWidth: 0,\n display: 'flex',\n flexDirection: 'column',\n gap: 0.25,\n textAlign: 'left',\n border: 'none',\n background: 'none',\n p: 0,\n cursor: 'pointer',\n color: 'text.primary',\n fontFamily: 'inherit',\n} as const;\n\nconst unreadDotSx = {\n width: 8,\n height: 8,\n borderRadius: '50%',\n bgcolor: 'primary.main',\n flex: '0 0 auto',\n} as const;\n\nexport function NotificationRow({\n notification,\n messages,\n onOpen,\n onDelete,\n}: {\n notification: InboxNotification;\n messages: NotificationMessages;\n onOpen: (notification: InboxNotification) => void;\n onDelete: (id: string) => void;\n}): JSX.Element {\n const unread = notification.readAt === null;\n return (\n <Box\n data-testid={`notification-${notification.id}`}\n sx={{\n display: 'flex',\n alignItems: 'flex-start',\n gap: 1,\n py: 1.5,\n px: 1,\n borderBottom: '1px solid',\n borderColor: 'divider',\n bgcolor: unread ? (t: Theme) => alpha(t.palette.primary.main, 0.06) : 'transparent',\n }}\n >\n <Box\n component=\"button\"\n type=\"button\"\n onClick={() => onOpen(notification)}\n aria-label={\n unread ? `${notification.title} (${messages.unreadSuffix})` : notification.title\n }\n sx={contentButtonSx}\n >\n <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>\n {unread ? <Box aria-hidden sx={unreadDotSx} /> : null}\n <Text variant=\"body\" size=\"sm\" weight={unread ? 'bold' : 'medium'} as=\"span\">\n {notification.title}\n </Text>\n </Box>\n <Text variant=\"caption\" size=\"xs\" color=\"secondary\" as=\"span\">\n {notification.body}\n </Text>\n <Text variant=\"caption\" size=\"xs\" color=\"secondary\" as=\"span\" italic>\n {relativeTime(notification.createdAt, messages)}\n </Text>\n </Box>\n\n <Button\n variant=\"ghost\"\n color=\"neutral\"\n size=\"xs\"\n aria-label={messages.deleteOne(notification.title)}\n onClick={() => onDelete(notification.id)}\n dataTestId={`notification-delete-${notification.id}`}\n >\n ✕\n </Button>\n </Box>\n );\n}\n","/**\n * The notification-preferences screen: the category × channel matrix over\n * `GET/PUT <mount>/notification-preferences`.\n *\n * Toggles auto-save (optimistic, per change); channels that cannot reach the\n * user right now (no phone on file / channel not declared) render disabled with\n * a hint. Web Push additionally carries the per-BROWSER enable step, since a\n * preference alone cannot reach a device that never subscribed.\n */\nimport { useCallback, useEffect, useState, type JSX } from 'react';\n\nimport { LoadingState } from '@12-apps/ui/data-display/LoadingState';\nimport { Switch } from '@12-apps/ui/form/Switch';\nimport { Box } from '@12-apps/ui/mui/Box';\nimport { Text } from '@12-apps/ui/typography/Text';\n\nimport type { NotificationMessages } from '../messages';\nimport { NOTIFICATION_CHANNELS, type NotificationChannel } from '../types';\n\nimport type { NotificationsApiClient, PreferencesPayload } from './api';\nimport { WebPushDeviceSetup, type WebPushSetupConfig } from './web-push-setup';\n\ntype Availability = Record<NotificationChannel, boolean>;\n\n/** One category's row of channel switches. */\nfunction CategoryCard({\n category,\n channels,\n availability,\n messages,\n onToggle,\n}: {\n category: string;\n channels: Record<NotificationChannel, boolean>;\n availability: Availability;\n messages: NotificationMessages;\n onToggle: (channel: NotificationChannel, enabled: boolean) => void;\n}): JSX.Element {\n const labels = messages.categoryLabels[category];\n return (\n <Box\n sx={{ p: 2, border: '1px solid', borderColor: 'divider', borderRadius: 2 }}\n data-testid={`prefs-${category}`}\n >\n <Text variant=\"body\" size=\"sm\" weight=\"semibold\" as=\"p\">\n {labels?.title ?? messages.categoryFallbackTitle(category)}\n </Text>\n {labels?.description ? (\n <Text variant=\"caption\" size=\"xs\" color=\"secondary\" as=\"p\">\n {labels.description}\n </Text>\n ) : null}\n <Box\n sx={{\n mt: 1.5,\n display: 'grid',\n gridTemplateColumns: { xs: '1fr 1fr', sm: 'repeat(4, 1fr)' },\n gap: 1,\n }}\n >\n {NOTIFICATION_CHANNELS.map((channel) => (\n <Switch\n key={channel}\n size=\"sm\"\n color=\"primary\"\n label={messages.channelLabels[channel] ?? channel}\n // An unavailable channel reads OFF regardless of the stored choice:\n // a toggle that says \"on\" for a channel that cannot reach you is a\n // promise the pipeline will not keep.\n checked={channels[channel] && availability[channel]}\n disabled={!availability[channel]}\n onChange={(_, checked) => onToggle(channel, checked)}\n // `dataTestId`, not `data-testid`: the UI Switch puts this one on\n // the INPUT (and derives `-container` / `-label` from it), which is\n // the element a click and a `disabled` assertion need.\n dataTestId={`prefs-${category}-${channel}`}\n />\n ))}\n </Box>\n </Box>\n );\n}\n\n/** Why disabled toggles are disabled, one line per unavailable channel. */\nfunction UnavailableHints({\n availability,\n messages,\n}: {\n availability: Availability;\n messages: NotificationMessages;\n}): JSX.Element | null {\n const unavailable = NOTIFICATION_CHANNELS.filter((channel) => !availability[channel]);\n if (unavailable.length === 0) return null;\n return (\n <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>\n {unavailable.map((channel) => (\n <Text\n key={channel}\n variant=\"caption\"\n size=\"xs\"\n color=\"secondary\"\n as=\"p\"\n data-testid={`prefs-hint-${channel}`}\n >\n {messages.channelLabels[channel] ?? channel}:{' '}\n {messages.channelUnavailableHints[channel] ?? ''}\n </Text>\n ))}\n </Box>\n );\n}\n\nexport interface PreferencesScreenProps {\n /** Rendered under the lead paragraph — a \"back to account\" link, typically. */\n footer?: JSX.Element;\n}\n\n/** Optimistic per-toggle auto-save; a failed PUT takes the server's answer. */\nfunction usePreferences(api: NotificationsApiClient): {\n payload: PreferencesPayload | null;\n toggle: (category: string, channel: NotificationChannel, enabled: boolean) => void;\n} {\n const [payload, setPayload] = useState<PreferencesPayload | null>(null);\n\n useEffect(() => {\n let cancelled = false;\n void api\n .getPreferences()\n .then((next) => {\n if (!cancelled) setPayload(next);\n })\n .catch(() => undefined);\n return () => {\n cancelled = true;\n };\n }, [api]);\n\n const toggle = useCallback(\n (category: string, channel: NotificationChannel, enabled: boolean) => {\n setPayload((current) => {\n const row = current?.preferences[category];\n if (!current || !row) return current;\n return {\n ...current,\n preferences: { ...current.preferences, [category]: { ...row, [channel]: enabled } },\n };\n });\n const reconcile = (): void => {\n void api.getPreferences().then(setPayload).catch(() => undefined);\n };\n void api\n .savePreference(category, channel, enabled)\n .then((result) => {\n // The PUT answers with the whole matrix, so a success REPLACES the\n // optimistic guess with the server's own row — which is what catches a\n // save the server merged differently from the way the screen assumed.\n if (result.ok) setPayload(result.data);\n else reconcile();\n })\n // The packaged transport never rejects (it folds a failure into\n // `ok: false`), but a HOST transport may — and an unhandled rejection\n // would leave the toggle showing a choice the server never took.\n .catch(reconcile);\n },\n [api],\n );\n\n return { payload, toggle };\n}\n\nexport function PreferencesScreen({\n footer,\n api,\n messages,\n webPush,\n}: PreferencesScreenProps & {\n api: NotificationsApiClient;\n messages: NotificationMessages;\n webPush: WebPushSetupConfig;\n}): JSX.Element {\n const { payload, toggle } = usePreferences(api);\n\n if (!payload) {\n return (\n <LoadingState\n variant=\"spinner\"\n message={messages.loadingMore}\n size=\"md\"\n dataTestId=\"notification-prefs-loading\"\n />\n );\n }\n\n const { preferences, availability, categories } = payload;\n return (\n <Box\n component=\"section\"\n data-testid=\"notification-prefs-view\"\n sx={{\n maxWidth: 640,\n mx: 'auto',\n width: '100%',\n px: 2,\n py: 4,\n display: 'flex',\n flexDirection: 'column',\n gap: 2,\n }}\n >\n <Box>\n <Text variant=\"heading\" size=\"lg\" as=\"h1\">\n {messages.preferencesTitle}\n </Text>\n <Text variant=\"caption\" size=\"sm\" color=\"secondary\" as=\"p\">\n {messages.preferencesLead} {footer}\n </Text>\n </Box>\n <WebPushDeviceSetup\n available={availability.WEB_PUSH}\n api={api}\n messages={messages}\n config={webPush}\n />\n {categories.map((category) => (\n <CategoryCard\n key={category}\n category={category}\n channels={preferences[category] ?? {\n EMAIL: false,\n SMS: false,\n WHATSAPP: false,\n WEB_PUSH: false,\n }}\n availability={availability}\n messages={messages}\n onToggle={(channel, enabled) => toggle(category, channel, enabled)}\n />\n ))}\n <UnavailableHints availability={availability} messages={messages} />\n </Box>\n );\n}\n","/**\n * The per-BROWSER Web Push enable step, which sits above the preference matrix\n * because a preference alone cannot reach a device that never subscribed.\n */\nimport { useEffect, useState, type JSX } from 'react';\n\nimport { Button } from '@12-apps/ui/form/Button';\nimport { Box } from '@12-apps/ui/mui/Box';\nimport { Text } from '@12-apps/ui/typography/Text';\n\nimport type { NotificationMessages } from '../messages';\n\nimport type { NotificationsApiClient } from './api';\nimport { enableWebPush, getExistingPushSubscription } from './web-push-client';\n\n/** The panel's copy for a host whose platform blocks browser-level push. */\nexport interface WebPushPlatformHint {\n title: string;\n body: string;\n}\n\nexport interface WebPushSetupConfig {\n /**\n * The host's service-worker path. Path-routed SPAs each control their own\n * scope, and the file itself is the host's.\n */\n swPath?: string;\n /**\n * Whether THIS platform must be installed to the home screen before a\n * subscription can exist at all.\n *\n * iOS is the case: Safari has no browser-level Web Push, so \"Ativar\" there\n * asks no permission, creates no subscription, and fails with nothing a user\n * could act on. The check is a config seam rather than a dependency because\n * \"is this an installable iOS browser\" is a question a host's PWA layer\n * already answers (the origin passes\n * `() => isIosInstallable() && !isStandalone()` from `@12-apps/pwa`).\n */\n needsInstallFirst?: () => boolean;\n /** What to say instead of the button when the check above is true. */\n installHint?: WebPushPlatformHint;\n}\n\nconst cardSx = {\n p: 2,\n border: '1px solid',\n borderColor: 'divider',\n borderRadius: 2,\n} as const;\n\nfunction InstallFirstHint({ hint }: { hint: WebPushPlatformHint }): JSX.Element {\n return (\n <Box sx={cardSx} data-testid=\"web-push-install-hint\">\n <Text variant=\"body\" size=\"sm\" weight=\"semibold\" as=\"p\">\n {hint.title}\n </Text>\n <Text variant=\"caption\" size=\"xs\" color=\"secondary\" as=\"p\">\n {hint.body}\n </Text>\n </Box>\n );\n}\n\ntype SetupState = 'idle' | 'checking' | 'on' | 'busy' | 'failed' | 'denied';\n\n/**\n * \"Is this browser receiving alerts?\" — asked of the browser AND of the server.\n *\n * The browser alone is not enough, and answering from it alone is how a real\n * person is silenced while being told they are fine. Two users share a counter\n * PC: Ana enables push, then Otávio signs in on the same browser and enables it\n * too. `PushManager` hands out the SAME endpoint for the same browser profile,\n * so the POST re-owns the row and Ana's is gone. Ana signs back in, her browser\n * still holds the subscription object, and a browser-only check renders \"Este\n * navegador está recebendo alertas.\" with no button to fix it — forever. The\n * 404/410 prune reaches the same state from the other direction.\n *\n * So the server is asked whether it still has THIS endpoint under THIS user. A\n * `false` (or an unreachable server) shows *Ativar* again, and one click\n * re-registers the row — which is the same request the happy path makes, so the\n * recovery costs nothing to maintain.\n */\nasync function resolveState(api: NotificationsApiClient): Promise<SetupState> {\n const subscription = await getExistingPushSubscription();\n if (!subscription) return 'idle';\n const registration = await api\n .getPushRegistration({ endpoint: subscription.endpoint })\n .catch(() => null);\n return registration?.registered ? 'on' : 'idle';\n}\n\nfunction statusText(state: SetupState, messages: NotificationMessages): string {\n if (state === 'on') return messages.devicePushOn;\n if (state === 'denied') return messages.devicePushDenied;\n if (state === 'failed') return messages.devicePushFailed;\n return messages.devicePushIdle;\n}\n\nexport function WebPushDeviceSetup({\n available,\n api,\n messages,\n config,\n}: {\n available: boolean;\n api: NotificationsApiClient;\n messages: NotificationMessages;\n config: WebPushSetupConfig;\n}): JSX.Element | null {\n const [state, setState] = useState<SetupState>('checking');\n // Read once, at mount: neither can change without a new document.\n const [needsInstall] = useState(() => config.needsInstallFirst?.() ?? false);\n\n useEffect(() => {\n let cancelled = false;\n void resolveState(api).then((next) => {\n if (!cancelled) setState(next);\n });\n return () => {\n cancelled = true;\n };\n }, [api]);\n\n if (!available) return null;\n if (needsInstall && config.installHint) return <InstallFirstHint hint={config.installHint} />;\n\n const enable = async (): Promise<void> => {\n setState('busy');\n const result = await enableWebPush(api, config.swPath);\n if (result.ok) setState('on');\n else setState(result.reason === 'permission-denied' ? 'denied' : 'failed');\n };\n\n return (\n <Box\n sx={{\n ...cardSx,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n gap: 2,\n }}\n data-testid=\"web-push-device-setup\"\n >\n <Box sx={{ minWidth: 0 }}>\n <Text variant=\"body\" size=\"sm\" weight=\"semibold\" as=\"p\">\n {messages.devicePushTitle}\n </Text>\n <Text variant=\"caption\" size=\"xs\" color=\"secondary\" as=\"p\">\n {statusText(state, messages)}\n </Text>\n </Box>\n {state !== 'on' ? (\n <Button\n variant=\"outline\"\n color=\"primary\"\n size=\"sm\"\n disabled={state === 'busy' || state === 'checking'}\n onClick={() => void enable()}\n dataTestId=\"web-push-enable\"\n >\n {state === 'busy' ? messages.devicePushEnabling : messages.devicePushEnable}\n </Button>\n ) : null}\n </Box>\n );\n}\n"],"mappings":"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","names":["useState","Box","jsx","Box","Button","Box","Box","jsx","jsxs","Box","jsx","jsxs","Box","Button","useCallback","useEffect","useState","LoadingState","Box","Text","useEffect","Button","Box","Text","jsx","jsxs","Box","Text","useEffect","Button","jsx","jsxs","Box","Text","useState","useEffect","useCallback","LoadingState","jsx","jsxs","useState"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts","../src/messages.ts"],"sourcesContent":["/**\n * Core types of the channel-agnostic notification system (12-15).\n *\n * Three decoupled layers, each open for extension without touching the others:\n * - GENERATORS map a typed domain event to agnostic content (title/body/…).\n * - The CHANNEL ROUTER always writes the notification-centre inbox record,\n * then fans out one delivery per enabled channel.\n * - TRANSPORTS format the agnostic content for one channel and send it.\n *\n * Nothing here knows about a concrete channel's wire format — that lives\n * entirely inside each transport adapter — and nothing here knows about a\n * concrete DOMAIN either: the event `type` set, the preference categories and\n * the channel list are all host config (see {@link NotificationTaxonomy}).\n */\n\n/** Transport channels a notification can fan out to (DB CHECK mirrors this). */\nexport const NOTIFICATION_CHANNELS = ['EMAIL', 'SMS', 'WHATSAPP', 'WEB_PUSH'] as const;\nexport type NotificationChannel = (typeof NOTIFICATION_CHANNELS)[number];\n\n/**\n * The preference categories are the HOST's, and required.\n *\n * There used to be a `NOTIFICATION_CATEGORIES = ['orders','payments','stock',\n * 'system']` here — one product's set — and `taxonomyOf` fell back to it\n * whenever a host passed none. The docstring argued the case itself: \"it is\n * product vocabulary, not machinery\", and then shipped the vocabulary anyway as\n * the default, which is the only part a forgetful host would ever see.\n *\n * The consequence was quiet rather than loud: the settings screen renders four\n * rows a foreign host never chose, its own categories are absent, and every\n * preference a user sets is filed against a taxonomy nothing else in that\n * system uses. Nothing throws, because `category` is deliberately a free string\n * — the packaged migration puts **no CHECK** on it, precisely because a closed\n * set would be wrong for every host but the first. That freedom is what made\n * the default undetectable.\n *\n * `channel` and `status` are different and keep their CHECKs: those ARE this\n * library's own closed sets.\n */\nexport type NotificationCategory = string;\n\n/**\n * Per-channel delivery lifecycle (DB CHECK mirrors this).\n *\n * `SENDING` is the CLAIM: exactly one dispatcher moves a row out of `QUEUED`,\n * so two dispatchers can never both send the same delivery. A row left\n * `SENDING` is a dispatcher that died mid-send, and the sweep reclaims it once\n * it is older than the cutoff.\n *\n * `DEAD` is terminal: the attempt ceiling was reached (or the recipient no\n * longer exists), and no sweep will pick the row up again. Without it a\n * permanently invalid destination is a billed provider call on every sweep,\n * forever, and the sweep's working set only grows.\n */\nexport type DeliveryStatus = 'QUEUED' | 'SENDING' | 'SENT' | 'FAILED' | 'DEAD';\n\n/**\n * Channel-agnostic content a generator produces. This is what the inbox stores\n * verbatim and what every transport's formatter receives — no channel may leak\n * its wire format into it.\n */\nexport interface NotificationContent {\n title: string;\n body: string;\n /** In-app deep link (a same-origin path such as `/orders/123`). */\n link?: string;\n /** Structured extras for consumers that want more than text. */\n data?: Record<string, unknown>;\n}\n\n/** Who receives a notification. `clientId` scopes it to a tenant when set. */\nexport interface NotificationRecipient {\n userId: string;\n clientId?: string;\n}\n\n/**\n * A typed domain event handed to `notify`. `type` selects the registered\n * generator; `payload` is that generator's typed input. Callers never touch\n * channels, formatting, or preferences.\n */\nexport interface NotificationEvent<TPayload = unknown> {\n type: string;\n recipient: NotificationRecipient;\n payload: TPayload;\n}\n\n/**\n * Maps one domain event type to agnostic content. Registered through the\n * server config (or `registerGenerator` for a late arrival); adding a\n * generator never touches existing generators, the router, or any transport\n * (open/closed).\n */\nexport interface NotificationGenerator<TPayload = unknown> {\n /** The event key, dot-namespaced (\"order.paid\"). One generator per type. */\n type: string;\n /** The preference category the router gates this type's fan-out on. */\n category: NotificationCategory;\n generate: (payload: TPayload) => NotificationContent;\n}\n\n/**\n * The recipient as a transport sees them: resolved destinations only. Built by\n * the router from the host's contact directory + the push subscriptions this\n * package owns; transports use it to answer\n * {@link NotificationTransport.supports}.\n */\nexport interface TransportRecipient {\n userId: string;\n email: string | null;\n /** Phone as the host stores it (transports normalize per provider rules). */\n phone: string | null;\n /** How many active browser push subscriptions the user holds. */\n pushSubscriptionCount: number;\n}\n\n/**\n * One pluggable channel adapter: a FORMATTER (agnostic content → channel\n * message) plus a SENDER. Adding a channel = registering one of these; the\n * router dispatches through the registry and needs no change.\n *\n * `send` resolves on success and THROWS on failure — the router records the\n * error on the delivery row and isolates it from other channels. Sends must be\n * retry-safe: the router may re-dispatch a QUEUED/FAILED delivery.\n */\nexport interface NotificationTransport<TMessage = unknown> {\n channel: NotificationChannel;\n /**\n * Whether this recipient is addressable on this channel right now — the\n * destination exists (e-mail / phone / push subscription) AND the provider\n * is configured. `false` simply skips the channel (no delivery row).\n */\n supports(recipient: TransportRecipient): boolean;\n /** Transform the agnostic content into this channel's message shape. */\n format(content: NotificationContent): TMessage;\n /** Deliver the formatted message to the recipient. Throws on failure. */\n send(message: TMessage, recipient: TransportRecipient): Promise<void>;\n}\n\n/**\n * The host's product vocabulary. Everything below the surface (routing,\n * delivery rows, retries, the wire) is identical for every host; WHICH\n * categories exist and how they are labelled is not.\n */\nexport interface NotificationTaxonomy {\n /** The preference categories, in the order the settings screen lists them. */\n categories: readonly NotificationCategory[];\n}\n\n/**\n * The taxonomy in force. `categories` is REQUIRED — see above.\n *\n * The empty check was already here and stays: an empty list and a missing one\n * are the same mistake, and both now fail at assembly rather than rendering an\n * empty settings screen or somebody else's four rows.\n */\nexport function taxonomyOf(config: {\n categories: readonly NotificationCategory[];\n}): NotificationTaxonomy {\n const categories = config.categories;\n if (!categories || categories.length === 0) {\n throw new Error(\n '@12-apps/notifications: `categories` is required and must not be empty — ' +\n 'the preference categories are the host\\'s product vocabulary.',\n );\n }\n return { categories: [...categories] };\n}\n\n/** The host's logger. Defaults to the console (the @12-apps/jobs precedent). */\nexport interface NotificationLogger {\n info(message: string, ...meta: unknown[]): void;\n error(message: string, ...meta: unknown[]): void;\n}\n","/**\n * Every sentence this package can say to a USER, stated by the HOST.\n *\n * The copy lives in ONE table rather than in each screen so the api half and\n * the react half can never disagree about a sentence — the 401 body the wire\n * returns and the error the panel renders come from the same key.\n *\n * THE pt-BR TABLE THAT USED TO BE THE DEFAULT IS GONE. Its own docstring said\n * what it was: \"the product copy the surface shipped with\", labelled in the\n * source as one named application's \"exact copy\". A description of one adopter,\n * shipped inside the package every other adopter installs, and reached by\n * saying nothing.\n *\n * `categoryLabels` is the sharpest of the forty. The categories themselves\n * became required config in the release before this one, precisely because\n * WHICH categories exist is product vocabulary — and their LABELS kept\n * defaulting, so a host that declared `['loans', 'fines']` got a labels map\n * describing somebody else's four. Required categories with defaulted labels\n * for a different host's categories is not a smaller version of the bug; it is\n * the same bug with a compile-time gesture in front of it.\n *\n * So `messages` is REQUIRED and whole. The interface is the checklist, and the\n * compiler names the sentences a host has not written yet.\n */\n/**\n * The sentences the SERVER half renders — and the whole of what a backend mount\n * has to state.\n *\n * Split out when `messages` became required. Requiring the full forty on a\n * server config would have made a backend-only adopter write three dozen\n * sentences for screens it does not serve, which is the kind of tax that gets a\n * required-config migration reverted rather than adopted. These four are the\n * ones the router and the route descriptors actually put on a wire.\n */\nexport interface NotificationWireMessages {\n unauthenticated: string;\n invalidBody: string;\n operationFailed: string;\n /** `POST /notifications/mark-read` with neither `ids` nor `all`. */\n markReadTargetRequired: string;\n}\n\n/** Every sentence, wire and screen — what the REACT half needs. */\nexport interface NotificationMessages extends NotificationWireMessages {\n // --- the inbox panel -----------------------------------------------------\n panelTitle: string;\n markAllRead: string;\n loading: string;\n loadMore: string;\n loadingMore: string;\n loadFailedTitle: string;\n loadFailedBody: string;\n retry: string;\n emptyTitle: string;\n emptyBody: string;\n openBell: string;\n /** `(count) => 'Abrir notificações (3 não lidas)'`. */\n openBellWithUnread: (count: number) => string;\n unreadSuffix: string;\n deleteOne: (title: string) => string;\n\n // --- relative timestamps -------------------------------------------------\n justNow: string;\n minutesAgo: (minutes: number) => string;\n hoursAgo: (hours: number) => string;\n daysAgo: (days: number) => string;\n /** Locale for the fallback absolute date on rows older than a week. */\n dateLocale: string;\n\n // --- the preferences screen ---------------------------------------------\n preferencesTitle: string;\n preferencesLead: string;\n channelLabels: Record<string, string>;\n channelUnavailableHints: Record<string, string>;\n categoryLabels: Record<string, { title: string; description: string }>;\n /** Fallback title for a category the host added but did not label. */\n categoryFallbackTitle: (category: string) => string;\n devicePushTitle: string;\n devicePushIdle: string;\n devicePushOn: string;\n devicePushDenied: string;\n devicePushFailed: string;\n devicePushEnable: string;\n devicePushEnabling: string;\n}\n\n/**\n * What a copy field takes once its words can follow a reader.\n *\n * Declared here rather than imported from `@12-apps/i18n`: this package must\n * stay liftable into a repo that has never heard of it, so the two agree\n * STRUCTURALLY and nothing forces the dependency. The context is deliberately\n * loose — a raw tag off the wire, unnarrowed — because matching it is the host\n * resolver's job, not this package's.\n */\nexport type NotificationsCopyResolver<T> = (context: {\n readonly locale?: string | null;\n}) => T;\nexport type NotificationsCopySource<T> = T | NotificationsCopyResolver<T>;\n\n/**\n * The messages in force, for ONE reader.\n *\n * A pass-through rather than a merge: there is nothing left to merge WITH, and\n * that is the point of the change. The old version spread the host's table over\n * the origin's, including PER KEY inside `channelLabels`,\n * `channelUnavailableHints` and `categoryLabels` — so a host that relabelled one\n * channel kept the origin's wording for the other three, and a host that\n * labelled its own two categories kept the origin's four sitting beside them in\n * the same screen.\n *\n * Kept as a function because all three mounts read it off a config object, and\n * because a later rule (a blank-string refusal, say) belongs in one place —\n * which is exactly what made it the right place to put the RESOLUTION when the\n * field learned to take a resolver.\n *\n * **Call it where the sentence is used.** `createApiNotifications` runs once\n * per process, and at least one host memoises its call behind an `if\n * (assembled) return assembled;`, so a value read there answers every later\n * request in the language the process started with — and a single-locale host\n * cannot tell the difference. The route handlers call it per request; the\n * parsers below them keep taking a plain pack, so one request resolves exactly\n * once and no helper can disagree with another about the language.\n *\n * The generic survives the widening: a host whose pack carries extra keys of\n * its own still gets them back, resolver or not.\n */\nexport function messagesOf<T extends NotificationWireMessages>(\n config: { messages: NotificationsCopySource<T> },\n locale?: string,\n): T {\n const source = config.messages;\n return typeof source === 'function'\n ? (source as NotificationsCopyResolver<T>)({ locale })\n : source;\n}\n"],"mappings":";;;;;AAgBO,IAAM,wBAAwB,CAAC,SAAS,OAAO,YAAY,UAAU;AA4IrE,SAAS,WAAW,QAEF;AACvB,QAAM,aAAa,OAAO;AAC1B,MAAI,CAAC,cAAc,WAAW,WAAW,GAAG;AAC1C,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AACA,SAAO,EAAE,YAAY,CAAC,GAAG,UAAU,EAAE;AACvC;AAXgB;;;AC7BT,SAAS,WACd,QACA,QACG;AACH,QAAM,SAAS,OAAO;AACtB,SAAO,OAAO,WAAW,aACpB,OAAwC,EAAE,OAAO,CAAC,IACnD;AACN;AARgB;","names":[]}
|
|
File without changes
|
|
File without changes
|