@colixsystems/widget-sdk 0.80.0 → 0.82.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 +10 -1
- package/dist/contract.cjs +74 -2
- package/dist/contract.js +74 -2
- package/dist/hooks.js +49 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +1 -0
- package/dist/index.native.js +1 -0
- package/dist/manifest.cjs +75 -0
- package/dist/manifest.js +75 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,6 +24,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
24
24
|
| **CORE** | `useRouteParams()` | `{ [paramKey]: value }` | `ctx.navigation.currentRoute.params` — no scope. The nav params the previous page passed via `goTo(pageId, params)`; the flat accessor for master→detail (read `recordId` on a detail page). Empty object when none. |
|
|
25
25
|
| **CORE** | `usePageContext()` | `{ params, records }` | `ctx.pageContext` — no scope. The page's DECLARED parameters, resolved once by the host: `params` are coerced to their declared types, `records` holds the row already fetched for each `record` param (read it instead of fetching again). Both empty when the page declares none. |
|
|
26
26
|
| **CORE** | `useWidgetEvent(name)` | `(payload?) => void` | `ctx.events.emit` — no scope |
|
|
27
|
+
| **CORE** | `useWidgetInput(inputName)` | the published payload, or `undefined` | `ctx.inputs` — no scope. Reads a value ANOTHER widget on the same page published with `useWidgetEvent`. Declare the input in `manifest.inputs`; the page author wires it to one sibling's declared event. The channel retains the last payload, so a widget that mounts later still reads it. `undefined` while unwired or before the first publish — always render a sensible default. Page-scoped and ephemeral: use `useRouteParams()` for state that must survive navigation, the datastore for state that must persist. |
|
|
27
28
|
| **CORE** | `useChildRenderer()` | `{ renderNode(node) }` | `ctx.renderer` — no scope (prefer the `WidgetTree` component) |
|
|
28
29
|
| **CORE** | `useFill()` | `boolean` | `ctx.fill` — no scope. `true` when the host sized this widget to fill its page-grid tile's reserved height (containers + media fill by default; the author can override per tile). Media-style widgets switch to a `flex: 1` / `height: "100%"` layout; others ignore it. Defaults `false`. |
|
|
29
30
|
| **CORE** | `useContainerWidth()` | `[width, onLayout]` | No context slice, no scope. Measures the width the widget's OWN box has, so it can lay itself out for the space it is in rather than for the screen. Spread the handler onto your outermost primitive. Use it for any widget with a wide form and a narrow one (a table, a toolbar, a row of tiles) — never switch on the device or window width, because a widget in a one-of-three grid cell on desktop has phone-width room and a widget filling a phone page does not. Width is 0 until the first layout: render the WIDE form then. One implementation covers web (react-native-web) and the native export. |
|
|
@@ -61,6 +62,14 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
61
62
|
|
|
62
63
|
`v0.77.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**.
|
|
63
64
|
|
|
65
|
+
### What's new in 0.82.0 (contract 1.57.0)
|
|
66
|
+
|
|
67
|
+
**Server-action scripts can notify a record's permission subjects (REQ-ACTION-NOTIFY-SUBJECTS, sc-4586).** `await notifications.notifyRecordSubjects(tableId, recordId, { title, body, link, emit_email, emit_push, exclude_user_id })` resolves to `{ recipients }` and notifies every app user whose per-record grant lets them **read** that record. This addresses the one audience the other two primitives cannot name: when membership *is* the ACL, there is no recipient column to read. The Chat widget is the case in point — a channel's participants ARE that channel record's grants, so neither a `recipient_expr` (which resolves only a fixed id or a single user/group reference column) nor a `notifyUser` loop over a column that does not exist can reach them. Subject kinds follow REQ-ACL-09: a `user` grant notifies its user, a `group` grant expands through its memberships, and the two synthetic kinds (`authenticated`, `everyone`) are skipped because they address the whole workspace rather than a membership. Recipients are **deduplicated**, so someone reachable through both a direct grant and a granted group is notified once, and `exclude_user_id` drops one — pass the author so nobody is notified of their own write. Dispatch goes through the same `notifyUser` path as before, so the always-written inbox row, the preference-gated email + push mirrors, the link sanitiser and the title/body caps are inherited unchanged, and written rows count toward the same per-run cap of 500. The tenant is bound host-side and the table/record pair is verified against it, so a foreign or missing id resolves to `{ recipients: 0 }` rather than distinguishing "absent" from "not yours". **The script never receives the member list — only the count.** `CONTRACT.version` → `1.57.0`. Additive; no widget hook, primitive, manifest field, or token changed shape.
|
|
68
|
+
|
|
69
|
+
### 0.82.0 also carries (contract 1.56.0)
|
|
70
|
+
|
|
71
|
+
**Server-action scripts gain a `notifications` global (REQ-ACTION-NOTIFY, sc-4514).** A `scriptSource` action can now send a real notification to a recipient it resolves at run time: `await notifications.notifyUser(userId, { title, body, link, emit_email, emit_push })` and `await notifications.notifyGroup(groupId, opts)`. Options are **snake_case**, matching every other shape a script sees. `notifyUser` resolves to the created notification row (or `null` when the recipient was skipped); `notifyGroup` resolves to `{ recipients }`. This is a directness change rather than a new capability: an action could already notify indirectly by writing into a table carrying an enabled `NotificationRule`, but that costs a throwaway table, a per-table rule, a recipient expressible only as a fixed id or one reference column — and it fails silently, since with no rule attached the row just lands and the run still reports success. `POST /notifications/send` is no alternative (it needs an app-user JWT an action cannot hold). The direct call removes the intermediary and makes a non-delivery throw. Both methods delegate to the platform's one notification dispatch path, so the inbox row is always written, the email + push mirrors respect the recipient's channel preferences, and the push ping never carries the title or body. The tenant is bound host-side: a recipient in another workspace, a soft-deleted group, or a deactivated user is a silent skip, never a cross-tenant write. A blank `title`, a non-string `body`, or exceeding the per-run cap of 500 written notifications throws a catchable Error. New entry in `CONTRACT.actionScriptGlobals`; `CONTRACT.version` → `1.56.0`. Additive — no widget hook, primitive, manifest field, or token changed shape.
|
|
72
|
+
|
|
64
73
|
### What's new in 0.78.0
|
|
65
74
|
|
|
66
75
|
**New `useIdentification()` hook — identify a visitor who is NOT signed in (REQ-IDENT, sc-4313).** A new IDENTIFICATION hook reading a newly-injected `ctx.identification` slice (the new `@colixsystems/identification-client`, constructed by both the web Player and the native Expo export). Returns `{ available, availabilityLoading, status, qr, autoStartToken, message, identity, identificationId, loading, error, start, refresh, cancel, reset }`.
|
|
@@ -619,7 +628,7 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
|
|
|
619
628
|
|
|
620
629
|
- `defineWidget({ manifest, component })` — validates the manifest and produces a widget module the host can register.
|
|
621
630
|
- `validateManifest(m)` / `validatePropertySchema(s)` / `validateProps(schema, props)` — shape validation; no third-party deps.
|
|
622
|
-
- `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreSchema`, `useDatastoreMutation`, `useDirectory`, `useUsers`, `useGroups`, `useRecordPermissions`, `useAsset`, `useWidgetEvent`, `usePayments`, `useSendNotification`, `useTheme`, `useI18n`, `useUser`, `useNavigation`, `useRouteParams`, `usePageContext`, `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, displayName, roles, groupIds }` (camelCase — the host-built context object, not a wire payload; `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)`). `useRouteParams()` returns the current route's params object (`currentRoute.params`) — the flat master→detail accessor; read a param off it (e.g. `recordId`), never call it. `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).
|
|
631
|
+
- `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreSchema`, `useDatastoreMutation`, `useDirectory`, `useUsers`, `useGroups`, `useRecordPermissions`, `useAsset`, `useWidgetEvent`, `useWidgetInput`, `usePayments`, `useSendNotification`, `useTheme`, `useI18n`, `useUser`, `useNavigation`, `useRouteParams`, `usePageContext`, `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, displayName, roles, groupIds }` (camelCase — the host-built context object, not a wire payload; `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)`). `useRouteParams()` returns the current route's params object (`currentRoute.params`) — the flat master→detail accessor; read a param off it (e.g. `recordId`), never call it. `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). `useWidgetInput(inputName)` returns the latest payload a sibling widget published on the event the page author wired to this widget's declared `inputs` entry (`undefined` when unwired or not yet published).
|
|
623
632
|
- `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.
|
|
624
633
|
- `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.
|
|
625
634
|
- `WidgetContextProvider` — React context provider that the host (Studio, Player, exported app) wraps widgets with.
|
package/dist/contract.cjs
CHANGED
|
@@ -226,6 +226,17 @@ const THEME_COMPONENTS = Object.freeze({
|
|
|
226
226
|
}),
|
|
227
227
|
});
|
|
228
228
|
|
|
229
|
+
// sc-4505 — the value types a widget event payload field may declare. Narrower
|
|
230
|
+
// than propertySchema VALID_TYPES on purpose: a payload is DATA that crosses to
|
|
231
|
+
// the native export, so it is the JSON-serializable set, not the editor set.
|
|
232
|
+
const PAYLOAD_VALUE_TYPES = Object.freeze([
|
|
233
|
+
"string",
|
|
234
|
+
"number",
|
|
235
|
+
"boolean",
|
|
236
|
+
"object",
|
|
237
|
+
"array",
|
|
238
|
+
]);
|
|
239
|
+
|
|
229
240
|
const HOOKS = [
|
|
230
241
|
{
|
|
231
242
|
name: "useTheme",
|
|
@@ -775,6 +786,18 @@ const HOOKS = [
|
|
|
775
786
|
requiredContextSlice: ["events.emit"],
|
|
776
787
|
scopes: null,
|
|
777
788
|
},
|
|
789
|
+
{
|
|
790
|
+
name: "useWidgetInput",
|
|
791
|
+
signature: "useWidgetInput(inputName)",
|
|
792
|
+
returnShape: {
|
|
793
|
+
value:
|
|
794
|
+
"The latest payload published on the sibling event this input is " +
|
|
795
|
+
"wired to, or undefined until the producer has published. The hook " +
|
|
796
|
+
"returns the value itself, not a wrapper.",
|
|
797
|
+
},
|
|
798
|
+
requiredContextSlice: ["inputs.subscribe"],
|
|
799
|
+
scopes: null,
|
|
800
|
+
},
|
|
778
801
|
{
|
|
779
802
|
name: "usePayments",
|
|
780
803
|
signature: "usePayments()",
|
|
@@ -1246,6 +1269,7 @@ const ACTION_SCRIPT_GLOBALS = [
|
|
|
1246
1269
|
"secrets",
|
|
1247
1270
|
"fetch",
|
|
1248
1271
|
"connectors",
|
|
1272
|
+
"notifications",
|
|
1249
1273
|
"console",
|
|
1250
1274
|
"record",
|
|
1251
1275
|
"tenantId",
|
|
@@ -1352,7 +1376,25 @@ const MANIFEST_SCHEMA = {
|
|
|
1352
1376
|
events: {
|
|
1353
1377
|
type: "object[]",
|
|
1354
1378
|
required: true,
|
|
1355
|
-
description:
|
|
1379
|
+
description:
|
|
1380
|
+
"Declared events the widget publishes. Each entry { name, description?, " +
|
|
1381
|
+
"payloadSchema? }. payloadSchema is a FLAT map of payload field name to " +
|
|
1382
|
+
"{ type, label? } — the SAME shape as propertySchema, NOT JSON Schema — " +
|
|
1383
|
+
"where type is one of string, number, boolean, object, array. The payload reaches " +
|
|
1384
|
+
"any sibling widget on the page whose inputs entry the page author wired " +
|
|
1385
|
+
"to this event.",
|
|
1386
|
+
default: [],
|
|
1387
|
+
},
|
|
1388
|
+
inputs: {
|
|
1389
|
+
type: "object[]",
|
|
1390
|
+
required: false,
|
|
1391
|
+
description:
|
|
1392
|
+
"Optional. Values this widget CONSUMES from a sibling widget on the same " +
|
|
1393
|
+
"page. Each entry { name, description?, schema? }; schema is the same flat " +
|
|
1394
|
+
"{ field: { type, label? } } map as events[].payloadSchema. The page author " +
|
|
1395
|
+
"wires each input to ONE sibling widget event; the widget reads the latest " +
|
|
1396
|
+
"published payload with useWidgetInput(name). An unwired input reads " +
|
|
1397
|
+
"undefined, so a widget must always render a sensible default.",
|
|
1356
1398
|
default: [],
|
|
1357
1399
|
},
|
|
1358
1400
|
datastoreTemplate: {
|
|
@@ -1520,6 +1562,18 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
1520
1562
|
required: true,
|
|
1521
1563
|
fields: { emit: "function" },
|
|
1522
1564
|
},
|
|
1565
|
+
inputs: {
|
|
1566
|
+
description:
|
|
1567
|
+
"{ get(name), subscribe(name, handler) } — the page-scoped input wiring " +
|
|
1568
|
+
"ALREADY RESOLVED for this widget instance, so widget code never sees node " +
|
|
1569
|
+
"ids. get() returns the retained value (undefined when unwired); " +
|
|
1570
|
+
"subscribe() returns an unsubscribe function. The host MUST keep this " +
|
|
1571
|
+
"slice referentially STABLE for a given set of author bindings — " +
|
|
1572
|
+
"useWidgetInput depends on the reference to decide when to resubscribe. " +
|
|
1573
|
+
"Backs useWidgetInput().",
|
|
1574
|
+
required: true,
|
|
1575
|
+
fields: { get: "function", subscribe: "function" },
|
|
1576
|
+
},
|
|
1523
1577
|
payments: {
|
|
1524
1578
|
description:
|
|
1525
1579
|
"Injected @colixsystems/payments-client instance (REQ-BILL-07-WIDGETPAY). { requestPayment(body) -> Promise<{ id, status }>, getPayment(id) -> Promise<payment> }; wire is snake_case (amount_cents, return_path). Backs usePayments(); requires the payments.charge:appUser scope. The host opens hosted Checkout itself — same-tab on web, in-app browser on native (or auto-confirms under the mock provider); the charge settles to the workspace owner.",
|
|
@@ -2453,7 +2507,24 @@ const CONTRACT = deepFreeze({
|
|
|
2453
2507
|
// already measured its own box with onLayout; this promotes that one
|
|
2454
2508
|
// pattern to the SDK so custom and marketplace widgets get it too,
|
|
2455
2509
|
// instead of each rolling its own.
|
|
2456
|
-
|
|
2510
|
+
// 1.55.0: additive (sc-4505) — `useWidgetInput(inputName)` + the
|
|
2511
|
+
// `PAYLOAD_VALUE_TYPES` a widget event payload field may declare, so
|
|
2512
|
+
// widgets on one page relate through declared inputs.
|
|
2513
|
+
// 1.56.0: additive (sc-4514) — `notifications` joins
|
|
2514
|
+
// `actionScriptGlobals`. `notifications.notifyUser(id, opts)` /
|
|
2515
|
+
// `notifyGroup(id, opts)` delegate to the one notification dispatch path.
|
|
2516
|
+
// Directness, not a new capability: an action could already notify by
|
|
2517
|
+
// writing into a table carrying a NotificationRule, but that needs a
|
|
2518
|
+
// throwaway table plus a per-table rule and fails SILENTLY when no rule
|
|
2519
|
+
// is attached — the row lands and the run still reports success.
|
|
2520
|
+
// 1.57.0: additive (sc-4586) — `notifications.notifyRecordSubjects(tableId,
|
|
2521
|
+
// recordId, opts)`, resolving to { recipients }. Notifies whoever a
|
|
2522
|
+
// record's per-record grants let read it, which is the only way to address
|
|
2523
|
+
// membership-by-ACL: a Chat channel's participants ARE its grants, so no
|
|
2524
|
+
// `recipient_expr` shape and no recipient column can name them. The member
|
|
2525
|
+
// list stays host-side — a script receives a count, never the ids — and
|
|
2526
|
+
// `exclude_user_id` keeps an author off their own message.
|
|
2527
|
+
version: "1.57.0",
|
|
2457
2528
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
2458
2529
|
hooks: HOOKS,
|
|
2459
2530
|
primitives: PRIMITIVES,
|
|
@@ -2474,6 +2545,7 @@ const CONTRACT = deepFreeze({
|
|
|
2474
2545
|
vettedImports: VETTED_IMPORTS,
|
|
2475
2546
|
allowedBareImports: ALLOWED_BARE_IMPORTS,
|
|
2476
2547
|
hostApiUrlPatterns: HOST_API_URL_PATTERNS,
|
|
2548
|
+
payloadValueTypes: PAYLOAD_VALUE_TYPES,
|
|
2477
2549
|
translationApiHosts: TRANSLATION_API_HOSTS,
|
|
2478
2550
|
});
|
|
2479
2551
|
|
package/dist/contract.js
CHANGED
|
@@ -226,6 +226,17 @@ const THEME_COMPONENTS = Object.freeze({
|
|
|
226
226
|
}),
|
|
227
227
|
});
|
|
228
228
|
|
|
229
|
+
// sc-4505 — the value types a widget event payload field may declare. Narrower
|
|
230
|
+
// than propertySchema VALID_TYPES on purpose: a payload is DATA that crosses to
|
|
231
|
+
// the native export, so it is the JSON-serializable set, not the editor set.
|
|
232
|
+
const PAYLOAD_VALUE_TYPES = Object.freeze([
|
|
233
|
+
"string",
|
|
234
|
+
"number",
|
|
235
|
+
"boolean",
|
|
236
|
+
"object",
|
|
237
|
+
"array",
|
|
238
|
+
]);
|
|
239
|
+
|
|
229
240
|
const HOOKS = [
|
|
230
241
|
{
|
|
231
242
|
name: "useTheme",
|
|
@@ -775,6 +786,18 @@ const HOOKS = [
|
|
|
775
786
|
requiredContextSlice: ["events.emit"],
|
|
776
787
|
scopes: null,
|
|
777
788
|
},
|
|
789
|
+
{
|
|
790
|
+
name: "useWidgetInput",
|
|
791
|
+
signature: "useWidgetInput(inputName)",
|
|
792
|
+
returnShape: {
|
|
793
|
+
value:
|
|
794
|
+
"The latest payload published on the sibling event this input is " +
|
|
795
|
+
"wired to, or undefined until the producer has published. The hook " +
|
|
796
|
+
"returns the value itself, not a wrapper.",
|
|
797
|
+
},
|
|
798
|
+
requiredContextSlice: ["inputs.subscribe"],
|
|
799
|
+
scopes: null,
|
|
800
|
+
},
|
|
778
801
|
{
|
|
779
802
|
name: "usePayments",
|
|
780
803
|
signature: "usePayments()",
|
|
@@ -1246,6 +1269,7 @@ const ACTION_SCRIPT_GLOBALS = [
|
|
|
1246
1269
|
"secrets",
|
|
1247
1270
|
"fetch",
|
|
1248
1271
|
"connectors",
|
|
1272
|
+
"notifications",
|
|
1249
1273
|
"console",
|
|
1250
1274
|
"record",
|
|
1251
1275
|
"tenantId",
|
|
@@ -1352,7 +1376,25 @@ const MANIFEST_SCHEMA = {
|
|
|
1352
1376
|
events: {
|
|
1353
1377
|
type: "object[]",
|
|
1354
1378
|
required: true,
|
|
1355
|
-
description:
|
|
1379
|
+
description:
|
|
1380
|
+
"Declared events the widget publishes. Each entry { name, description?, " +
|
|
1381
|
+
"payloadSchema? }. payloadSchema is a FLAT map of payload field name to " +
|
|
1382
|
+
"{ type, label? } — the SAME shape as propertySchema, NOT JSON Schema — " +
|
|
1383
|
+
"where type is one of string, number, boolean, object, array. The payload reaches " +
|
|
1384
|
+
"any sibling widget on the page whose inputs entry the page author wired " +
|
|
1385
|
+
"to this event.",
|
|
1386
|
+
default: [],
|
|
1387
|
+
},
|
|
1388
|
+
inputs: {
|
|
1389
|
+
type: "object[]",
|
|
1390
|
+
required: false,
|
|
1391
|
+
description:
|
|
1392
|
+
"Optional. Values this widget CONSUMES from a sibling widget on the same " +
|
|
1393
|
+
"page. Each entry { name, description?, schema? }; schema is the same flat " +
|
|
1394
|
+
"{ field: { type, label? } } map as events[].payloadSchema. The page author " +
|
|
1395
|
+
"wires each input to ONE sibling widget event; the widget reads the latest " +
|
|
1396
|
+
"published payload with useWidgetInput(name). An unwired input reads " +
|
|
1397
|
+
"undefined, so a widget must always render a sensible default.",
|
|
1356
1398
|
default: [],
|
|
1357
1399
|
},
|
|
1358
1400
|
datastoreTemplate: {
|
|
@@ -1520,6 +1562,18 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
1520
1562
|
required: true,
|
|
1521
1563
|
fields: { emit: "function" },
|
|
1522
1564
|
},
|
|
1565
|
+
inputs: {
|
|
1566
|
+
description:
|
|
1567
|
+
"{ get(name), subscribe(name, handler) } — the page-scoped input wiring " +
|
|
1568
|
+
"ALREADY RESOLVED for this widget instance, so widget code never sees node " +
|
|
1569
|
+
"ids. get() returns the retained value (undefined when unwired); " +
|
|
1570
|
+
"subscribe() returns an unsubscribe function. The host MUST keep this " +
|
|
1571
|
+
"slice referentially STABLE for a given set of author bindings — " +
|
|
1572
|
+
"useWidgetInput depends on the reference to decide when to resubscribe. " +
|
|
1573
|
+
"Backs useWidgetInput().",
|
|
1574
|
+
required: true,
|
|
1575
|
+
fields: { get: "function", subscribe: "function" },
|
|
1576
|
+
},
|
|
1523
1577
|
payments: {
|
|
1524
1578
|
description:
|
|
1525
1579
|
"Injected @colixsystems/payments-client instance (REQ-BILL-07-WIDGETPAY). { requestPayment(body) -> Promise<{ id, status }>, getPayment(id) -> Promise<payment> }; wire is snake_case (amount_cents, return_path). Backs usePayments(); requires the payments.charge:appUser scope. The host opens hosted Checkout itself — same-tab on web, in-app browser on native (or auto-confirms under the mock provider); the charge settles to the workspace owner.",
|
|
@@ -2453,7 +2507,24 @@ const CONTRACT = deepFreeze({
|
|
|
2453
2507
|
// already measured its own box with onLayout; this promotes that one
|
|
2454
2508
|
// pattern to the SDK so custom and marketplace widgets get it too,
|
|
2455
2509
|
// instead of each rolling its own.
|
|
2456
|
-
|
|
2510
|
+
// 1.55.0: additive (sc-4505) — `useWidgetInput(inputName)` + the
|
|
2511
|
+
// `PAYLOAD_VALUE_TYPES` a widget event payload field may declare, so
|
|
2512
|
+
// widgets on one page relate through declared inputs.
|
|
2513
|
+
// 1.56.0: additive (sc-4514) — `notifications` joins
|
|
2514
|
+
// `actionScriptGlobals`. `notifications.notifyUser(id, opts)` /
|
|
2515
|
+
// `notifyGroup(id, opts)` delegate to the one notification dispatch path.
|
|
2516
|
+
// Directness, not a new capability: an action could already notify by
|
|
2517
|
+
// writing into a table carrying a NotificationRule, but that needs a
|
|
2518
|
+
// throwaway table plus a per-table rule and fails SILENTLY when no rule
|
|
2519
|
+
// is attached — the row lands and the run still reports success.
|
|
2520
|
+
// 1.57.0: additive (sc-4586) — `notifications.notifyRecordSubjects(tableId,
|
|
2521
|
+
// recordId, opts)`, resolving to { recipients }. Notifies whoever a
|
|
2522
|
+
// record's per-record grants let read it, which is the only way to address
|
|
2523
|
+
// membership-by-ACL: a Chat channel's participants ARE its grants, so no
|
|
2524
|
+
// `recipient_expr` shape and no recipient column can name them. The member
|
|
2525
|
+
// list stays host-side — a script receives a count, never the ids — and
|
|
2526
|
+
// `exclude_user_id` keeps an author off their own message.
|
|
2527
|
+
version: "1.57.0",
|
|
2457
2528
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
2458
2529
|
hooks: HOOKS,
|
|
2459
2530
|
primitives: PRIMITIVES,
|
|
@@ -2474,6 +2545,7 @@ const CONTRACT = deepFreeze({
|
|
|
2474
2545
|
vettedImports: VETTED_IMPORTS,
|
|
2475
2546
|
allowedBareImports: ALLOWED_BARE_IMPORTS,
|
|
2476
2547
|
hostApiUrlPatterns: HOST_API_URL_PATTERNS,
|
|
2548
|
+
payloadValueTypes: PAYLOAD_VALUE_TYPES,
|
|
2477
2549
|
translationApiHosts: TRANSLATION_API_HOSTS,
|
|
2478
2550
|
});
|
|
2479
2551
|
|
package/dist/hooks.js
CHANGED
|
@@ -80,6 +80,55 @@ export function useWidgetEvent(name) {
|
|
|
80
80
|
return useCallback((payload) => ctx.events.emit(name, payload), [ctx, name]);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* sc-4505 — read a value published by ANOTHER widget on the same page.
|
|
85
|
+
*
|
|
86
|
+
* The page author wires this widget's manifest-declared `inputs` entry to one
|
|
87
|
+
* sibling widget's declared event, so widget code names only its OWN input and
|
|
88
|
+
* never learns which widget feeds it:
|
|
89
|
+
*
|
|
90
|
+
* const period = useWidgetInput("period"); // { month: "2027-02" } | undefined
|
|
91
|
+
*
|
|
92
|
+
* The channel RETAINS the last published payload, so a widget that mounts after
|
|
93
|
+
* the producer published (a tab switch, a lazily rendered section) still reads
|
|
94
|
+
* it. An input the author has not wired — and one whose producer has not
|
|
95
|
+
* published yet — reads `undefined`, so a widget must always render a sensible
|
|
96
|
+
* default rather than an empty state.
|
|
97
|
+
*
|
|
98
|
+
* The value is page-scoped and ephemeral: it is gone once the page unmounts.
|
|
99
|
+
* For state that must survive navigation read `useRouteParams()`; for state
|
|
100
|
+
* that must persist, write it to the datastore.
|
|
101
|
+
*/
|
|
102
|
+
export function useWidgetInput(inputName) {
|
|
103
|
+
const ctx = useWidgetContextOrThrow("useWidgetInput");
|
|
104
|
+
// The host keeps `ctx.inputs` referentially stable for a given set of author
|
|
105
|
+
// bindings (CONTRACT.widgetContextShape.inputs), so depending on it here
|
|
106
|
+
// resubscribes when — and only when — the wiring actually changes.
|
|
107
|
+
const inputs = ctx.inputs;
|
|
108
|
+
const [value, setValue] = useState(() => readInput(inputs, inputName));
|
|
109
|
+
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (!inputs || typeof inputs.subscribe !== "function") {
|
|
112
|
+
setValue(undefined);
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
// The producer may have published between the initial render and this
|
|
116
|
+
// effect, so re-read rather than waiting for the next publish.
|
|
117
|
+
setValue(readInput(inputs, inputName));
|
|
118
|
+
// `() => next` so a payload that is itself a function can never be
|
|
119
|
+
// mistaken for a React state updater.
|
|
120
|
+
return inputs.subscribe(inputName, (next) => setValue(() => next));
|
|
121
|
+
}, [inputs, inputName]);
|
|
122
|
+
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function readInput(inputs, inputName) {
|
|
127
|
+
return inputs && typeof inputs.get === "function"
|
|
128
|
+
? inputs.get(inputName)
|
|
129
|
+
: undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
83
132
|
/**
|
|
84
133
|
* Returns the host-provided theme tokens. The host guarantees every field
|
|
85
134
|
* documented in CONTRACT.themeTokens is present (defaults merged with
|
package/dist/index.d.ts
CHANGED
|
@@ -124,6 +124,17 @@ export interface WidgetEventDescriptor {
|
|
|
124
124
|
payloadSchema?: WidgetPropertySchema;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* A value the widget CONSUMES from a sibling widget on the same page. The page
|
|
129
|
+
* author wires each input to one sibling event; the widget reads the latest
|
|
130
|
+
* published payload with `useWidgetInput(name)`.
|
|
131
|
+
*/
|
|
132
|
+
export interface WidgetInputDescriptor {
|
|
133
|
+
name: string;
|
|
134
|
+
description?: string;
|
|
135
|
+
schema?: WidgetPropertySchema;
|
|
136
|
+
}
|
|
137
|
+
|
|
127
138
|
/**
|
|
128
139
|
* Optional datastore template a widget can ship in its manifest. When the
|
|
129
140
|
* tenant installs the widget, every declared table is created in their
|
|
@@ -249,6 +260,11 @@ export interface WidgetManifest {
|
|
|
249
260
|
*/
|
|
250
261
|
styleSchema?: WidgetPropertySchema;
|
|
251
262
|
events: WidgetEventDescriptor[];
|
|
263
|
+
/**
|
|
264
|
+
* Optional (sc-4505). Values this widget reads from a sibling widget on the
|
|
265
|
+
* same page, wired by the page author. Read each with `useWidgetInput(name)`.
|
|
266
|
+
*/
|
|
267
|
+
inputs?: WidgetInputDescriptor[];
|
|
252
268
|
/**
|
|
253
269
|
* Optional datastore template seeded into the tenant's workspace when
|
|
254
270
|
* the widget is installed. The author wires the resulting tables into
|
|
@@ -703,6 +719,13 @@ export function useDirectory(query?: DirectoryQuery): DirectoryResult;
|
|
|
703
719
|
|
|
704
720
|
export function useWidgetEvent(name: string): (payload?: unknown) => void;
|
|
705
721
|
|
|
722
|
+
/**
|
|
723
|
+
* Reads the latest payload published on the sibling event this widget input is
|
|
724
|
+
* wired to. `undefined` until the producer publishes, and for an unwired input
|
|
725
|
+
* — so always render a sensible default. Page-scoped and ephemeral.
|
|
726
|
+
*/
|
|
727
|
+
export function useWidgetInput(inputName: string): unknown;
|
|
728
|
+
|
|
706
729
|
/**
|
|
707
730
|
* Arguments for `usePayments().requestPayment(...)`. snake_case VERBATIM —
|
|
708
731
|
* this is the wire contract (REQ-GEN-09). `amount_cents` is the charge in the
|
package/dist/index.js
CHANGED
package/dist/index.native.js
CHANGED
package/dist/manifest.cjs
CHANGED
|
@@ -4,6 +4,35 @@
|
|
|
4
4
|
// re-exports the same functions defined here so there is exactly one
|
|
5
5
|
// implementation.
|
|
6
6
|
|
|
7
|
+
const { CONTRACT } = require("./contract.cjs");
|
|
8
|
+
|
|
9
|
+
const PAYLOAD_VALUE_TYPES = CONTRACT.payloadValueTypes;
|
|
10
|
+
|
|
11
|
+
// sc-4505 — a payload / input field map: { <field>: { type, label? } }. This is
|
|
12
|
+
// the FLAT propertySchema-style shape index.d.ts already types, NOT JSON Schema.
|
|
13
|
+
// `type` is restricted to the JSON-serializable set because a payload crosses
|
|
14
|
+
// to the exported Expo app (CLAUDE.md §8).
|
|
15
|
+
function validatePayloadFieldMap(schema, label, errors) {
|
|
16
|
+
if (schema === null || typeof schema !== "object" || Array.isArray(schema)) {
|
|
17
|
+
errors.push(`${label} must be an object mapping field name to { type }`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
for (const [field, def] of Object.entries(schema)) {
|
|
21
|
+
if (def === null || typeof def !== "object" || Array.isArray(def)) {
|
|
22
|
+
errors.push(`${label}.${field} must be an object`);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (!PAYLOAD_VALUE_TYPES.includes(def.type)) {
|
|
26
|
+
errors.push(
|
|
27
|
+
`${label}.${field}.type must be one of ${PAYLOAD_VALUE_TYPES.join(", ")}`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
if (def.label !== undefined && typeof def.label !== "string") {
|
|
31
|
+
errors.push(`${label}.${field}.label must be a string when present`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
7
36
|
const VALID_CATEGORIES = new Set([
|
|
8
37
|
"input", "display", "layout", "data", "media", "communication", "administration", "custom",
|
|
9
38
|
"INPUT", "DISPLAY", "LAYOUT", "DATA", "MEDIA", "COMMUNICATION", "ADMINISTRATION", "CUSTOM",
|
|
@@ -275,6 +304,52 @@ function validateManifest(m) {
|
|
|
275
304
|
}
|
|
276
305
|
if (!isNonEmptyString(e.name)) {
|
|
277
306
|
errors.push("manifest.events[].name must be a non-empty string");
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (e.description !== undefined && !isNonEmptyString(e.description)) {
|
|
310
|
+
errors.push(
|
|
311
|
+
`manifest.events[${e.name}].description must be a non-empty string when present`,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
if (e.payloadSchema !== undefined) {
|
|
315
|
+
validatePayloadFieldMap(
|
|
316
|
+
e.payloadSchema,
|
|
317
|
+
`manifest.events[${e.name}].payloadSchema`,
|
|
318
|
+
errors,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// sc-4505 — `inputs` is optional (additive). Declares the values this widget
|
|
325
|
+
// CONSUMES from a sibling widget event, wired by the page author.
|
|
326
|
+
if (manifest.inputs !== undefined) {
|
|
327
|
+
if (!Array.isArray(manifest.inputs)) {
|
|
328
|
+
errors.push("manifest.inputs must be an array (use [] for none)");
|
|
329
|
+
} else {
|
|
330
|
+
const seenInputs = new Set();
|
|
331
|
+
for (const i of manifest.inputs) {
|
|
332
|
+
if (i === null || typeof i !== "object" || Array.isArray(i)) {
|
|
333
|
+
errors.push("manifest.inputs entries must be objects");
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
if (!isNonEmptyString(i.name)) {
|
|
337
|
+
errors.push("manifest.inputs[].name must be a non-empty string");
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
// A duplicate name would make the author-facing wiring ambiguous.
|
|
341
|
+
if (seenInputs.has(i.name)) {
|
|
342
|
+
errors.push(`manifest.inputs[].name "${i.name}" is declared twice`);
|
|
343
|
+
}
|
|
344
|
+
seenInputs.add(i.name);
|
|
345
|
+
if (i.description !== undefined && !isNonEmptyString(i.description)) {
|
|
346
|
+
errors.push(
|
|
347
|
+
`manifest.inputs[${i.name}].description must be a non-empty string when present`,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
if (i.schema !== undefined) {
|
|
351
|
+
validatePayloadFieldMap(i.schema, `manifest.inputs[${i.name}].schema`, errors);
|
|
352
|
+
}
|
|
278
353
|
}
|
|
279
354
|
}
|
|
280
355
|
}
|
package/dist/manifest.js
CHANGED
|
@@ -4,6 +4,35 @@
|
|
|
4
4
|
// re-exports the same functions defined here so there is exactly one
|
|
5
5
|
// implementation.
|
|
6
6
|
|
|
7
|
+
import { CONTRACT } from "./contract.js";
|
|
8
|
+
|
|
9
|
+
const PAYLOAD_VALUE_TYPES = CONTRACT.payloadValueTypes;
|
|
10
|
+
|
|
11
|
+
// sc-4505 — a payload / input field map: { <field>: { type, label? } }. This is
|
|
12
|
+
// the FLAT propertySchema-style shape index.d.ts already types, NOT JSON Schema.
|
|
13
|
+
// `type` is restricted to the JSON-serializable set because a payload crosses
|
|
14
|
+
// to the exported Expo app (CLAUDE.md §8).
|
|
15
|
+
function validatePayloadFieldMap(schema, label, errors) {
|
|
16
|
+
if (schema === null || typeof schema !== "object" || Array.isArray(schema)) {
|
|
17
|
+
errors.push(`${label} must be an object mapping field name to { type }`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
for (const [field, def] of Object.entries(schema)) {
|
|
21
|
+
if (def === null || typeof def !== "object" || Array.isArray(def)) {
|
|
22
|
+
errors.push(`${label}.${field} must be an object`);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (!PAYLOAD_VALUE_TYPES.includes(def.type)) {
|
|
26
|
+
errors.push(
|
|
27
|
+
`${label}.${field}.type must be one of ${PAYLOAD_VALUE_TYPES.join(", ")}`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
if (def.label !== undefined && typeof def.label !== "string") {
|
|
31
|
+
errors.push(`${label}.${field}.label must be a string when present`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
7
36
|
const VALID_CATEGORIES = new Set([
|
|
8
37
|
"input", "display", "layout", "data", "media", "communication", "administration", "custom",
|
|
9
38
|
"INPUT", "DISPLAY", "LAYOUT", "DATA", "MEDIA", "COMMUNICATION", "ADMINISTRATION", "CUSTOM",
|
|
@@ -275,6 +304,52 @@ function validateManifest(m) {
|
|
|
275
304
|
}
|
|
276
305
|
if (!isNonEmptyString(e.name)) {
|
|
277
306
|
errors.push("manifest.events[].name must be a non-empty string");
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (e.description !== undefined && !isNonEmptyString(e.description)) {
|
|
310
|
+
errors.push(
|
|
311
|
+
`manifest.events[${e.name}].description must be a non-empty string when present`,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
if (e.payloadSchema !== undefined) {
|
|
315
|
+
validatePayloadFieldMap(
|
|
316
|
+
e.payloadSchema,
|
|
317
|
+
`manifest.events[${e.name}].payloadSchema`,
|
|
318
|
+
errors,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// sc-4505 — `inputs` is optional (additive). Declares the values this widget
|
|
325
|
+
// CONSUMES from a sibling widget event, wired by the page author.
|
|
326
|
+
if (manifest.inputs !== undefined) {
|
|
327
|
+
if (!Array.isArray(manifest.inputs)) {
|
|
328
|
+
errors.push("manifest.inputs must be an array (use [] for none)");
|
|
329
|
+
} else {
|
|
330
|
+
const seenInputs = new Set();
|
|
331
|
+
for (const i of manifest.inputs) {
|
|
332
|
+
if (i === null || typeof i !== "object" || Array.isArray(i)) {
|
|
333
|
+
errors.push("manifest.inputs entries must be objects");
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
if (!isNonEmptyString(i.name)) {
|
|
337
|
+
errors.push("manifest.inputs[].name must be a non-empty string");
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
// A duplicate name would make the author-facing wiring ambiguous.
|
|
341
|
+
if (seenInputs.has(i.name)) {
|
|
342
|
+
errors.push(`manifest.inputs[].name "${i.name}" is declared twice`);
|
|
343
|
+
}
|
|
344
|
+
seenInputs.add(i.name);
|
|
345
|
+
if (i.description !== undefined && !isNonEmptyString(i.description)) {
|
|
346
|
+
errors.push(
|
|
347
|
+
`manifest.inputs[${i.name}].description must be a non-empty string when present`,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
if (i.schema !== undefined) {
|
|
351
|
+
validatePayloadFieldMap(i.schema, `manifest.inputs[${i.name}].schema`, errors);
|
|
352
|
+
}
|
|
278
353
|
}
|
|
279
354
|
}
|
|
280
355
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.82.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",
|