@hermesihq/react 0.1.0 → 0.2.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/dist/index.d.ts CHANGED
@@ -1,330 +1,47 @@
1
+ import { HermsClient, HermsStoreHost, CountsState, HermsInboxReadStatus, HermsInboxItem, HermsPreferences, HermsPreferenceUpdate } from '@hermesihq/js';
2
+ export { HERMS_CHANNELS, HermsApiError, HermsCategoryPreference, HermsChannel, HermsChannelPreference, HermsClient, HermsClientOptions, HermsErrorDetail, HermsEventListener, HermsInboxCategory, HermsInboxCounts, HermsInboxItem, HermsInboxListParams, HermsInboxPage, HermsInboxReadStatus, HermsPreferenceUpdate, HermsPreferences, HermsRealtimeEvent, HermsRegisterChannelParams, decodeSubscriberTokenExp } from '@hermesihq/js';
1
3
  import * as react from 'react';
2
4
  import { ReactNode } from 'react';
3
5
 
4
6
  /**
5
- * Shared, framework-agnostic shapes for `HermsClient` and every React binding
6
- * built on top of it. Camel-cased on purpose. The wire (§6.6 `/v1/client/inbox/*`)
7
- * is snake_case; `HermsClient` is the one place that translation happens, the same
8
- * posture the dashboard's own hand-written API layer takes (`frontend/src/api/*.ts`)
9
- * until generated OpenAPI types exist for either surface.
7
+ * What every hook in this package reads from context. Exactly the shape the stores take
8
+ * as their host, so a hook hands this straight to the store it creates.
10
9
  */
11
- interface HermsClientOptions {
12
- /** `hm_pk_<env>_<random>`, safe to embed in a browser bundle (§6.2: "grant
13
- * nothing by themselves"). */
14
- publicKey: string;
15
- /** Base URL of the client API, e.g. `https://api.hermesi.dev/v1/client` or,
16
- * same-origin behind a dev proxy, `/v1/client`. No trailing slash required:
17
- * normalised internally. */
18
- apiBaseUrl: string;
19
- /**
20
- * Host-app-supplied. This SDK never has the secret key and never could
21
- * (F-INB-2: a subscriber token is minted server-side, by the host's own
22
- * backend). Called fresh before *every* request; never cached past what the
23
- * host itself returns, since refreshing ahead of expiry is explicitly the
24
- * host's job, not this client's (see `onTokenExpiring`).
25
- */
26
- getSubscriberToken: () => string | Promise<string>;
27
- /**
28
- * Called proactively, once, some seconds before the *currently held* token's
29
- * own `exp` claim (read back out of the token's payload segment client-side:
30
- * it's HMAC-signed, not encrypted, so this never requires verifying the
31
- * signature). This is a hook for the host app to refresh ahead of expiry rather
32
- * than reactively after a `401`. Best-effort: a token whose payload can't be
33
- * decoded (malformed, or a host app testing with a garbage string) simply
34
- * never schedules a call.
35
- */
36
- onTokenExpiring?: () => void;
37
- }
38
- type HermsInboxReadStatus = 'read' | 'unread';
39
- interface HermsInboxCategory {
40
- key: string;
41
- name: string;
42
- }
43
- interface HermsInboxItem {
44
- id: string;
45
- title: string;
46
- body: string;
47
- actionUrl: string | null;
48
- category: HermsInboxCategory | null;
49
- seenAt: string | null;
50
- readAt: string | null;
51
- createdAt: string;
52
- }
53
- interface HermsInboxCounts {
54
- unread: number;
55
- unseen: number;
56
- }
57
- interface HermsInboxPage {
58
- items: HermsInboxItem[];
59
- hasMore: boolean;
60
- nextCursor: string | null;
61
- }
62
- interface HermsInboxListParams {
63
- /** Maps to `GET /inbox?read=<bool>`: `'read'` -> `read=true`, `'unread'` ->
64
- * `read=false`, omitted -> both. */
65
- status?: HermsInboxReadStatus;
66
- /** Category key(s), `?category=` (repeatable server-side). */
67
- category?: string | string[];
68
- cursor?: string;
69
- limit?: number;
70
- }
71
- /**
72
- * Every channel Hermesi can hold an identity for.
73
- *
74
- * **Written out, and checked against the API by a test rather than trusted.** This
75
- * union said `'push' | 'sms' | 'email' | 'in_app'` while the platform had grown to
76
- * nine, so an integrator could not register a Telegram or Slack identity at all:
77
- * TypeScript refused it at their call site, and no test here or in the dashboard had
78
- * any reason to notice, because the demo page never registers a channel.
79
- *
80
- * It cannot be generated: this package is published standalone and must not depend on
81
- * the repository's OpenAPI snapshot at build time. So `channelParity.test.ts` compares
82
- * it to that snapshot's `Channel` enum instead. The list stays hand-written, and
83
- * drifting from the server is what fails.
84
- *
85
- * `push` is included because the API accepts it. No adapter delivers it today, which is
86
- * a deployment fact rather than a reason to refuse the identity.
87
- *
88
- * Exported as a value, not only a type: a host building a preference centre needs to
89
- * iterate the channels at runtime, and deriving the type from the array means the two
90
- * cannot disagree.
91
- */
92
- declare const HERMS_CHANNELS: readonly ["in_app", "email", "push", "sms", "whatsapp", "telegram", "slack", "teams", "discord"];
93
- type HermsChannel = (typeof HERMS_CHANNELS)[number];
94
- interface HermsRegisterChannelParams {
95
- channel: HermsChannel;
96
- identifier: string;
97
- metadata?: Record<string, unknown>;
98
- }
99
- /** One channel's setting, either globally or inside a category. `null` means the
100
- * subscriber has expressed no preference and the category's or platform's default
101
- * applies. It is not the same as `false`, and collapsing the two would silently
102
- * opt somebody out of something they never declined. */
103
- interface HermsChannelPreference {
104
- channel: HermsChannel;
105
- enabled: boolean | null;
106
- }
107
- interface HermsCategoryPreference {
108
- categoryId: string;
109
- key: string;
110
- name: string;
111
- /** A critical category is always delivered on every live channel and cannot be turned
112
- * off. Render it, but not as a control: an affordance that refuses is worse than
113
- * none. */
114
- isCritical: boolean;
115
- channels: HermsChannelPreference[];
116
- emailEnabled: boolean | null;
117
- inAppEnabled: boolean | null;
118
- }
119
- interface HermsPreferences {
120
- /** For addressing the subscriber in a preference page's own copy. `null` when the
121
- * application never supplied one. */
122
- firstName: string | null;
123
- /** Settings that apply across every category. */
124
- globalChannels: HermsChannelPreference[];
125
- /** `email` and `in_app` again, as scalars.
126
- *
127
- * Carried rather than dropped because the API publishes them, and dropping a
128
- * published field is the defect `HermsApiError` had. They are the same values as
129
- * the matching entries in `globalChannels`; read whichever suits, but do not treat
130
- * a disagreement between them as meaningful. */
131
- globalEmailEnabled: boolean | null;
132
- globalInAppEnabled: boolean | null;
133
- categories: HermsCategoryPreference[];
134
- }
135
- /** One setting to change. Omit `categoryId` to change the global setting for that
136
- * channel; supply it to change the setting inside one category. */
137
- interface HermsPreferenceUpdate {
138
- channel: HermsChannel;
139
- enabled: boolean;
140
- categoryId?: string;
141
- }
142
- interface HermsItemCreatedEvent {
143
- type: 'item.created';
144
- data: {
145
- id: string;
146
- title: string;
147
- created_at: string;
148
- };
149
- }
150
- interface HermsCountsChangedEvent {
151
- type: 'counts.changed';
152
- data: HermsInboxCounts;
153
- }
154
- type HermsRealtimeEvent = HermsItemCreatedEvent | HermsCountsChangedEvent;
155
- type HermsEventListener = (event: HermsRealtimeEvent) => void;
156
- /**
157
- * One specific problem inside an error.
158
- *
159
- * Open, not closed, because the API says so: most entries name a request field
160
- * (`field` + `issue`), some name a dependency instead, and its own schema declares
161
- * `additionalProperties` rather than publishing a shape a client would be right to
162
- * reject on. Typing it closed here would reintroduce exactly that.
163
- */
164
- type HermsErrorDetail = Record<string, unknown>;
165
- /** Thrown for any failed request to the client API. It mirrors the server's error
166
- * envelope where one was returned, and falls back to a generic shape when the body was
167
- * not JSON at all, which is usually an upstream proxy's own HTML error page.
168
- *
169
- * `detail` and `docUrl` were missing for a long time while the API published both as
170
- * required fields. A 422's per-field `detail` reached a consumer as a bare sentence,
171
- * and `doc_url` (the one thing that lets an integrator resolve an error without
172
- * asking anybody) never arrived at all. */
173
- declare class HermsApiError extends Error {
174
- readonly status: number;
175
- readonly code: string;
176
- readonly type: string;
177
- readonly requestId: string;
178
- /** Per-problem specifics. Possibly empty, never absent: the API's own words. */
179
- readonly detail: readonly HermsErrorDetail[];
180
- /** Documentation for this `code`, or `''` when the server sent none. */
181
- readonly docUrl: string;
182
- constructor(status: number, body: unknown);
183
- }
184
-
185
- declare class HermsClient {
186
- private readonly options;
187
- private readonly baseUrl;
188
- private expiryTimer;
189
- private lastScheduledExp;
190
- constructor(options: HermsClientOptions);
191
- /** Every request goes through here: always a *fresh* call to
192
- * `getSubscriberToken()` (never cached past what the host itself returns,
193
- * per this class's own contract), which also re-arms `onTokenExpiring`
194
- * against whatever token comes back. */
195
- private getFreshToken;
196
- private scheduleExpiryHook;
197
- /** Stops the scheduled `onTokenExpiring` timer. Called from
198
- * `HermsProvider`'s unmount cleanup so a torn-down client never fires the
199
- * hook after the app has stopped caring. */
200
- destroy(): void;
201
- /**
202
- * `expectsBody` is how this knows a missing response body is a failure rather than a
203
- * normal outcome. It defaults to `true` because most calls here read a payload; the
204
- * methods that resolve to `void` pass `false`.
205
- *
206
- * Without it the success path did no checking at all. A `200` carrying a proxy's HTML
207
- * page, or a `204` where a body was expected, left `parsed` null and returned it as
208
- * `T`, so `listInbox()` rejected with `TypeError: Cannot read properties of null
209
- * (reading 'data')` and the stack pointed at this SDK instead of at whatever was
210
- * actually in the way. The *error* path already handled exactly that case carefully;
211
- * only the success path did not.
212
- */
213
- private request;
214
- listInbox(params?: HermsInboxListParams): Promise<HermsInboxPage>;
215
- getCounts(): Promise<HermsInboxCounts>;
216
- markRead(id: string): Promise<HermsInboxItem>;
217
- markAllRead(): Promise<{
218
- updated: number;
219
- }>;
220
- markSeen(ids: string[]): Promise<{
221
- updated: number;
222
- }>;
223
- archive(id: string): Promise<HermsInboxItem>;
224
- delete(id: string): Promise<void>;
225
- registerChannel(params: HermsRegisterChannelParams): Promise<void>;
226
- /**
227
- * The subscriber's own notification settings, for rendering a preference centre.
228
- *
229
- * There is deliberately no `unsubscribe()` beside this, although the API exposes
230
- * `POST /v1/client/unsubscribe`. That route is the target of a `List-Unsubscribe`
231
- * one-click link (RFC 8058): its token is minted by Hermesi and embedded in an
232
- * outgoing email's headers, and the API's own description says it is invoked by a
233
- * mail client rather than by application code. Wrapping it here would invite a host
234
- * to build an unsubscribe button on a token it cannot obtain. The equivalent here
235
- * is `updatePreference({ channel: 'email', enabled: false })`.
236
- */
237
- getPreferences(): Promise<HermsPreferences>;
238
- /**
239
- * Change one setting, and receive the whole updated state back.
240
- *
241
- * The server answers `PATCH` with the full preferences rather than an
242
- * acknowledgement, so a caller never has to refetch, and never renders a view
243
- * assembled from its own optimistic guess plus whatever else has changed since.
244
- * Omit `categoryId` for the global setting; supply it to change one category.
245
- */
246
- updatePreference(update: HermsPreferenceUpdate): Promise<HermsPreferences>;
247
- /**
248
- * **Path parameters, not a query string.** This sent
249
- * `DELETE /channels?channel=...&identifier=...` for as long as the method has existed,
250
- * and there is no `DELETE` on `/v1/client/channels` at all: the route is
251
- * `/v1/client/channels/{channel}/{identifier}`, so every call 404'd. Nothing caught
252
- * it: the reference integration never deregisters, and the package had no tests.
253
- *
254
- * `identifier` is encoded because it is somebody's address (an email, a phone
255
- * number, a chat conversation id), and those carry `@`, `+` and `:`.
256
- */
257
- deregisterChannel(channel: HermsChannel, identifier: string): Promise<void>;
258
- /**
259
- * Opens a live connection for real-time inbox events and returns an
260
- * unsubscribe function. SSR-safe: `window`/`EventSource` are only read here,
261
- * at call time, never at module or constructor time.
262
- *
263
- * - Prefers SSE (`GET /inbox/stream`) when `EventSource` exists.
264
- * - Falls back to polling `getCounts()` every 60s (§6.6) when `EventSource`
265
- * doesn't exist at all, or after `MAX_FAST_RECONNECT_ATTEMPTS` consecutive
266
- * immediate reconnect failures (an invalid/expired token, or a network
267
- * that blocks SSE outright). It still retries SSE in the background with
268
- * exponential backoff rather than abandoning it permanently, so a fixed
269
- * token/network problem self-heals without a page reload.
270
- */
271
- subscribe(onEvent: HermsEventListener): () => void;
272
- }
273
-
274
- /**
275
- * Pure, dependency-free helpers for the *reading* half of F-INB-2's subscriber
276
- * token (§6.2). `HermsClient` never mints or verifies a signature (it never has
277
- * the secret key hash to do either), it only needs to read the `exp` claim back
278
- * out of a token the host app already handed it, to drive `onTokenExpiring`.
279
- *
280
- * Token shape (verbatim, `hermesi.core.security`'s "F-INB-2 subscriber token"
281
- * section): `base64url(payload) + "." + base64url(hmac_sha256(...))`, where
282
- * `payload` is a plain JSON object `{sub, env, exp}`. No JOSE/JWT header segment.
283
- */
284
- /**
285
- * Returns the token's `exp` claim (unix seconds) or `null` for anything that
286
- * isn't a well-formed `<payload>.<signature>` token with a numeric `exp`. A
287
- * malformed value never throws, it just means `onTokenExpiring` won't be
288
- * scheduled for this particular token.
289
- */
290
- declare function decodeSubscriberTokenExp(token: string): number | null;
291
-
292
- interface HermsContextValue {
293
- client: HermsClient;
294
- /** Registers `listener` against the *one* `client.subscribe(...)` connection
295
- * this provider owns for the whole subtree, returning an unsubscribe
296
- * function. Every `useUnreadCount`/`useInbox` consumer calls this instead
297
- * of opening its own `EventSource`/poll loop. */
298
- addEventListener: (listener: HermsEventListener) => () => void;
299
- }
10
+ type HermsContextValue = HermsStoreHost;
300
11
  interface HermsProviderProps {
301
12
  client: HermsClient;
302
13
  children: ReactNode;
303
14
  }
304
15
  /**
305
- * `<HermsProvider client={new HermsClient({...})}>`: one `HermsClient`
306
- * instance for the whole subtree. Opens the client's real-time subscription
307
- * once on mount and tears it (and the client's own `onTokenExpiring` timer)
308
- * down on unmount; every listener registered via `addEventListener` below
309
- * shares that single connection rather than each hook paying for its own.
16
+ * `<HermsProvider client={new HermsClient({...})}>`: one `HermsClient` instance for the
17
+ * whole subtree. Opens the client's real-time subscription once on mount and tears it
18
+ * (and the client's own `onTokenExpiring` timer) down on unmount; every listener
19
+ * registered via `addEventListener` shares that single connection rather than each hook
20
+ * paying for its own.
21
+ *
22
+ * The connection itself is `HermsSession`, which knows nothing about React: this
23
+ * component is the few lines that tie its lifetime to a mount. That is deliberate, and it
24
+ * is the same session a Vue or vanilla page uses.
310
25
  *
311
26
  * `client` is expected to be a stable reference (constructed once, e.g. in
312
- * `useMemo`/module scope in the host app). Passing a new instance on every
313
- * render tears down and reopens the underlying connection every render.
27
+ * `useMemo`/module scope in the host app). Passing a new instance on every render tears
28
+ * down and reopens the underlying connection every render.
314
29
  */
315
30
  declare function HermsProvider({ client, children }: HermsProviderProps): react.JSX.Element;
316
31
  /** Every hook/component in this package throws this same error outside a
317
32
  * `<HermsProvider>`: there is no sensible default client to fall back to. */
318
33
  declare function useHermsContext(): HermsContextValue;
319
34
 
320
- interface UseUnreadCountResult extends HermsInboxCounts {
321
- isLoading: boolean;
322
- }
35
+ type UseUnreadCountResult = CountsState;
323
36
  /**
324
- * Headless. `{unread, unseen, isLoading}`, kept live via the provider's
325
- * shared subscription: one `getCounts()` call on mount, then recomputed on
326
- * every `counts.changed` event (real-time push, or the client's own 60s
327
- * polling fallback) for as long as this component stays mounted.
37
+ * Headless. `{unread, unseen, isLoading}`, kept live via the provider's shared
38
+ * subscription: one `getCounts()` call on mount, then recomputed on every
39
+ * `counts.changed` event (real-time push, or the client's own 60s polling fallback) for
40
+ * as long as this component stays mounted.
41
+ *
42
+ * A binding over `CountsStore`, which holds all of that. The store's constructor does
43
+ * nothing, so React discarding a memoised one costs nothing: it is the `connect()` in the
44
+ * effect that starts work.
328
45
  */
329
46
  declare function useUnreadCount(): UseUnreadCountResult;
330
47
 
@@ -346,15 +63,20 @@ interface UseInboxResult {
346
63
  refetch: () => void;
347
64
  }
348
65
  /**
349
- * Headless, cursor-paginated inbox list state: thin wrappers around
350
- * `HermsClient`'s own methods that also patch local state, so a consumer
351
- * never has to manually refetch the whole list after every mutating action.
66
+ * Headless, cursor-paginated inbox list state: thin wrappers around `HermsClient`'s own
67
+ * methods that also patch local state, so a consumer never has to manually refetch the
68
+ * whole list after every mutating action.
352
69
  *
353
- * Live-updated on `item.created`: the new item is prepended immediately from
354
- * the (deliberately minimal: `{id, title, created_at}`, §6.6) SSE payload so
355
- * the UI reacts instantly, then reconciled against a fresh first page fetched
356
- * in the background so the authoritative row (body, category, action_url)
357
- * lands without a full list reload/flicker.
70
+ * Live-updated on `item.created`: the new item is prepended immediately from the
71
+ * (deliberately minimal: `{id, title, created_at}`) SSE payload so the UI reacts
72
+ * instantly, then reconciled against a fresh first page fetched in the background so the
73
+ * authoritative row (body, category, action_url) lands without a full list
74
+ * reload/flicker.
75
+ *
76
+ * All of that lives in `InboxStore`; this is the binding. The filter is handed to the
77
+ * store rather than being part of what identifies it, so changing tabs reloads the list
78
+ * in place, keeping the rows on screen, instead of discarding the store and blanking the
79
+ * panel.
358
80
  */
359
81
  declare function useInbox(options?: UseInboxOptions): UseInboxResult;
360
82
 
@@ -374,20 +96,22 @@ interface UsePreferencesResult {
374
96
  * Headless preferences, the counterpart to `useInbox`.
375
97
  *
376
98
  * **No optimistic update, deliberately.** A toggle here is not a like button: turning
377
- * off a channel is a consent decision, and showing it as done before the server agreed
378
- * is showing somebody they have opted out when they may not have. The server answers
379
- * `PATCH` with the *whole* updated state, so the correct view arrives with the write
380
- * and there is nothing to reconcile. A per-row pending state in the host is a better
381
- * answer to the latency than a lie.
99
+ * off a channel is a consent decision, and showing it as done before the server agreed is
100
+ * showing somebody they have opted out when they may not have. The server answers `PATCH`
101
+ * with the *whole* updated state, so the correct view arrives with the write and there is
102
+ * nothing to reconcile. A per-row pending state in the host is a better answer to the
103
+ * latency than a lie.
382
104
  *
383
- * **`error` is state, not an exception.** `setPreference` still rejects, so a caller
384
- * can `await` it and react per-control, but the hook also holds the failure so a
385
- * preference centre can render it without every consumer writing the same try/catch.
386
- * A failed write leaves the last known-good state on screen rather than an empty page.
105
+ * **`error` is state, not an exception.** `setPreference` still rejects, so a caller can
106
+ * `await` it and react per-control, but the hook also holds the failure so a preference
107
+ * centre can render it without every consumer writing the same try/catch. A failed write
108
+ * leaves the last known-good state on screen rather than an empty page.
387
109
  *
388
110
  * Critical categories arrive with `isCritical: true` and are always delivered. Render
389
111
  * them (a subscriber should see what they are receiving), but not as a control: an
390
112
  * affordance that refuses is worse than none.
113
+ *
114
+ * A binding over `PreferencesStore`, which holds the behaviour above.
391
115
  */
392
116
  declare function usePreferences(): UsePreferencesResult;
393
117
 
@@ -395,11 +119,11 @@ declare function usePreferences(): UsePreferencesResult;
395
119
  * `<HermsInbox />`'s own tiny, self-contained EN/FR string table.
396
120
  *
397
121
  * Deliberately **not** i18next: this package ships to arbitrary host apps
398
- * (§8.5: "zero hard dependency on the host app's state library") that may run
122
+ * (with zero hard dependency on the host app's state library) that may run
399
123
  * their own i18n stack, a different one, or none at all. Pulling i18next in
400
124
  * as a real dependency here would be exactly the kind of host-app-shaped
401
125
  * assumption the SDK is supposed to avoid, for a handful of short strings.
402
- * `locale` mirrors §8.5's own `<HermsProvider locale="fr">` example prop.
126
+ * `locale` is a prop of `<HermsInbox />`.
403
127
  */
404
128
  type HermsLocale = 'en' | 'fr';
405
129
 
@@ -412,7 +136,7 @@ interface HermsInboxProps {
412
136
  /** Popover position relative to the bell trigger. Default `bottom-end`. */
413
137
  placement?: HermsInboxPlacement;
414
138
  /** Called when an item is activated (click, or Enter on a focused row).
415
- * Lets a host app drive its own router (§8.5's own example:
139
+ * Lets a host app drive its own router (for example
416
140
  * `onItemClick={(item) => navigate(item.actionUrl)}`) instead of a hard
417
141
  * page navigation. When omitted, activating an item with a non-null
418
142
  * `actionUrl` does a plain `window.location.assign` to it. Either way, the
@@ -430,7 +154,7 @@ interface HermsInboxProps {
430
154
  className?: string;
431
155
  }
432
156
  /**
433
- * `<HermsInbox />`: bell trigger + dropdown panel (§4.8/F-INB-5). Built
157
+ * `<HermsInbox />`: bell trigger + dropdown panel. Built
434
158
  * entirely on the headless `useInbox`/`useUnreadCount` hooks; every state
435
159
  * (loading, empty, error, populated) renders here, none of it invented by a
436
160
  * host app. Accessible: a real `<button>` trigger with `aria-label`/
@@ -441,4 +165,4 @@ interface HermsInboxProps {
441
165
  */
442
166
  declare function HermsInbox({ placement, onItemClick, theme, colorScheme, locale, className }: HermsInboxProps): react.JSX.Element;
443
167
 
444
- export { HERMS_CHANNELS, HermsApiError, type HermsCategoryPreference, type HermsChannel, type HermsChannelPreference, HermsClient, type HermsClientOptions, type HermsErrorDetail, type HermsEventListener, HermsInbox, type HermsInboxCategory, type HermsInboxCounts, type HermsInboxItem, type HermsInboxListParams, type HermsInboxPage, type HermsInboxPlacement, type HermsInboxProps, type HermsInboxReadStatus, type HermsInboxTheme, type HermsLocale, type HermsPreferenceUpdate, type HermsPreferences, HermsProvider, type HermsProviderProps, type HermsRealtimeEvent, type HermsRegisterChannelParams, type UseInboxOptions, type UseInboxResult, type UsePreferencesResult, type UseUnreadCountResult, decodeSubscriberTokenExp, useHermsContext, useInbox, usePreferences, useUnreadCount };
168
+ export { HermsInbox, type HermsInboxPlacement, type HermsInboxProps, type HermsInboxTheme, type HermsLocale, HermsProvider, type HermsProviderProps, type UseInboxOptions, type UseInboxResult, type UsePreferencesResult, type UseUnreadCountResult, useHermsContext, useInbox, usePreferences, useUnreadCount };