@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 +8 -3
- package/dist/contract.cjs +35 -1
- package/dist/contract.js +35 -1
- package/dist/hooks.js +121 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +2 -0
- package/dist/index.native.js +2 -0
- package/package.json +1 -1
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 (
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
package/dist/index.native.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,
|
|
@@ -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.
|
|
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",
|