@colixsystems/widget-sdk 0.47.0 → 0.48.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/README.md CHANGED
@@ -13,7 +13,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
13
13
 
14
14
  **Wire / casing: snake_case end to end.** The clients send and return snake_case **verbatim** (`created_at`, `group_ids`, `can_read`, `amount_cents`, `data_type`, `is_active`, …). There is **no case transform anywhere** — not on the client and not in the backend; the only casing boundary is Prisma `@map` (snake_case field → camelCase column). Author-defined record column values pass through verbatim. Every `list(...)` returns the `{ data, meta }` envelope; the read hooks unwrap `res.data` for you.
15
15
 
16
- **Hooks read the injected clients** — they do not hold their own HTTP. This is the **complete** hook surface (19 hooks), grouped by the domain client each one reads. **CORE** hooks read host state directly off `WidgetContext` (no data client); the rest delegate to one of the four injected clients. The grouping mirrors the banner sections in [`src/hooks.js`](src/hooks.js).
16
+ **Hooks read the injected clients** — they do not hold their own HTTP. This is the **complete** hook surface (20 hooks), grouped by the domain client each one reads. **CORE** hooks read host state directly off `WidgetContext` (no data client); the rest delegate to one of the four injected clients. The grouping mirrors the banner sections in [`src/hooks.js`](src/hooks.js).
17
17
 
18
18
  | Group | Hook (signature) | Returns | Reads / scope |
19
19
  | ----- | ---------------- | ------- | ------------- |
@@ -40,6 +40,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
40
40
  | **DIRECTORY** | `useGroups(query?)` | `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` | `directory.groups.*` — `groups.read:*` (mutations also `groups.write:*`) |
41
41
  | **DIRECTORY** | `useBankIdLink()` | `{ linked, available, status, qr, message, startLink, refresh, cancel, unlink, refetchStatus, … }` | `directory.bankid.*` — no scope (JWT-gated self-service) |
42
42
  | **PAYMENTS** (`ctx.payments`) | `usePayments()` | `{ requestPayment, getPayment }` | `ctx.payments.*` — `payments.charge:appUser` |
43
+ | **NOTIFICATIONS** (`ctx.notifications`) | `useSendNotification()` | `{ send, sending, error }` | `ctx.notifications.send` — `notifications.send:appUser`. `send({ recipient_user_id, title, body, link?, payload? })` notifies one app user in the same workspace; call from an event handler (never render); rejects with `NotificationError`. |
43
44
 
44
45
  All list calls return the `{ data, meta }` envelope; the read hooks unwrap `res.data` for you. There is no `useWorkspace()` or `useLogger()` hook — read the theme via `useTheme()` and the locale via `useI18n()`; the host logger lives on `ctx.logger` (`{ debug, info, warn, error }`).
45
46
 
@@ -49,7 +50,11 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
49
50
 
50
51
  ## Status
51
52
 
52
- `v0.47.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
53
+ `v0.48.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
54
+
55
+ ### What's new in 0.48.0
56
+
57
+ **New `useSendNotification()` hook — send an in-app notification to an app user (sc-890).** A new NOTIFICATIONS hook reading a newly-injected `ctx.notifications` slice (the `@colixsystems/notifications-client`, now constructed by both the web Player and the native Expo export). Returns `{ send, sending, error }`. `send({ recipient_user_id, title, body, link?, payload? })` posts a notification to one app user **in the same workspace** and resolves to the created row; `recipient_user_id` must be a member of the tenant or the call is rejected (cross-workspace targets never resolve). Call it from an **event handler** (a `Pressable.onPress`, a submit, a mutation callback) — never in render, where the abuse/rate-limit guard would fire on every paint. Gated by the new `notifications.send:appUser` scope, which the widget declares in its manifest `requestedScopes`. Rejections surface as a structured `NotificationError` (new named export) with a stable `.code` (`INVALID_TITLE` / `INVALID_BODY` / `INVALID_RECIPIENT` / `INVALID_PAYLOAD` / `VALIDATION` / `AUTH_REQUIRED` / `FORBIDDEN` / `RECIPIENT_NOT_FOUND` / `RATE_LIMITED` / `INTERNAL`). `CONTRACT.version` → `1.35.0`. Additive — one new hook, one new context slice, one new scope, one new error class; no existing export changed signature.
53
58
 
54
59
  ### What's new in 0.47.0
55
60
 
@@ -396,7 +401,7 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
396
401
 
397
402
  - `defineWidget({ manifest, component })` — validates the manifest and produces a widget module the host can register.
398
403
  - `validateManifest(m)` / `validatePropertySchema(s)` / `validateProps(schema, props)` — shape validation; no third-party deps.
399
- - `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreSchema`, `useDatastoreMutation`, `useDirectory`, `useUsers`, `useGroups`, `useRecordPermissions`, `useAsset`, `useWidgetEvent`, `usePayments`, `useTheme`, `useI18n`, `useUser`, `useNavigation`, `useChildRenderer`, `useClipboard`, `useToast` — hooks that read from the host-provided `WidgetContext` (or, for `useClipboard`, the platform clipboard API directly). `useDirectory(query?)` returns `{ users, loading, error, refetch }` (each user `{ id, name, role }`) and requires the `directory.read:users` scope. `useUsers(query?)` returns `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }` and requires `users.read:*` (mutations also need `users.write:*`); rejections are a `DirectoryError`. `useGroups(query?)` returns `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` and requires `groups.read:*` (mutations also need `groups.write:*`). `usePayments()` returns `{ requestPayment, getPayment }` and requires the `payments.charge:appUser` scope; `requestPayment(...)` rejects with a `PaymentError`. `useUser()` returns the active end-user identity `{ id, email, display_name, roles, group_ids }` (snake_case verbatim; `id` is `null` for anonymous / preview). `useNavigation()` returns `{ goTo, goBack, push, replace, back, currentRoute }` for internal page navigation — for external URLs use the `Linking` primitive (`Linking.openURL(url)`). `useDatastoreRecord(tableId, recordId)` returns `{ data, loading, error, refetch }` for a single record (data is one row or null). `useDatastoreSchema(tableId)` returns `{ schema, loading, error, refetch }` where `schema` is `{ id, name, columns: [{ id, name, data_type, required, relation_type, target_table_id, is_identification }] }` (structure only, no row data; snake_case verbatim) — use it to resolve a stored `columnId` to its column type at runtime; requires the `datastore.read:<table>` scope. `useAsset(fileId)` returns `{ url, file, loading, error, refetch }` — the `url` is an absolute URL composed against the host's API base. `useChildRenderer()` returns `{ renderNode(node) }` — container widgets call it to render arbitrary child page-tree nodes (prefer the `WidgetTree` component for the common case).
404
+ - `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreSchema`, `useDatastoreMutation`, `useDirectory`, `useUsers`, `useGroups`, `useRecordPermissions`, `useAsset`, `useWidgetEvent`, `usePayments`, `useSendNotification`, `useTheme`, `useI18n`, `useUser`, `useNavigation`, `useChildRenderer`, `useClipboard`, `useToast` — hooks that read from the host-provided `WidgetContext` (or, for `useClipboard`, the platform clipboard API directly). `useDirectory(query?)` returns `{ users, loading, error, refetch }` (each user `{ id, name, role }`) and requires the `directory.read:users` scope. `useUsers(query?)` returns `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }` and requires `users.read:*` (mutations also need `users.write:*`); rejections are a `DirectoryError`. `useGroups(query?)` returns `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` and requires `groups.read:*` (mutations also need `groups.write:*`). `usePayments()` returns `{ requestPayment, getPayment }` and requires the `payments.charge:appUser` scope; `requestPayment(...)` rejects with a `PaymentError`. `useSendNotification()` returns `{ send, sending, error }` and requires the `notifications.send:appUser` scope; `send({ recipient_user_id, title, body, link?, payload? })` notifies one app user in the same workspace (cross-workspace `recipient_user_id` is rejected), must be called from an event handler rather than render, and rejects with a `NotificationError`. `useUser()` returns the active end-user identity `{ id, email, display_name, roles, group_ids }` (snake_case verbatim; `id` is `null` for anonymous / preview). `useNavigation()` returns `{ goTo, goBack, push, replace, back, currentRoute }` for internal page navigation — for external URLs use the `Linking` primitive (`Linking.openURL(url)`). `useDatastoreRecord(tableId, recordId)` returns `{ data, loading, error, refetch }` for a single record (data is one row or null). `useDatastoreSchema(tableId)` returns `{ schema, loading, error, refetch }` where `schema` is `{ id, name, columns: [{ id, name, data_type, required, relation_type, target_table_id, is_identification }] }` (structure only, no row data; snake_case verbatim) — use it to resolve a stored `columnId` to its column type at runtime; requires the `datastore.read:<table>` scope. `useAsset(fileId)` returns `{ url, file, loading, error, refetch }` — the `url` is an absolute URL composed against the host's API base. `useChildRenderer()` returns `{ renderNode(node) }` — container widgets call it to render arbitrary child page-tree nodes (prefer the `WidgetTree` component for the common case).
400
405
  - `WidgetTree({ node })` — component that renders an author-authored child node through the host's renderer; used by Tabs / Card / custom containers to host arbitrary child widgets.
401
406
  - `Text`, `View`, `Pressable`, `Image`, `ScrollView`, `TextInput`, `FlatList`, `SectionList`, `ActivityIndicator`, `Switch`, `StyleSheet`, `Linking`, `Icon`, `DateTimePicker` — re-exported from `react-native` (the RN primitives) or implemented in the SDK (`Icon` wraps `lucide-react-native`; `DateTimePicker` wraps `@react-native-community/datetimepicker` on native and renders `<input type="date|time|datetime-local">` directly on web because the RN library has no react-native-web mapping). The web build aliases `react-native` to `react-native-web` so the RN-re-exported primitives render in the browser without any per-platform code; the exported Expo app's Metro bundler resolves the real `react-native` library. `Linking` is a static API (`Linking.openURL(url)`) — use it for external URLs, and use `useNavigation().goTo(pageId)` for internal page navigation. See https://reactnative.dev/docs/ for per-component props.
402
407
  - `WidgetContextProvider` — React context provider that the host (Studio, Player, exported app) wraps widgets with.
package/dist/contract.cjs CHANGED
@@ -420,6 +420,22 @@ const HOOKS = [
420
420
  requiredContextSlice: ["payments.requestPayment"],
421
421
  scopes: ["payments.charge:appUser"],
422
422
  },
423
+ // sc-890 — send an in-app notification to one app user in the tenant.
424
+ // IMPERATIVE: send() never fires on mount; the widget calls it from an
425
+ // event handler. Reads ctx.notifications.send (the injected
426
+ // @colixsystems/notifications-client). Requires the
427
+ // notifications.send:appUser scope.
428
+ {
429
+ name: "useSendNotification",
430
+ signature: "useSendNotification()",
431
+ returnShape: {
432
+ send: "({ recipient_user_id, title, body, link?, payload? }) => Promise<notification> // rejects with NotificationError",
433
+ sending: "boolean",
434
+ error: "NotificationError | null",
435
+ },
436
+ requiredContextSlice: ["notifications.send"],
437
+ scopes: ["notifications.send:appUser"],
438
+ },
423
439
  // REQ-USERMGMT / REQ-ACL-SYS M3 — AppUser administration. Returns
424
440
  // `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }`.
425
441
  // Reads need `users.read:*` scope; edit-style mutations (invite /
@@ -1026,6 +1042,13 @@ const WIDGET_CONTEXT_SHAPE = {
1026
1042
  required: true,
1027
1043
  fields: { requestPayment: "function", getPayment: "function" },
1028
1044
  },
1045
+ // sc-890 — backs useSendNotification(). Mirror of contract.js.
1046
+ notifications: {
1047
+ description:
1048
+ "Injected @colixsystems/notifications-client instance (sc-890). { send(body) -> Promise<notification> }. Backs useSendNotification(); requires the notifications.send:appUser scope. The host POSTs the body snake_case verbatim ({ recipient_user_id, title, body, link?, payload? }) to /notifications/send and returns the created notification row; the call is imperative (a send never fires on mount).",
1049
+ required: true,
1050
+ fields: { send: "function" },
1051
+ },
1029
1052
  // REQ-WSDK-DOMAIN-CLIENTS — the AppUser administration, AppUserGroup
1030
1053
  // administration, and per-record VirtualPermission facades that used to
1031
1054
  // live here (`users`, `groups`, `recordPermissions`) were folded into the
@@ -1744,7 +1767,18 @@ const CONTRACT = deepFreeze({
1744
1767
  // type }` shape the native picker returns, so the same hook code path
1745
1768
  // feeds the multipart in both hosts. No existing hook, primitive, or
1746
1769
  // manifest field changed shape — minor bump.
1747
- version: "1.34.0",
1770
+ //
1771
+ // 1.35.0: additive (sc-890) — new `useSendNotification()` hook reading the
1772
+ // newly-injected `ctx.notifications` (@colixsystems/notifications-client).
1773
+ // Returns `{ send, sending, error }`; `send({ recipient_user_id, title,
1774
+ // body, link?, payload? })` POSTs to `/notifications/send` and resolves to
1775
+ // the created notification row, rejecting with a structured
1776
+ // NotificationError. Imperative — never fires on mount. New required field
1777
+ // on the new `notifications` context slice (`send: "function"`) + the new
1778
+ // `notifications.send:appUser` scope it gates on. No existing hook,
1779
+ // primitive, manifest field, or token changed shape — minor bump on the
1780
+ // pre-1.0 channel.
1781
+ version: "1.35.0",
1748
1782
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
1749
1783
  hooks: HOOKS,
1750
1784
  primitives: PRIMITIVES,
package/dist/contract.js CHANGED
@@ -420,6 +420,22 @@ const HOOKS = [
420
420
  requiredContextSlice: ["payments.requestPayment"],
421
421
  scopes: ["payments.charge:appUser"],
422
422
  },
423
+ // sc-890 — send an in-app notification to one app user in the tenant.
424
+ // IMPERATIVE: send() never fires on mount; the widget calls it from an
425
+ // event handler. Reads ctx.notifications.send (the injected
426
+ // @colixsystems/notifications-client). Requires the
427
+ // notifications.send:appUser scope.
428
+ {
429
+ name: "useSendNotification",
430
+ signature: "useSendNotification()",
431
+ returnShape: {
432
+ send: "({ recipient_user_id, title, body, link?, payload? }) => Promise<notification> // rejects with NotificationError",
433
+ sending: "boolean",
434
+ error: "NotificationError | null",
435
+ },
436
+ requiredContextSlice: ["notifications.send"],
437
+ scopes: ["notifications.send:appUser"],
438
+ },
423
439
  // REQ-USERMGMT / REQ-ACL-SYS M3 — AppUser administration. Returns
424
440
  // `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }`.
425
441
  // Reads need `users.read:*` scope; edit-style mutations (invite /
@@ -1026,6 +1042,13 @@ const WIDGET_CONTEXT_SHAPE = {
1026
1042
  required: true,
1027
1043
  fields: { requestPayment: "function", getPayment: "function" },
1028
1044
  },
1045
+ // sc-890 — backs useSendNotification(). Mirror of contract.cjs.
1046
+ notifications: {
1047
+ description:
1048
+ "Injected @colixsystems/notifications-client instance (sc-890). { send(body) -> Promise<notification> }. Backs useSendNotification(); requires the notifications.send:appUser scope. The host POSTs the body snake_case verbatim ({ recipient_user_id, title, body, link?, payload? }) to /notifications/send and returns the created notification row; the call is imperative (a send never fires on mount).",
1049
+ required: true,
1050
+ fields: { send: "function" },
1051
+ },
1029
1052
  // REQ-WSDK-DOMAIN-CLIENTS — the AppUser administration, AppUserGroup
1030
1053
  // administration, and per-record VirtualPermission facades that used to
1031
1054
  // live here (`users`, `groups`, `recordPermissions`) were folded into the
@@ -1744,7 +1767,18 @@ const CONTRACT = deepFreeze({
1744
1767
  // type }` shape the native picker returns, so the same hook code path
1745
1768
  // feeds the multipart in both hosts. No existing hook, primitive, or
1746
1769
  // manifest field changed shape — minor bump.
1747
- version: "1.34.0",
1770
+ //
1771
+ // 1.35.0: additive (sc-890) — new `useSendNotification()` hook reading the
1772
+ // newly-injected `ctx.notifications` (@colixsystems/notifications-client).
1773
+ // Returns `{ send, sending, error }`; `send({ recipient_user_id, title,
1774
+ // body, link?, payload? })` POSTs to `/notifications/send` and resolves to
1775
+ // the created notification row, rejecting with a structured
1776
+ // NotificationError. Imperative — never fires on mount. New required field
1777
+ // on the new `notifications` context slice (`send: "function"`) + the new
1778
+ // `notifications.send:appUser` scope it gates on. No existing hook,
1779
+ // primitive, manifest field, or token changed shape — minor bump on the
1780
+ // pre-1.0 channel.
1781
+ version: "1.35.0",
1748
1782
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
1749
1783
  hooks: HOOKS,
1750
1784
  primitives: PRIMITIVES,
package/dist/hooks.js CHANGED
@@ -2553,3 +2553,124 @@ export function usePayments() {
2553
2553
  }, []);
2554
2554
  return { requestPayment, getPayment };
2555
2555
  }
2556
+
2557
+ /* ============================================================================
2558
+ * NOTIFICATIONS CLIENT — ctx.notifications (@colixsystems/notifications-client)
2559
+ *
2560
+ * send. Covers: useSendNotification.
2561
+ * ==========================================================================*/
2562
+
2563
+ /**
2564
+ * sc-890 — structured error thrown by `useSendNotification().send`. Carries a
2565
+ * stable `code` so widgets can branch without parsing message strings; mirrors
2566
+ * the shape of `PaymentError` / `DirectoryError`.
2567
+ *
2568
+ * `code` is one of:
2569
+ * - "INVALID_TITLE" / "INVALID_BODY" / "INVALID_RECIPIENT" /
2570
+ * "INVALID_PAYLOAD" / "VALIDATION" — 400 (bad request)
2571
+ * - "AUTH_REQUIRED" — 401 (no signed-in app user)
2572
+ * - "FORBIDDEN" — 403 (widget lacks notifications.send:appUser)
2573
+ * - "RECIPIENT_NOT_FOUND" — 404 (recipient not a member of the tenant)
2574
+ * - "RATE_LIMITED" — 429 (too many sends)
2575
+ * - "INTERNAL" — anything else (network, 5xx)
2576
+ *
2577
+ * The server's stable `code` (when volunteered in the error body) is preserved
2578
+ * over the status-derived one.
2579
+ */
2580
+ export class NotificationError extends Error {
2581
+ constructor(code, message, opts) {
2582
+ super(message);
2583
+ this.name = "NotificationError";
2584
+ this.code = code;
2585
+ if (opts && opts.cause) this.cause = opts.cause;
2586
+ }
2587
+ }
2588
+
2589
+ function toNotificationError(err) {
2590
+ if (err instanceof NotificationError) return err;
2591
+ // The injected notifications-client rejects with its own NotificationError
2592
+ // subclass carrying { code, status }; the host facade may instead throw an
2593
+ // axios-shaped error ({ response: { status, data: { code, error } } }). Read
2594
+ // both shapes so the hook surfaces a stable code either way.
2595
+ const status =
2596
+ err && err.response && typeof err.response.status === "number"
2597
+ ? err.response.status
2598
+ : err && typeof err.status === "number"
2599
+ ? err.status
2600
+ : null;
2601
+ const bodyCode =
2602
+ (err && err.response && err.response.data && err.response.data.code) ||
2603
+ (err && typeof err.code === "string" ? err.code : null);
2604
+ const bodyMessage =
2605
+ err && err.response && err.response.data && err.response.data.error;
2606
+ let code = "INTERNAL";
2607
+ if (typeof bodyCode === "string" && bodyCode) code = bodyCode;
2608
+ else if (status === 401) code = "AUTH_REQUIRED";
2609
+ else if (status === 403) code = "FORBIDDEN";
2610
+ else if (status === 404) code = "RECIPIENT_NOT_FOUND";
2611
+ else if (status === 429) code = "RATE_LIMITED";
2612
+ else if (status === 400) code = "VALIDATION";
2613
+ const message =
2614
+ (typeof bodyMessage === "string" && bodyMessage) ||
2615
+ (err && typeof err.message === "string"
2616
+ ? err.message
2617
+ : "Send notification failed");
2618
+ return new NotificationError(code, message, { cause: err });
2619
+ }
2620
+
2621
+ /**
2622
+ * sc-890 — send an in-app notification to one app user in the tenant. Returns
2623
+ * `{ send, sending, error }`.
2624
+ *
2625
+ * send({ recipient_user_id, title, body, link?, payload? })
2626
+ * → Promise<notification>. Body is snake_case VERBATIM (the wire contract,
2627
+ * REQ-GEN-09) — the SDK does NOT transform it. Resolves to the created
2628
+ * notification row (`{ id, tenant_id, user_id, title, body, link,
2629
+ * payload, read_at, created_at }`) and rejects with a `NotificationError`.
2630
+ *
2631
+ * The hook is IMPERATIVE — it NEVER fires on mount; the widget calls `send`
2632
+ * from an event handler (a button press, a form submit). `sending` tracks an
2633
+ * in-flight call and `error` holds the last `NotificationError` (cleared at the
2634
+ * start of each `send`). Reads the injected
2635
+ * `@colixsystems/notifications-client` at `ctx.notifications`.
2636
+ *
2637
+ * Requires the `notifications.send:appUser` scope in the manifest's
2638
+ * `requestedScopes`. A widget that declares the scope but whose caller is not
2639
+ * permitted receives a `NotificationError { code: "FORBIDDEN" }`.
2640
+ */
2641
+ export function useSendNotification() {
2642
+ const ctx = useWidgetContextOrThrow("useSendNotification");
2643
+ if (!ctx.notifications || typeof ctx.notifications.send !== "function") {
2644
+ throw new Error(
2645
+ "useSendNotification: host did not inject a notifications client. The " +
2646
+ "widget must declare the notifications.send:appUser scope and be " +
2647
+ "installed in a workspace whose host supports notifications.",
2648
+ );
2649
+ }
2650
+ // `ctx` is a fresh identity each host render — hold the send fn in a ref so
2651
+ // the callback stays stable.
2652
+ const sendRef = useRef(ctx.notifications.send);
2653
+ sendRef.current = ctx.notifications.send;
2654
+
2655
+ const [sending, setSending] = useState(false);
2656
+ const [error, setError] = useState(null);
2657
+
2658
+ const send = useCallback(async (body) => {
2659
+ setSending(true);
2660
+ setError(null);
2661
+ try {
2662
+ // body is snake_case verbatim ({ recipient_user_id, title, body, link?,
2663
+ // payload? }) — passed straight through.
2664
+ const row = await sendRef.current(body);
2665
+ setSending(false);
2666
+ return row;
2667
+ } catch (err) {
2668
+ const e = toNotificationError(err);
2669
+ setError(e);
2670
+ setSending(false);
2671
+ throw e;
2672
+ }
2673
+ }, []);
2674
+
2675
+ return { send, sending, error };
2676
+ }
package/dist/index.d.ts CHANGED
@@ -395,6 +395,16 @@ export interface PaymentsClient {
395
395
  getPayment(paymentId: string): Promise<PaymentResult>;
396
396
  }
397
397
 
398
+ /**
399
+ * Structural shape of the injected `@colixsystems/notifications-client`
400
+ * (`ctx.notifications`, sc-890). Backs `useSendNotification`. `send` POSTs the
401
+ * body snake_case verbatim to `/notifications/send` and resolves to the created
402
+ * notification row.
403
+ */
404
+ export interface NotificationsClient {
405
+ send(body: SendNotificationRequest): Promise<NotificationRow>;
406
+ }
407
+
398
408
  export interface WidgetContext<TProps = unknown> {
399
409
  props: TProps;
400
410
  widget: { id: string; instanceId: string; version: string };
@@ -435,6 +445,8 @@ export interface WidgetContext<TProps = unknown> {
435
445
  assets: AssetsClient;
436
446
  /** Injected @colixsystems/payments-client. */
437
447
  payments: PaymentsClient;
448
+ /** Injected @colixsystems/notifications-client; backs useSendNotification. */
449
+ notifications: NotificationsClient;
438
450
  /** Host child-node renderer; backs WidgetTree / useChildRenderer. */
439
451
  renderer: { renderNode(node: unknown): unknown };
440
452
  events: { emit(eventName: string, payload?: unknown): void };
@@ -612,6 +624,52 @@ export interface PaymentsApi {
612
624
  */
613
625
  export function usePayments(): PaymentsApi;
614
626
 
627
+ /**
628
+ * Arguments for `useSendNotification().send(...)`. snake_case VERBATIM — this
629
+ * is the wire contract (REQ-GEN-09). `recipient_user_id`, `title`, and `body`
630
+ * are required; `link` and `payload` are optional.
631
+ */
632
+ export interface SendNotificationRequest {
633
+ recipient_user_id: string;
634
+ title: string;
635
+ body: string;
636
+ link?: string | null;
637
+ payload?: Record<string, unknown> | null;
638
+ }
639
+
640
+ /**
641
+ * A notification row, snake_case verbatim as returned by the backend
642
+ * (`POST /notifications/send`, 201). The shape is open-ended; the well-known
643
+ * fields are listed.
644
+ */
645
+ export interface NotificationRow {
646
+ id: string;
647
+ tenant_id?: string;
648
+ user_id?: string;
649
+ title?: string;
650
+ body?: string;
651
+ link?: string | null;
652
+ payload?: Record<string, unknown> | null;
653
+ read_at?: string | null;
654
+ created_at?: string;
655
+ [key: string]: unknown;
656
+ }
657
+
658
+ export interface SendNotificationApi {
659
+ send(body: SendNotificationRequest): Promise<NotificationRow>;
660
+ sending: boolean;
661
+ error: NotificationError | null;
662
+ }
663
+
664
+ /**
665
+ * sc-890 — send an in-app notification to one app user in the tenant. Returns
666
+ * `{ send, sending, error }`. The hook is imperative — `send` never fires on
667
+ * mount; the widget calls it from an event handler. Rejects with a
668
+ * `NotificationError`. Requires the `notifications.send:appUser` scope in the
669
+ * widget manifest.
670
+ */
671
+ export function useSendNotification(): SendNotificationApi;
672
+
615
673
  export function useTheme(): ThemeTokens;
616
674
 
617
675
  /**
@@ -905,6 +963,30 @@ export class DirectoryError extends Error {
905
963
  );
906
964
  }
907
965
 
966
+ /**
967
+ * sc-890 — error class thrown by `useSendNotification().send`. The `code` is a
968
+ * stable categorisation widgets can branch on.
969
+ */
970
+ export class NotificationError extends Error {
971
+ code:
972
+ | "INVALID_TITLE"
973
+ | "INVALID_BODY"
974
+ | "INVALID_RECIPIENT"
975
+ | "INVALID_PAYLOAD"
976
+ | "VALIDATION"
977
+ | "AUTH_REQUIRED"
978
+ | "FORBIDDEN"
979
+ | "RECIPIENT_NOT_FOUND"
980
+ | "RATE_LIMITED"
981
+ | "INTERNAL"
982
+ | string;
983
+ constructor(
984
+ code: NotificationError["code"],
985
+ message: string,
986
+ opts?: { cause?: unknown },
987
+ );
988
+ }
989
+
908
990
  // --------------------------------------------------------------- useUsers
909
991
  //
910
992
  // REQ-USERMGMT / REQ-ACL-SYS M3 — AppUser administration hook.
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ export {
9
9
  WidgetContextProvider,
10
10
  DatastoreError,
11
11
  PaymentError,
12
+ NotificationError,
12
13
  DirectoryError,
13
14
  PermissionError,
14
15
  useDatastoreQuery,
@@ -32,6 +33,7 @@ export {
32
33
  useDatastoreSubscription,
33
34
  useWidgetEvent,
34
35
  usePayments,
36
+ useSendNotification,
35
37
  useTheme,
36
38
  useWidgetStyle,
37
39
  useI18n,
@@ -9,6 +9,7 @@ export {
9
9
  WidgetContextProvider,
10
10
  DatastoreError,
11
11
  PaymentError,
12
+ NotificationError,
12
13
  DirectoryError,
13
14
  PermissionError,
14
15
  useDatastoreQuery,
@@ -30,6 +31,7 @@ export {
30
31
  useDatastoreSubscription,
31
32
  useWidgetEvent,
32
33
  usePayments,
34
+ useSendNotification,
33
35
  useTheme,
34
36
  useWidgetStyle,
35
37
  useI18n,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.47.0",
3
+ "version": "0.48.0",
4
4
  "description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",