@colixsystems/widget-sdk 0.47.0 → 0.49.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
  | ----- | ---------------- | ------- | ------------- |
@@ -27,6 +27,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
27
27
  | **CORE** | `useRefresh(handler)` | `void` | `ctx.refresh.subscribe` — no scope. Subscribes the handler to the page-level refresh tick (pull-to-refresh on mobile). Handler may return a Promise — the host waits for `allSettled` before clearing the spinner. The three datastore hooks auto-subscribe their own `refetch`; widgets only call this directly to re-run non-datastore work. No-op on a host that doesn't implement refresh. |
28
28
  | **CORE** | `useClipboard()` | `{ copy, paste, hasContent }` | platform clipboard (web `navigator.clipboard` / native `expo-clipboard`); rejects with `ClipboardError` — no scope |
29
29
  | **CORE** | `useToast()` | `{ showToast }` | `ctx.toast.showToast` (falls back to a CustomEvent / console) — no scope |
30
+ | **CORE** | `useGeolocation(options?)` | `{ latitude, longitude, accuracy, loading, error, getCurrentPosition }` | `ctx.device.geolocation` — no scope. Capture is IMPERATIVE: call `getCurrentPosition()` from a user gesture (a tap), never on mount. Resolves to `{ latitude, longitude, accuracy }`; rejects with `GeolocationError` (`.code` in `PERMISSION_DENIED \| UNAVAILABLE \| TIMEOUT \| UNSUPPORTED \| INTERNAL`). Identical on web (`navigator.geolocation`) and the Expo export (`expo-location`). |
30
31
  | **CORE** | `useI18n()` | `{ t, locale }` | `ctx.i18n` — no scope. `t(key)` resolves the widget-namespaced key (`widget.<id>.<key>`, declared in `manifest.translations`) first, then a **predefined shared key** (`shared.<key>`) when `key` is one of the standard strings (`submit`, `cancel`, `save`, `loading`, …), then the raw key. Use a shared key for an identical default string so it translates once and any per-instance `widget.<id>.<key>` override still wins. |
31
32
  | **DATASTORE** (`ctx.datastore`) | `useDatastoreQuery(table, options?)` | `{ data, loading, error, refetch }` | `records(table).list` (unwraps `{ data, meta }` to `data: []`) — `datastore.read:*` |
32
33
  | **DATASTORE** | `useDatastoreRecord(table, id)` | `{ data, loading, error, refetch }` | `records(table).get` — `datastore.read:<table>` |
@@ -40,6 +41,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
40
41
  | **DIRECTORY** | `useGroups(query?)` | `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` | `directory.groups.*` — `groups.read:*` (mutations also `groups.write:*`) |
41
42
  | **DIRECTORY** | `useBankIdLink()` | `{ linked, available, status, qr, message, startLink, refresh, cancel, unlink, refetchStatus, … }` | `directory.bankid.*` — no scope (JWT-gated self-service) |
42
43
  | **PAYMENTS** (`ctx.payments`) | `usePayments()` | `{ requestPayment, getPayment }` | `ctx.payments.*` — `payments.charge:appUser` |
44
+ | **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
45
 
44
46
  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
47
 
@@ -49,7 +51,15 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
49
51
 
50
52
  ## Status
51
53
 
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**.
54
+ `v0.49.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**.
55
+
56
+ ### What's new in 0.49.0
57
+
58
+ **New `useGeolocation()` hook — read the device's current position (sc-1584).** A new CORE hook reading a newly-injected `ctx.device` slice (host-brokered device capabilities). Returns `{ latitude, longitude, accuracy, loading, error, getCurrentPosition }`. Capture is **imperative** — call `getCurrentPosition()` from a user gesture (a `Pressable.onPress`); browsers and the mobile OS gate the permission prompt on a gesture, so it NEVER fires on mount. The promise resolves to `{ latitude, longitude, accuracy }` and mirrors the same values onto the hook; `options` (`{ enableHighAccuracy, timeout, maximumAge }`) pass through to the host. Rejections surface as a structured `GeolocationError` (new named export) with a stable `.code` (`PERMISSION_DENIED` / `UNAVAILABLE` / `TIMEOUT` / `UNSUPPORTED` / `INTERNAL`). It needs **no manifest scope** and **no `requestedScopes` entry**. The web Player brokers it via `navigator.geolocation`; the Expo export via `expo-location` — so device access is identical on both platforms. The `ctx.device` slice is optional: a host that can't broker the sensor omits it and the hook degrades to an `UNSUPPORTED` error rather than throwing at render. `CONTRACT.version` → `1.36.0`. Additive — one new hook, one new optional context slice, one new error class; no existing export changed signature.
59
+
60
+ ### What's new in 0.48.0
61
+
62
+ **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
63
 
54
64
  ### What's new in 0.47.0
55
65
 
@@ -364,6 +374,7 @@ The "split-implementation + vetted package list" pivot.
364
374
  ### What was in 0.4.1
365
375
 
366
376
  - **`useDatastoreQuery` returns a stable `refetch` identity.** The hook no longer rebinds the underlying callback when the host's `WidgetContext` value (a fresh object identity on every render in Studio + PageRenderer) changes. Widgets that put `refetch` in a `useEffect` dep array no longer loop.
377
+ - **Keep the `useDatastoreQuery` argument stable across renders.** The hook re-fetches whenever `[table, JSON.stringify(query)]` changes, so a query whose serialized value differs every render refetches forever and the widget is stuck on its loading state. The classic trap is a time-relative filter built with `new Date()` / `Date.now()` inline in the query (a "this week" / "today" range) — the timestamp advances each render, so the key is never the same twice. Compute the date/time bound once with `useMemo(() => …, [])` (round to the day if you only need day granularity) and pass the stable value in. The same applies to any per-render value (a freshly built object, `Math.random()`): memoize it before it enters the query.
367
378
 
368
379
  ### What's new in 0.4.0
369
380
 
@@ -396,7 +407,7 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
396
407
 
397
408
  - `defineWidget({ manifest, component })` — validates the manifest and produces a widget module the host can register.
398
409
  - `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).
410
+ - `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
411
  - `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
412
  - `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
413
  - `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 /
@@ -621,6 +637,30 @@ const HOOKS = [
621
637
  requiredContextSlice: ["toast.showToast"],
622
638
  scopes: null,
623
639
  },
640
+ // sc-1584 — host-brokered device geolocation. Optional slice; the hook
641
+ // degrades to an UNSUPPORTED error rather than throwing at render.
642
+ {
643
+ name: "useGeolocation",
644
+ signature: "useGeolocation(options?)",
645
+ description:
646
+ "Read the device's current position. Returns { latitude, longitude, accuracy, loading, error, getCurrentPosition }. " +
647
+ "Capture is IMPERATIVE — call getCurrentPosition() from a user gesture (a tap); browsers and the mobile OS gate the " +
648
+ "permission prompt on a gesture, so it NEVER fires on mount. The promise resolves to { latitude, longitude, accuracy } " +
649
+ "and stores the same values on the hook; it rejects with a GeolocationError whose .code is one of PERMISSION_DENIED | " +
650
+ "UNAVAILABLE | TIMEOUT | UNSUPPORTED | INTERNAL. options pass through to the host ({ enableHighAccuracy, timeout, " +
651
+ "maximumAge }). Identical on web (navigator.geolocation) and the Expo export (expo-location).",
652
+ returnShape: {
653
+ latitude: "number | null",
654
+ longitude: "number | null",
655
+ accuracy: "number | null // metres, best-effort",
656
+ loading: "boolean",
657
+ error: "GeolocationError | null",
658
+ getCurrentPosition:
659
+ "() => Promise<{ latitude, longitude, accuracy }> // rejects with GeolocationError",
660
+ },
661
+ requiredContextSlice: [],
662
+ scopes: null,
663
+ },
624
664
  ];
625
665
 
626
666
  // REQ-WSDK-RN-WEB: the SDK exposes the React Native primitive API
@@ -1026,6 +1066,13 @@ const WIDGET_CONTEXT_SHAPE = {
1026
1066
  required: true,
1027
1067
  fields: { requestPayment: "function", getPayment: "function" },
1028
1068
  },
1069
+ // sc-890 — backs useSendNotification(). Mirror of contract.js.
1070
+ notifications: {
1071
+ description:
1072
+ "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).",
1073
+ required: true,
1074
+ fields: { send: "function" },
1075
+ },
1029
1076
  // REQ-WSDK-DOMAIN-CLIENTS — the AppUser administration, AppUserGroup
1030
1077
  // administration, and per-record VirtualPermission facades that used to
1031
1078
  // live here (`users`, `groups`, `recordPermissions`) were folded into the
@@ -1060,6 +1107,19 @@ const WIDGET_CONTEXT_SHAPE = {
1060
1107
  required: false,
1061
1108
  fields: { showToast: "function" },
1062
1109
  },
1110
+ // sc-1584 — host-brokered device capabilities. Optional: a host that can't
1111
+ // broker a sensor omits the slice and useGeolocation() surfaces UNSUPPORTED.
1112
+ // Both the web Player (navigator.geolocation) and the Expo export
1113
+ // (expo-location) inject it, so device access is identical on both platforms.
1114
+ device: {
1115
+ description:
1116
+ "Optional host-brokered device capabilities. " +
1117
+ "{ geolocation: { getCurrentPosition(options?) -> Promise<{ latitude, longitude, accuracy }> } }. " +
1118
+ "Backs useGeolocation(). The web Player brokers it via navigator.geolocation; the Expo export via expo-location. " +
1119
+ "getCurrentPosition rejects with a GeolocationError (.code PERMISSION_DENIED | UNAVAILABLE | TIMEOUT | UNSUPPORTED | INTERNAL).",
1120
+ required: false,
1121
+ fields: { geolocation: "object" },
1122
+ },
1063
1123
  };
1064
1124
 
1065
1125
  const BUNDLE_EXPORT_CONTRACT = [
@@ -1744,7 +1804,18 @@ const CONTRACT = deepFreeze({
1744
1804
  // type }` shape the native picker returns, so the same hook code path
1745
1805
  // feeds the multipart in both hosts. No existing hook, primitive, or
1746
1806
  // manifest field changed shape — minor bump.
1747
- version: "1.34.0",
1807
+ //
1808
+ // 1.35.0: additive (sc-890) — new `useSendNotification()` hook reading the
1809
+ // newly-injected `ctx.notifications` (@colixsystems/notifications-client).
1810
+ // Returns `{ send, sending, error }`; `send({ recipient_user_id, title,
1811
+ // body, link?, payload? })` POSTs to `/notifications/send` and resolves to
1812
+ // the created notification row, rejecting with a structured
1813
+ // NotificationError. Imperative — never fires on mount. New required field
1814
+ // on the new `notifications` context slice (`send: "function"`) + the new
1815
+ // `notifications.send:appUser` scope it gates on. No existing hook,
1816
+ // primitive, manifest field, or token changed shape — minor bump on the
1817
+ // pre-1.0 channel.
1818
+ version: "1.36.0",
1748
1819
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
1749
1820
  hooks: HOOKS,
1750
1821
  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 /
@@ -621,6 +637,30 @@ const HOOKS = [
621
637
  requiredContextSlice: ["toast.showToast"],
622
638
  scopes: null,
623
639
  },
640
+ // sc-1584 — host-brokered device geolocation. Optional slice; the hook
641
+ // degrades to an UNSUPPORTED error rather than throwing at render.
642
+ {
643
+ name: "useGeolocation",
644
+ signature: "useGeolocation(options?)",
645
+ description:
646
+ "Read the device's current position. Returns { latitude, longitude, accuracy, loading, error, getCurrentPosition }. " +
647
+ "Capture is IMPERATIVE — call getCurrentPosition() from a user gesture (a tap); browsers and the mobile OS gate the " +
648
+ "permission prompt on a gesture, so it NEVER fires on mount. The promise resolves to { latitude, longitude, accuracy } " +
649
+ "and stores the same values on the hook; it rejects with a GeolocationError whose .code is one of PERMISSION_DENIED | " +
650
+ "UNAVAILABLE | TIMEOUT | UNSUPPORTED | INTERNAL. options pass through to the host ({ enableHighAccuracy, timeout, " +
651
+ "maximumAge }). Identical on web (navigator.geolocation) and the Expo export (expo-location).",
652
+ returnShape: {
653
+ latitude: "number | null",
654
+ longitude: "number | null",
655
+ accuracy: "number | null // metres, best-effort",
656
+ loading: "boolean",
657
+ error: "GeolocationError | null",
658
+ getCurrentPosition:
659
+ "() => Promise<{ latitude, longitude, accuracy }> // rejects with GeolocationError",
660
+ },
661
+ requiredContextSlice: [],
662
+ scopes: null,
663
+ },
624
664
  ];
625
665
 
626
666
  // REQ-WSDK-RN-WEB: the SDK exposes the React Native primitive API
@@ -1026,6 +1066,13 @@ const WIDGET_CONTEXT_SHAPE = {
1026
1066
  required: true,
1027
1067
  fields: { requestPayment: "function", getPayment: "function" },
1028
1068
  },
1069
+ // sc-890 — backs useSendNotification(). Mirror of contract.cjs.
1070
+ notifications: {
1071
+ description:
1072
+ "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).",
1073
+ required: true,
1074
+ fields: { send: "function" },
1075
+ },
1029
1076
  // REQ-WSDK-DOMAIN-CLIENTS — the AppUser administration, AppUserGroup
1030
1077
  // administration, and per-record VirtualPermission facades that used to
1031
1078
  // live here (`users`, `groups`, `recordPermissions`) were folded into the
@@ -1060,6 +1107,19 @@ const WIDGET_CONTEXT_SHAPE = {
1060
1107
  required: false,
1061
1108
  fields: { showToast: "function" },
1062
1109
  },
1110
+ // sc-1584 — host-brokered device capabilities. Optional: a host that can't
1111
+ // broker a sensor omits the slice and useGeolocation() surfaces UNSUPPORTED.
1112
+ // Both the web Player (navigator.geolocation) and the Expo export
1113
+ // (expo-location) inject it, so device access is identical on both platforms.
1114
+ device: {
1115
+ description:
1116
+ "Optional host-brokered device capabilities. " +
1117
+ "{ geolocation: { getCurrentPosition(options?) -> Promise<{ latitude, longitude, accuracy }> } }. " +
1118
+ "Backs useGeolocation(). The web Player brokers it via navigator.geolocation; the Expo export via expo-location. " +
1119
+ "getCurrentPosition rejects with a GeolocationError (.code PERMISSION_DENIED | UNAVAILABLE | TIMEOUT | UNSUPPORTED | INTERNAL).",
1120
+ required: false,
1121
+ fields: { geolocation: "object" },
1122
+ },
1063
1123
  };
1064
1124
 
1065
1125
  const BUNDLE_EXPORT_CONTRACT = [
@@ -1744,7 +1804,18 @@ const CONTRACT = deepFreeze({
1744
1804
  // type }` shape the native picker returns, so the same hook code path
1745
1805
  // feeds the multipart in both hosts. No existing hook, primitive, or
1746
1806
  // manifest field changed shape — minor bump.
1747
- version: "1.34.0",
1807
+ //
1808
+ // 1.35.0: additive (sc-890) — new `useSendNotification()` hook reading the
1809
+ // newly-injected `ctx.notifications` (@colixsystems/notifications-client).
1810
+ // Returns `{ send, sending, error }`; `send({ recipient_user_id, title,
1811
+ // body, link?, payload? })` POSTs to `/notifications/send` and resolves to
1812
+ // the created notification row, rejecting with a structured
1813
+ // NotificationError. Imperative — never fires on mount. New required field
1814
+ // on the new `notifications` context slice (`send: "function"`) + the new
1815
+ // `notifications.send:appUser` scope it gates on. No existing hook,
1816
+ // primitive, manifest field, or token changed shape — minor bump on the
1817
+ // pre-1.0 channel.
1818
+ version: "1.36.0",
1748
1819
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
1749
1820
  hooks: HOOKS,
1750
1821
  primitives: PRIMITIVES,
package/dist/hooks.js CHANGED
@@ -336,6 +336,141 @@ export function useI18n() {
336
336
  return { t, locale };
337
337
  }
338
338
 
339
+ /* ============================================================================
340
+ * DEVICE — ctx.device (host-brokered device capabilities)
341
+ *
342
+ * Sensor / hardware the host brokers for the widget — geolocation today. The
343
+ * host injects `ctx.device.<cap>` on BOTH platforms (web Player via
344
+ * navigator.geolocation; the Expo export via expo-location), so a widget reads
345
+ * the device identically on web and native (widget-parity skill). The slice is
346
+ * optional — a host that cannot broker a capability omits it and the hook
347
+ * surfaces an UNSUPPORTED error instead of throwing at render. Covers:
348
+ * useGeolocation.
349
+ * ==========================================================================*/
350
+
351
+ /**
352
+ * Error thrown by `useGeolocation().getCurrentPosition()` (and surfaced in the
353
+ * hook's `error` slot). Carries a stable `code` so widgets branch on the error
354
+ * class without parsing message strings.
355
+ *
356
+ * `code` is one of:
357
+ * - "PERMISSION_DENIED" — the user (or OS) refused location access.
358
+ * - "UNAVAILABLE" — position could not be determined (no fix / sensor).
359
+ * - "TIMEOUT" — the request exceeded the host/option timeout.
360
+ * - "UNSUPPORTED" — this host does not broker geolocation.
361
+ * - "INTERNAL" — anything else.
362
+ */
363
+ export class GeolocationError extends Error {
364
+ constructor(code, message, opts) {
365
+ super(message);
366
+ this.name = "GeolocationError";
367
+ this.code = code;
368
+ if (opts && opts.cause) this.cause = opts.cause;
369
+ }
370
+ }
371
+
372
+ /**
373
+ * Coerce a thrown value into a GeolocationError with a stable `.code`. Maps the
374
+ * browser PositionError numeric codes (1/2/3) and the host clients' string
375
+ * codes onto one vocabulary.
376
+ */
377
+ function toGeolocationError(err) {
378
+ if (err instanceof GeolocationError) return err;
379
+ const raw = err && err.code !== undefined ? err.code : null;
380
+ let code = "INTERNAL";
381
+ if (raw === 1 || raw === "PERMISSION_DENIED") code = "PERMISSION_DENIED";
382
+ else if (raw === 2 || raw === "UNAVAILABLE" || raw === "POSITION_UNAVAILABLE")
383
+ code = "UNAVAILABLE";
384
+ else if (raw === 3 || raw === "TIMEOUT") code = "TIMEOUT";
385
+ else if (raw === "UNSUPPORTED") code = "UNSUPPORTED";
386
+ const message =
387
+ (err && typeof err.message === "string" && err.message) ||
388
+ "Geolocation request failed";
389
+ return new GeolocationError(code, message, { cause: err });
390
+ }
391
+
392
+ /**
393
+ * Read the device's current position. Returns
394
+ * `{ latitude, longitude, accuracy, loading, error, getCurrentPosition }`.
395
+ *
396
+ * Capture is IMPERATIVE — call `getCurrentPosition()` from a user gesture (a
397
+ * tap on a button). Browsers and the mobile OS gate the permission prompt on a
398
+ * user gesture, so the hook never fires on mount. The promise resolves to
399
+ * `{ latitude, longitude, accuracy }` and the same values are stored on the
400
+ * hook; it rejects with a `GeolocationError` carrying a stable `.code`.
401
+ *
402
+ * `options` pass through to the host (`{ enableHighAccuracy, timeout,
403
+ * maximumAge }`). The SAME hook drives both platforms: the web Player brokers
404
+ * it through `navigator.geolocation`, the Expo export through `expo-location`.
405
+ *
406
+ * Safe-by-default: on a host that does not inject `ctx.device.geolocation`,
407
+ * `getCurrentPosition()` rejects with `code: "UNSUPPORTED"` rather than
408
+ * throwing at render, so a widget can call the hook unconditionally.
409
+ */
410
+ export function useGeolocation(options) {
411
+ const ctx = useWidgetContextOrThrow("useGeolocation");
412
+ const [coords, setCoords] = useState(null);
413
+ const [loading, setLoading] = useState(false);
414
+ const [error, setError] = useState(null);
415
+
416
+ // `ctx` is a fresh identity every host render — hold the live client +
417
+ // options in refs so getCurrentPosition is a stable callback.
418
+ const clientRef = useRef(ctx.device && ctx.device.geolocation);
419
+ clientRef.current = ctx.device && ctx.device.geolocation;
420
+ const optionsRef = useRef(options);
421
+ optionsRef.current = options;
422
+ const runRef = useRef(0);
423
+
424
+ const getCurrentPosition = useCallback(async () => {
425
+ const myRun = ++runRef.current;
426
+ const client = clientRef.current;
427
+ if (!client || typeof client.getCurrentPosition !== "function") {
428
+ const e = new GeolocationError(
429
+ "UNSUPPORTED",
430
+ "This host does not provide device geolocation.",
431
+ );
432
+ if (runRef.current === myRun) {
433
+ setError(e);
434
+ setLoading(false);
435
+ }
436
+ throw e;
437
+ }
438
+ setLoading(true);
439
+ setError(null);
440
+ try {
441
+ const pos = await client.getCurrentPosition(optionsRef.current);
442
+ const next = {
443
+ latitude:
444
+ pos && typeof pos.latitude === "number" ? pos.latitude : null,
445
+ longitude:
446
+ pos && typeof pos.longitude === "number" ? pos.longitude : null,
447
+ accuracy:
448
+ pos && typeof pos.accuracy === "number" ? pos.accuracy : null,
449
+ };
450
+ if (runRef.current !== myRun) return next;
451
+ setCoords(next);
452
+ setLoading(false);
453
+ return next;
454
+ } catch (err) {
455
+ const ge = toGeolocationError(err);
456
+ if (runRef.current === myRun) {
457
+ setError(ge);
458
+ setLoading(false);
459
+ }
460
+ throw ge;
461
+ }
462
+ }, []);
463
+
464
+ return {
465
+ latitude: coords ? coords.latitude : null,
466
+ longitude: coords ? coords.longitude : null,
467
+ accuracy: coords ? coords.accuracy : null,
468
+ loading,
469
+ error,
470
+ getCurrentPosition,
471
+ };
472
+ }
473
+
339
474
  /* ============================================================================
340
475
  * DATASTORE CLIENT — ctx.datastore (@colixsystems/datastore-client)
341
476
  *
@@ -2553,3 +2688,124 @@ export function usePayments() {
2553
2688
  }, []);
2554
2689
  return { requestPayment, getPayment };
2555
2690
  }
2691
+
2692
+ /* ============================================================================
2693
+ * NOTIFICATIONS CLIENT — ctx.notifications (@colixsystems/notifications-client)
2694
+ *
2695
+ * send. Covers: useSendNotification.
2696
+ * ==========================================================================*/
2697
+
2698
+ /**
2699
+ * sc-890 — structured error thrown by `useSendNotification().send`. Carries a
2700
+ * stable `code` so widgets can branch without parsing message strings; mirrors
2701
+ * the shape of `PaymentError` / `DirectoryError`.
2702
+ *
2703
+ * `code` is one of:
2704
+ * - "INVALID_TITLE" / "INVALID_BODY" / "INVALID_RECIPIENT" /
2705
+ * "INVALID_PAYLOAD" / "VALIDATION" — 400 (bad request)
2706
+ * - "AUTH_REQUIRED" — 401 (no signed-in app user)
2707
+ * - "FORBIDDEN" — 403 (widget lacks notifications.send:appUser)
2708
+ * - "RECIPIENT_NOT_FOUND" — 404 (recipient not a member of the tenant)
2709
+ * - "RATE_LIMITED" — 429 (too many sends)
2710
+ * - "INTERNAL" — anything else (network, 5xx)
2711
+ *
2712
+ * The server's stable `code` (when volunteered in the error body) is preserved
2713
+ * over the status-derived one.
2714
+ */
2715
+ export class NotificationError extends Error {
2716
+ constructor(code, message, opts) {
2717
+ super(message);
2718
+ this.name = "NotificationError";
2719
+ this.code = code;
2720
+ if (opts && opts.cause) this.cause = opts.cause;
2721
+ }
2722
+ }
2723
+
2724
+ function toNotificationError(err) {
2725
+ if (err instanceof NotificationError) return err;
2726
+ // The injected notifications-client rejects with its own NotificationError
2727
+ // subclass carrying { code, status }; the host facade may instead throw an
2728
+ // axios-shaped error ({ response: { status, data: { code, error } } }). Read
2729
+ // both shapes so the hook surfaces a stable code either way.
2730
+ const status =
2731
+ err && err.response && typeof err.response.status === "number"
2732
+ ? err.response.status
2733
+ : err && typeof err.status === "number"
2734
+ ? err.status
2735
+ : null;
2736
+ const bodyCode =
2737
+ (err && err.response && err.response.data && err.response.data.code) ||
2738
+ (err && typeof err.code === "string" ? err.code : null);
2739
+ const bodyMessage =
2740
+ err && err.response && err.response.data && err.response.data.error;
2741
+ let code = "INTERNAL";
2742
+ if (typeof bodyCode === "string" && bodyCode) code = bodyCode;
2743
+ else if (status === 401) code = "AUTH_REQUIRED";
2744
+ else if (status === 403) code = "FORBIDDEN";
2745
+ else if (status === 404) code = "RECIPIENT_NOT_FOUND";
2746
+ else if (status === 429) code = "RATE_LIMITED";
2747
+ else if (status === 400) code = "VALIDATION";
2748
+ const message =
2749
+ (typeof bodyMessage === "string" && bodyMessage) ||
2750
+ (err && typeof err.message === "string"
2751
+ ? err.message
2752
+ : "Send notification failed");
2753
+ return new NotificationError(code, message, { cause: err });
2754
+ }
2755
+
2756
+ /**
2757
+ * sc-890 — send an in-app notification to one app user in the tenant. Returns
2758
+ * `{ send, sending, error }`.
2759
+ *
2760
+ * send({ recipient_user_id, title, body, link?, payload? })
2761
+ * → Promise<notification>. Body is snake_case VERBATIM (the wire contract,
2762
+ * REQ-GEN-09) — the SDK does NOT transform it. Resolves to the created
2763
+ * notification row (`{ id, tenant_id, user_id, title, body, link,
2764
+ * payload, read_at, created_at }`) and rejects with a `NotificationError`.
2765
+ *
2766
+ * The hook is IMPERATIVE — it NEVER fires on mount; the widget calls `send`
2767
+ * from an event handler (a button press, a form submit). `sending` tracks an
2768
+ * in-flight call and `error` holds the last `NotificationError` (cleared at the
2769
+ * start of each `send`). Reads the injected
2770
+ * `@colixsystems/notifications-client` at `ctx.notifications`.
2771
+ *
2772
+ * Requires the `notifications.send:appUser` scope in the manifest's
2773
+ * `requestedScopes`. A widget that declares the scope but whose caller is not
2774
+ * permitted receives a `NotificationError { code: "FORBIDDEN" }`.
2775
+ */
2776
+ export function useSendNotification() {
2777
+ const ctx = useWidgetContextOrThrow("useSendNotification");
2778
+ if (!ctx.notifications || typeof ctx.notifications.send !== "function") {
2779
+ throw new Error(
2780
+ "useSendNotification: host did not inject a notifications client. The " +
2781
+ "widget must declare the notifications.send:appUser scope and be " +
2782
+ "installed in a workspace whose host supports notifications.",
2783
+ );
2784
+ }
2785
+ // `ctx` is a fresh identity each host render — hold the send fn in a ref so
2786
+ // the callback stays stable.
2787
+ const sendRef = useRef(ctx.notifications.send);
2788
+ sendRef.current = ctx.notifications.send;
2789
+
2790
+ const [sending, setSending] = useState(false);
2791
+ const [error, setError] = useState(null);
2792
+
2793
+ const send = useCallback(async (body) => {
2794
+ setSending(true);
2795
+ setError(null);
2796
+ try {
2797
+ // body is snake_case verbatim ({ recipient_user_id, title, body, link?,
2798
+ // payload? }) — passed straight through.
2799
+ const row = await sendRef.current(body);
2800
+ setSending(false);
2801
+ return row;
2802
+ } catch (err) {
2803
+ const e = toNotificationError(err);
2804
+ setError(e);
2805
+ setSending(false);
2806
+ throw e;
2807
+ }
2808
+ }, []);
2809
+
2810
+ return { send, sending, error };
2811
+ }
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 };
@@ -452,6 +464,16 @@ export interface WidgetContext<TProps = unknown> {
452
464
  toast?: {
453
465
  showToast(args: { kind?: string; message: string }): void;
454
466
  };
467
+ /** Optional host-brokered device capabilities; backs useGeolocation. */
468
+ device?: {
469
+ geolocation?: {
470
+ getCurrentPosition(options?: GeolocationOptions): Promise<{
471
+ latitude: number;
472
+ longitude: number;
473
+ accuracy: number;
474
+ }>;
475
+ };
476
+ };
455
477
  }
456
478
 
457
479
  /**
@@ -612,6 +634,52 @@ export interface PaymentsApi {
612
634
  */
613
635
  export function usePayments(): PaymentsApi;
614
636
 
637
+ /**
638
+ * Arguments for `useSendNotification().send(...)`. snake_case VERBATIM — this
639
+ * is the wire contract (REQ-GEN-09). `recipient_user_id`, `title`, and `body`
640
+ * are required; `link` and `payload` are optional.
641
+ */
642
+ export interface SendNotificationRequest {
643
+ recipient_user_id: string;
644
+ title: string;
645
+ body: string;
646
+ link?: string | null;
647
+ payload?: Record<string, unknown> | null;
648
+ }
649
+
650
+ /**
651
+ * A notification row, snake_case verbatim as returned by the backend
652
+ * (`POST /notifications/send`, 201). The shape is open-ended; the well-known
653
+ * fields are listed.
654
+ */
655
+ export interface NotificationRow {
656
+ id: string;
657
+ tenant_id?: string;
658
+ user_id?: string;
659
+ title?: string;
660
+ body?: string;
661
+ link?: string | null;
662
+ payload?: Record<string, unknown> | null;
663
+ read_at?: string | null;
664
+ created_at?: string;
665
+ [key: string]: unknown;
666
+ }
667
+
668
+ export interface SendNotificationApi {
669
+ send(body: SendNotificationRequest): Promise<NotificationRow>;
670
+ sending: boolean;
671
+ error: NotificationError | null;
672
+ }
673
+
674
+ /**
675
+ * sc-890 — send an in-app notification to one app user in the tenant. Returns
676
+ * `{ send, sending, error }`. The hook is imperative — `send` never fires on
677
+ * mount; the widget calls it from an event handler. Rejects with a
678
+ * `NotificationError`. Requires the `notifications.send:appUser` scope in the
679
+ * widget manifest.
680
+ */
681
+ export function useSendNotification(): SendNotificationApi;
682
+
615
683
  export function useTheme(): ThemeTokens;
616
684
 
617
685
  /**
@@ -863,6 +931,60 @@ export function useRefresh(
863
931
  handler: () => void | Promise<unknown>,
864
932
  ): void;
865
933
 
934
+ /** Pass-through options for `useGeolocation().getCurrentPosition(...)`. */
935
+ export interface GeolocationOptions {
936
+ enableHighAccuracy?: boolean;
937
+ timeout?: number;
938
+ maximumAge?: number;
939
+ }
940
+
941
+ export interface GeolocationResult {
942
+ latitude: number | null;
943
+ longitude: number | null;
944
+ /** Best-effort accuracy in metres. */
945
+ accuracy: number | null;
946
+ loading: boolean;
947
+ error: GeolocationError | null;
948
+ /**
949
+ * Imperatively read the device position — call from a user gesture. Resolves
950
+ * to `{ latitude, longitude, accuracy }` and stores the same on the hook;
951
+ * rejects with a `GeolocationError`.
952
+ */
953
+ getCurrentPosition(): Promise<{
954
+ latitude: number;
955
+ longitude: number;
956
+ accuracy: number;
957
+ }>;
958
+ }
959
+
960
+ /**
961
+ * sc-1584 — read the device's current position. Capture is imperative (call
962
+ * `getCurrentPosition()` from a user gesture; it never fires on mount). The
963
+ * same hook drives both platforms — the web Player brokers it via
964
+ * `navigator.geolocation`, the Expo export via `expo-location`. Safe to call on
965
+ * a host that doesn't broker geolocation: `getCurrentPosition()` then rejects
966
+ * with `code: "UNSUPPORTED"`.
967
+ */
968
+ export function useGeolocation(options?: GeolocationOptions): GeolocationResult;
969
+
970
+ /**
971
+ * sc-1584 — error thrown by `useGeolocation().getCurrentPosition()` and
972
+ * surfaced in the hook's `error` slot. `code` is a stable categorisation.
973
+ */
974
+ export class GeolocationError extends Error {
975
+ code:
976
+ | "PERMISSION_DENIED"
977
+ | "UNAVAILABLE"
978
+ | "TIMEOUT"
979
+ | "UNSUPPORTED"
980
+ | "INTERNAL";
981
+ constructor(
982
+ code: GeolocationError["code"],
983
+ message: string,
984
+ opts?: { cause?: unknown },
985
+ );
986
+ }
987
+
866
988
  /**
867
989
  * Error class thrown by useDatastoreMutation callbacks (and surfaced by
868
990
  * useDatastoreQuery in its `error` slot). The `code` is a stable
@@ -905,6 +1027,30 @@ export class DirectoryError extends Error {
905
1027
  );
906
1028
  }
907
1029
 
1030
+ /**
1031
+ * sc-890 — error class thrown by `useSendNotification().send`. The `code` is a
1032
+ * stable categorisation widgets can branch on.
1033
+ */
1034
+ export class NotificationError extends Error {
1035
+ code:
1036
+ | "INVALID_TITLE"
1037
+ | "INVALID_BODY"
1038
+ | "INVALID_RECIPIENT"
1039
+ | "INVALID_PAYLOAD"
1040
+ | "VALIDATION"
1041
+ | "AUTH_REQUIRED"
1042
+ | "FORBIDDEN"
1043
+ | "RECIPIENT_NOT_FOUND"
1044
+ | "RATE_LIMITED"
1045
+ | "INTERNAL"
1046
+ | string;
1047
+ constructor(
1048
+ code: NotificationError["code"],
1049
+ message: string,
1050
+ opts?: { cause?: unknown },
1051
+ );
1052
+ }
1053
+
908
1054
  // --------------------------------------------------------------- useUsers
909
1055
  //
910
1056
  // 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,
@@ -40,6 +42,8 @@ export {
40
42
  useNavigation,
41
43
  useChildRenderer,
42
44
  useRefresh,
45
+ useGeolocation,
46
+ GeolocationError,
43
47
  WidgetTree,
44
48
  } from "./hooks.js";
45
49
  // REQ-WSDK-PLATFORM §6 — Tier A hooks. Each ships in a per-platform file
@@ -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,
@@ -38,6 +40,8 @@ export {
38
40
  useNavigation,
39
41
  useChildRenderer,
40
42
  useRefresh,
43
+ useGeolocation,
44
+ GeolocationError,
41
45
  WidgetTree,
42
46
  } from "./hooks.js";
43
47
  // REQ-WSDK-PLATFORM §6 — Tier A hooks (native variants).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.47.0",
3
+ "version": "0.49.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",
@@ -42,7 +42,7 @@
42
42
  ],
43
43
  "scripts": {
44
44
  "build": "node scripts/build.js",
45
- "test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-assets-by-tag.test.js src/__tests__/hooks-filestore-upload.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-subscription.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/lucide-icon-names.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/devserver.test.js src/__tests__/host-externals.test.js"
45
+ "test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-assets-by-tag.test.js src/__tests__/hooks-filestore-upload.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-geolocation.test.js src/__tests__/hooks-subscription.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/lucide-icon-names.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/devserver.test.js src/__tests__/host-externals.test.js"
46
46
  },
47
47
  "engines": {
48
48
  "node": ">=18"