@colixsystems/widget-sdk 0.32.0 → 0.34.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
@@ -11,7 +11,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
11
11
  | `ctx.files` | `@colixsystems/files-client` | the Asset Manager: `get(id)`, `list(query)`, `upload(formData)` over `/files` — what `useFile()` resolves |
12
12
  | `ctx.payments` | `@colixsystems/payments-client` | `requestPayment(body)`, `getPayment(id)` |
13
13
 
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 client-side case transform anywhere** — the only transform left in the system is the backend's wire Prisma middleware. Author-defined record column values pass through verbatim. Every `list(...)` returns the `{ data, meta }` envelope; the read hooks unwrap `res.data` for you.
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
16
  **Hooks read the injected clients** — they do not hold their own HTTP. This is the **complete** hook surface (18 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
 
@@ -36,6 +36,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
36
36
  | **DIRECTORY** (`ctx.directory`) | `useDirectory(query?)` | `{ users, loading, error, refetch }` | `directory.users.list` — `directory.read:users` |
37
37
  | **DIRECTORY** | `useUsers(query?)` | `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }` | `directory.users.*` — `users.read:*` (mutations also `users.write:*`) |
38
38
  | **DIRECTORY** | `useGroups(query?)` | `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` | `directory.groups.*` — `groups.read:*` (mutations also `groups.write:*`) |
39
+ | **DIRECTORY** | `useBankIdLink()` | `{ linked, available, status, qr, message, startLink, refresh, cancel, unlink, refetchStatus, … }` | `directory.bankid.*` — no scope (JWT-gated self-service) |
39
40
  | **PAYMENTS** (`ctx.payments`) | `usePayments()` | `{ requestPayment, getPayment }` | `ctx.payments.*` — `payments.charge:appUser` |
40
41
 
41
42
  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 }`).
@@ -46,7 +47,15 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
46
47
 
47
48
  ## Status
48
49
 
49
- `v0.32.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**.
50
+ `v0.34.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**.
51
+
52
+ ### What's new in 0.34.0
53
+
54
+ **`useBankIdLink()` — link / unlink BankID from a widget (REQ-BANKID-AUTH).** A new DIRECTORY hook reading the new `ctx.directory.bankid` namespace on `@colixsystems/directory-client` **0.2.0** (`status` / `startLink` / `collect` / `cancel` / `unlink`). It lets a signed-in app-user attach a BankID identity to their account (or remove it) via an animated-QR poll — `startLink()` opens the order, `refresh()` polls it (drive on a ~2s interval while `status === "pending"`; render `qr` with the `Image` primitive), `unlink()` removes it. `available` is `false` when BankID can't be used in the app (provider disabled or no platform cert) — hide the affordance then. Self-service + JWT-gated, so **no `requestedScopes` entry is required**. Backs the built-in **User** widget's "Link BankID" section. `CONTRACT.version` → `1.24.0`. Additive — no existing export or type changed.
55
+
56
+ ### What's new in 0.33.0
57
+
58
+ **New `filterList` propertySchema type + `ui.resetOnFieldChange` / `ui.defaultFactory` hints (REQ-WBLT-03, Tab Layout migration / #140).** `filterList` is a multi-condition record-filter builder — per-row column + operator + value with a "Relative date (N days ago)" toggle for DATE + ordering operators. The persisted value is `Array<{ column, operator, value, valueMode }>` matching the records-filter contract (`?filter[col]=op:value`). Columns resolve from a sibling tableRef (defaults to `tableId`; override with `ui.tableProp`). Pair with `ui.resetOnFieldChange: "tableId"` so the chain wipes when the source table changes. `ui.resetOnFieldChange: "<sibling>"` resets a property to its default when the named sibling changes — used to clear stale `columnRef`/`filterList` values nested inside an `array<object>` when the form-root tableId switches. `ui.defaultFactory: "tabId"` seeds a unique stable id when a new array item is added (Tab Layout's `tabs[*].id`). The Tab Layout built-in widget moved fully onto the manifest-driven Properties Panel; its hand-rolled per-tab editor is gone. `CONTRACT.version` → `1.23.0`. Additive — no existing export or type changed.
50
59
 
51
60
  ### What's new in 0.32.0
52
61
 
package/dist/contract.cjs CHANGED
@@ -377,6 +377,42 @@ const HOOKS = [
377
377
  requiredContextSlice: ["directory.groups"],
378
378
  scopes: ["groups.read:*"],
379
379
  },
380
+ // REQ-BANKID-AUTH — link / unlink a BankID identity to the signed-in
381
+ // app-user. Self-service + JWT-gated (no widget scope). Mirror of contract.js.
382
+ {
383
+ name: "useBankIdLink",
384
+ signature: "useBankIdLink()",
385
+ description:
386
+ "Link / unlink a BankID identity to the signed-in app-user via the " +
387
+ "injected directory-client at ctx.directory.bankid.{status,startLink," +
388
+ "collect,cancel,unlink}. Returns { linked, available, status, qr, message, " +
389
+ "loading, statusLoading, error, startLink, refresh, cancel, unlink, " +
390
+ "refetchStatus }. On mount it reads the link status; `available` is false " +
391
+ "when BankID can't be used here (provider disabled or no platform cert) — " +
392
+ "hide the Link affordance then. The link flow is an animated-QR poll like " +
393
+ "useFileSignature: startLink() opens an order (status 'pending' + qr — a " +
394
+ "PNG data-URL, render with the Image primitive); refresh() polls (drive on " +
395
+ "a ~2s interval while pending; sets linked=true on complete); cancel() " +
396
+ "aborts; unlink() removes the link (rejects DirectoryError LAST_AUTH_METHOD " +
397
+ "when BankID is the only sign-in method). No requestedScopes entry needed.",
398
+ returnShape: {
399
+ linked: "boolean | null // null until the status loads",
400
+ available: "boolean // false → hide the Link affordance",
401
+ status: "'pending' | 'complete' | 'failed' | 'cancelled' | null",
402
+ qr: "string | null // PNG data-URL of the animated BankID QR",
403
+ message: "string | null",
404
+ loading: "boolean",
405
+ statusLoading: "boolean",
406
+ error: "DirectoryError | null",
407
+ startLink: "() => Promise<{ order_ref, qr, auto_start_token, status }>",
408
+ refresh: "() => Promise<void>",
409
+ cancel: "() => Promise<void>",
410
+ unlink: "() => Promise<{ linked: boolean }> // rejects with DirectoryError",
411
+ refetchStatus: "() => Promise<{ linked, available }>",
412
+ },
413
+ requiredContextSlice: ["directory.bankid"],
414
+ scopes: null,
415
+ },
380
416
  // REQ-ACL-06 / REQ-ACL-RELINHERIT-05 — per-record VirtualPermission
381
417
  // management for a single record. Mirror of contract.js.
382
418
  {
@@ -809,10 +845,11 @@ const WIDGET_CONTEXT_SHAPE = {
809
845
  "Injected @colixsystems/directory-client instance. " +
810
846
  "{ me(), users: { list(query?) -> Promise<{ data, meta }>, get(id), invite(body), deactivate(id), reactivate(id) }, " +
811
847
  "groups: { list(query?) -> Promise<{ data, meta }>, create(body), remove(id), addMember(groupId, userId), removeMember(groupId, userId), listMine() }, " +
812
- "invites: { list(), revoke(id), resend(id) } }. " +
813
- "users backs useDirectory() + useUsers(); groups backs useGroups(). List methods return the { data, meta } envelope verbatim (hooks unwrap res.data); rows/bodies are snake_case. Reads gated by directory.read:users / users.read:* / groups.read:*; mutations by users.write:* / groups.write:*.",
848
+ "invites: { list(), revoke(id), resend(id) }, " +
849
+ "bankid: { status() -> { linked, available }, startLink() -> { order_ref, qr, ... }, collect(orderRef), cancel(orderRef), unlink() } }. " +
850
+ "users backs useDirectory() + useUsers(); groups backs useGroups(); bankid backs useBankIdLink() (REQ-BANKID-AUTH — self-service account linking, JWT-gated, no widget scope). List methods return the { data, meta } envelope verbatim (hooks unwrap res.data); rows/bodies are snake_case. Reads gated by directory.read:users / users.read:* / groups.read:*; mutations by users.write:* / groups.write:*.",
814
851
  required: true,
815
- fields: { users: "object", groups: "object" },
852
+ fields: { users: "object", groups: "object", bankid: "object" },
816
853
  },
817
854
  files: {
818
855
  description:
@@ -1335,7 +1372,29 @@ const CONTRACT = deepFreeze({
1335
1372
  // instead of the hand-rolled block it shipped with — removes the last data
1336
1373
  // widget from `LEGACY_EDITOR_TYPES`. No existing type changed shape, so
1337
1374
  // this is additive — minor bump on the pre-1.0 channel.
1338
- version: "1.22.0",
1375
+ //
1376
+ // 1.23.0: additive (REQ-WBLT-03, Tab Layout migration / #140) — new
1377
+ // `filterList` propertySchema type. A multi-condition record-filter
1378
+ // builder whose persisted value is `Array<{ column, operator, value,
1379
+ // valueMode }>` matching the records-filter contract
1380
+ // (`?filter[col]=op:value`). Columns resolve from a sibling tableRef
1381
+ // (default `tableId`; override via `ui.tableProp`). Plus two free-form
1382
+ // `ui.*` hints used by the Tab Layout manifest and available to any
1383
+ // widget: `ui.resetOnFieldChange: "<sibling>"` (reset this property to
1384
+ // its default when the named sibling changes — wipes stale
1385
+ // columnRef/filterList values inside an array<object> when the
1386
+ // form-root tableId switches) and `ui.defaultFactory: "tabId"`
1387
+ // (per-instance dynamic default). No existing field, hook, primitive,
1388
+ // or token changed shape — minor bump on the pre-1.0 channel.
1389
+ //
1390
+ // 1.24.0: additive (REQ-BANKID-AUTH) — new `useBankIdLink()` hook reading the
1391
+ // new `ctx.directory.bankid` namespace (status/startLink/collect/cancel/
1392
+ // unlink) on the @colixsystems/directory-client (0.2.0). Lets the built-in
1393
+ // User widget link / unlink a BankID identity to the signed-in app-user
1394
+ // (animated-QR poll). Self-service + JWT-gated — no requestedScopes entry.
1395
+ // No existing hook, primitive, or field changed shape — minor bump on the
1396
+ // pre-1.0 channel.
1397
+ version: "1.24.0",
1339
1398
  hooks: HOOKS,
1340
1399
  primitives: PRIMITIVES,
1341
1400
  manifestSchema: MANIFEST_SCHEMA,
package/dist/contract.js CHANGED
@@ -377,6 +377,42 @@ const HOOKS = [
377
377
  requiredContextSlice: ["directory.groups"],
378
378
  scopes: ["groups.read:*"],
379
379
  },
380
+ // REQ-BANKID-AUTH — link / unlink a BankID identity to the signed-in
381
+ // app-user. Self-service + JWT-gated (no widget scope). Mirror of contract.cjs.
382
+ {
383
+ name: "useBankIdLink",
384
+ signature: "useBankIdLink()",
385
+ description:
386
+ "Link / unlink a BankID identity to the signed-in app-user via the " +
387
+ "injected directory-client at ctx.directory.bankid.{status,startLink," +
388
+ "collect,cancel,unlink}. Returns { linked, available, status, qr, message, " +
389
+ "loading, statusLoading, error, startLink, refresh, cancel, unlink, " +
390
+ "refetchStatus }. On mount it reads the link status; `available` is false " +
391
+ "when BankID can't be used here (provider disabled or no platform cert) — " +
392
+ "hide the Link affordance then. The link flow is an animated-QR poll like " +
393
+ "useFileSignature: startLink() opens an order (status 'pending' + qr — a " +
394
+ "PNG data-URL, render with the Image primitive); refresh() polls (drive on " +
395
+ "a ~2s interval while pending; sets linked=true on complete); cancel() " +
396
+ "aborts; unlink() removes the link (rejects DirectoryError LAST_AUTH_METHOD " +
397
+ "when BankID is the only sign-in method). No requestedScopes entry needed.",
398
+ returnShape: {
399
+ linked: "boolean | null // null until the status loads",
400
+ available: "boolean // false → hide the Link affordance",
401
+ status: "'pending' | 'complete' | 'failed' | 'cancelled' | null",
402
+ qr: "string | null // PNG data-URL of the animated BankID QR",
403
+ message: "string | null",
404
+ loading: "boolean",
405
+ statusLoading: "boolean",
406
+ error: "DirectoryError | null",
407
+ startLink: "() => Promise<{ order_ref, qr, auto_start_token, status }>",
408
+ refresh: "() => Promise<void>",
409
+ cancel: "() => Promise<void>",
410
+ unlink: "() => Promise<{ linked: boolean }> // rejects with DirectoryError",
411
+ refetchStatus: "() => Promise<{ linked, available }>",
412
+ },
413
+ requiredContextSlice: ["directory.bankid"],
414
+ scopes: null,
415
+ },
380
416
  // REQ-ACL-06 / REQ-ACL-RELINHERIT-05 — per-record VirtualPermission
381
417
  // management for a single record. Mirror of contract.js.
382
418
  {
@@ -809,10 +845,11 @@ const WIDGET_CONTEXT_SHAPE = {
809
845
  "Injected @colixsystems/directory-client instance. " +
810
846
  "{ me(), users: { list(query?) -> Promise<{ data, meta }>, get(id), invite(body), deactivate(id), reactivate(id) }, " +
811
847
  "groups: { list(query?) -> Promise<{ data, meta }>, create(body), remove(id), addMember(groupId, userId), removeMember(groupId, userId), listMine() }, " +
812
- "invites: { list(), revoke(id), resend(id) } }. " +
813
- "users backs useDirectory() + useUsers(); groups backs useGroups(). List methods return the { data, meta } envelope verbatim (hooks unwrap res.data); rows/bodies are snake_case. Reads gated by directory.read:users / users.read:* / groups.read:*; mutations by users.write:* / groups.write:*.",
848
+ "invites: { list(), revoke(id), resend(id) }, " +
849
+ "bankid: { status() -> { linked, available }, startLink() -> { order_ref, qr, ... }, collect(orderRef), cancel(orderRef), unlink() } }. " +
850
+ "users backs useDirectory() + useUsers(); groups backs useGroups(); bankid backs useBankIdLink() (REQ-BANKID-AUTH — self-service account linking, JWT-gated, no widget scope). List methods return the { data, meta } envelope verbatim (hooks unwrap res.data); rows/bodies are snake_case. Reads gated by directory.read:users / users.read:* / groups.read:*; mutations by users.write:* / groups.write:*.",
814
851
  required: true,
815
- fields: { users: "object", groups: "object" },
852
+ fields: { users: "object", groups: "object", bankid: "object" },
816
853
  },
817
854
  files: {
818
855
  description:
@@ -1335,7 +1372,29 @@ const CONTRACT = deepFreeze({
1335
1372
  // instead of the hand-rolled block it shipped with — removes the last data
1336
1373
  // widget from `LEGACY_EDITOR_TYPES`. No existing type changed shape, so
1337
1374
  // this is additive — minor bump on the pre-1.0 channel.
1338
- version: "1.22.0",
1375
+ //
1376
+ // 1.23.0: additive (REQ-WBLT-03, Tab Layout migration / #140) — new
1377
+ // `filterList` propertySchema type. A multi-condition record-filter
1378
+ // builder whose persisted value is `Array<{ column, operator, value,
1379
+ // valueMode }>` matching the records-filter contract
1380
+ // (`?filter[col]=op:value`). Columns resolve from a sibling tableRef
1381
+ // (default `tableId`; override via `ui.tableProp`). Plus two free-form
1382
+ // `ui.*` hints used by the Tab Layout manifest and available to any
1383
+ // widget: `ui.resetOnFieldChange: "<sibling>"` (reset this property to
1384
+ // its default when the named sibling changes — wipes stale
1385
+ // columnRef/filterList values inside an array<object> when the
1386
+ // form-root tableId switches) and `ui.defaultFactory: "tabId"`
1387
+ // (per-instance dynamic default). No existing field, hook, primitive,
1388
+ // or token changed shape — minor bump on the pre-1.0 channel.
1389
+ //
1390
+ // 1.24.0: additive (REQ-BANKID-AUTH) — new `useBankIdLink()` hook reading the
1391
+ // new `ctx.directory.bankid` namespace (status/startLink/collect/cancel/
1392
+ // unlink) on the @colixsystems/directory-client (0.2.0). Lets the built-in
1393
+ // User widget link / unlink a BankID identity to the signed-in app-user
1394
+ // (animated-QR poll). Self-service + JWT-gated — no requestedScopes entry.
1395
+ // No existing hook, primitive, or field changed shape — minor bump on the
1396
+ // pre-1.0 channel.
1397
+ version: "1.24.0",
1339
1398
  hooks: HOOKS,
1340
1399
  primitives: PRIMITIVES,
1341
1400
  manifestSchema: MANIFEST_SCHEMA,
package/dist/hooks.js CHANGED
@@ -1850,6 +1850,170 @@ export function useGroups(query) {
1850
1850
  return { groups, loading, error, refetch, create, remove, addMember, removeMember };
1851
1851
  }
1852
1852
 
1853
+ /**
1854
+ * REQ-BANKID-AUTH — link / unlink a BankID identity to the signed-in app-user,
1855
+ * and read whether BankID is available + already linked. Returns
1856
+ * `{ linked, available, status, qr, message, loading, statusLoading, error,
1857
+ * startLink, refresh, cancel, unlink, refetchStatus }`.
1858
+ *
1859
+ * Reads the injected `@colixsystems/directory-client` at `ctx.directory.bankid`
1860
+ * (a self-service, JWT-gated surface — no widget scope needed). On mount it
1861
+ * fetches the link status; `available` is false when BankID can't be used in
1862
+ * this app (provider disabled or no platform cert), so a widget should hide the
1863
+ * Link affordance entirely when `!available`.
1864
+ *
1865
+ * The link flow is an animated-QR poll, mirroring `useFileSignature`:
1866
+ * - `startLink()` opens an order → sets `status: "pending"` + `qr` (a PNG
1867
+ * data-URL; render it with the `Image` primitive).
1868
+ * - `refresh()` polls the order — refreshing the QR frame while pending and,
1869
+ * on completion, setting `linked: true` + `status: "complete"`. The widget
1870
+ * drives the cadence (e.g. a 2s interval while `status === "pending"`).
1871
+ * - `cancel()` aborts the in-flight order.
1872
+ * - `unlink()` removes the link (rejects with a DirectoryError carrying
1873
+ * `code: "LAST_AUTH_METHOD"` / status 409 when BankID is the user's only
1874
+ * sign-in method).
1875
+ *
1876
+ * No `requestedScopes` entry is required — the endpoints are gated by the
1877
+ * app-user session the host already holds.
1878
+ */
1879
+ export function useBankIdLink() {
1880
+ const ctx = useWidgetContextOrThrow("useBankIdLink");
1881
+ if (
1882
+ !ctx.directory ||
1883
+ !ctx.directory.bankid ||
1884
+ typeof ctx.directory.bankid.status !== "function"
1885
+ ) {
1886
+ throw new Error(
1887
+ "useBankIdLink: host did not inject a directory client (ctx.directory.bankid)",
1888
+ );
1889
+ }
1890
+ // `ctx` is a fresh identity each host render — hold the namespace in a ref so
1891
+ // the callbacks stay stable.
1892
+ const apiRef = useRef(ctx.directory.bankid);
1893
+ apiRef.current = ctx.directory.bankid;
1894
+
1895
+ const [linked, setLinked] = useState(null); // null until the status loads
1896
+ const [available, setAvailable] = useState(false);
1897
+ const [statusLoading, setStatusLoading] = useState(true);
1898
+ const [status, setStatus] = useState(null); // null | pending | complete | failed | cancelled
1899
+ const [qr, setQr] = useState(null);
1900
+ const [message, setMessage] = useState(null);
1901
+ const [loading, setLoading] = useState(false);
1902
+ const [error, setError] = useState(null);
1903
+ const orderRef = useRef(null);
1904
+
1905
+ const refetchStatus = useCallback(async () => {
1906
+ setStatusLoading(true);
1907
+ try {
1908
+ const res = await apiRef.current.status();
1909
+ setLinked(Boolean(res && res.linked));
1910
+ setAvailable(Boolean(res && res.available));
1911
+ setStatusLoading(false);
1912
+ return res;
1913
+ } catch (err) {
1914
+ setError(toDirectoryError(err));
1915
+ setStatusLoading(false);
1916
+ return null;
1917
+ }
1918
+ }, []);
1919
+
1920
+ useEffect(() => {
1921
+ refetchStatus();
1922
+ }, [refetchStatus]);
1923
+
1924
+ const startLink = useCallback(async () => {
1925
+ setLoading(true);
1926
+ setError(null);
1927
+ try {
1928
+ const res = await apiRef.current.startLink();
1929
+ orderRef.current = res && res.order_ref ? res.order_ref : null;
1930
+ setStatus(res && res.status ? res.status : "pending");
1931
+ setQr(res && res.qr ? res.qr : null);
1932
+ setMessage(null);
1933
+ setLoading(false);
1934
+ return res;
1935
+ } catch (err) {
1936
+ const e = toDirectoryError(err);
1937
+ setError(e);
1938
+ setStatus("failed");
1939
+ setMessage(e.message);
1940
+ setLoading(false);
1941
+ throw e;
1942
+ }
1943
+ }, []);
1944
+
1945
+ const refresh = useCallback(async () => {
1946
+ const id = orderRef.current;
1947
+ if (!id) return null;
1948
+ try {
1949
+ const res = await apiRef.current.collect(id);
1950
+ if (res) {
1951
+ if (res.status != null) setStatus(res.status);
1952
+ if (res.qr !== undefined) setQr(res.qr || null);
1953
+ if (res.message !== undefined) setMessage(res.message || null);
1954
+ if (res.status === "complete") setLinked(true);
1955
+ }
1956
+ return res;
1957
+ } catch (err) {
1958
+ // A terminal client error (e.g. 409 BANKID_ALREADY_LINKED) carries a
1959
+ // helpful message — surface it as a failed order rather than throwing
1960
+ // into the widget's poll interval.
1961
+ const e = toDirectoryError(err);
1962
+ setError(e);
1963
+ setStatus("failed");
1964
+ setMessage(e.message);
1965
+ return null;
1966
+ }
1967
+ }, []);
1968
+
1969
+ const cancel = useCallback(async () => {
1970
+ const id = orderRef.current;
1971
+ orderRef.current = null;
1972
+ setStatus(null);
1973
+ setQr(null);
1974
+ setMessage(null);
1975
+ if (id) {
1976
+ try {
1977
+ await apiRef.current.cancel(id);
1978
+ } catch {
1979
+ // best-effort — the order may have already expired.
1980
+ }
1981
+ }
1982
+ }, []);
1983
+
1984
+ const unlink = useCallback(async () => {
1985
+ setLoading(true);
1986
+ setError(null);
1987
+ try {
1988
+ const res = await apiRef.current.unlink();
1989
+ setLinked(Boolean(res && res.linked));
1990
+ setLoading(false);
1991
+ return res;
1992
+ } catch (err) {
1993
+ const e = toDirectoryError(err);
1994
+ setError(e);
1995
+ setLoading(false);
1996
+ throw e;
1997
+ }
1998
+ }, []);
1999
+
2000
+ return {
2001
+ linked,
2002
+ available,
2003
+ status,
2004
+ qr,
2005
+ message,
2006
+ loading,
2007
+ statusLoading,
2008
+ error,
2009
+ startLink,
2010
+ refresh,
2011
+ cancel,
2012
+ unlink,
2013
+ refetchStatus,
2014
+ };
2015
+ }
2016
+
1853
2017
  /* ============================================================================
1854
2018
  * PAYMENTS CLIENT — ctx.payments (@colixsystems/payments-client)
1855
2019
  *
package/dist/index.d.ts CHANGED
@@ -56,7 +56,13 @@ export type WidgetPropertyType =
56
56
  | "expression"
57
57
  | "eventBinding"
58
58
  | "object"
59
- | "array";
59
+ | "array"
60
+ // REQ-WBLT-03 (Tab Layout migration): multi-condition record filter
61
+ // builder. Persisted value is `Array<{ column, operator, value, valueMode }>`
62
+ // matching the ?filter[col]=op:value contract; columns resolve from a
63
+ // sibling tableRef (defaults to the form-root `tableId`, override via
64
+ // `ui.tableProp`).
65
+ | "filterList";
60
66
 
61
67
  export interface WidgetPropertyDef {
62
68
  type: WidgetPropertyType;
@@ -71,6 +77,15 @@ export interface WidgetPropertyDef {
71
77
  widget?: "textarea" | "slider" | "code";
72
78
  group?: string;
73
79
  order?: number;
80
+ // REQ-WBLT-03: when the named sibling field changes, reset this
81
+ // property to its schema default. Used to wipe stale columnRef /
82
+ // filterList bindings nested inside array<object> when the form-root
83
+ // tableId switches.
84
+ resetOnFieldChange?: string;
85
+ // REQ-WBLT-03: per-instance dynamic default (can't be expressed as
86
+ // a static `default`). Today: `"tabId"` → seed a unique stable id
87
+ // for newly added Tab Layout tabs.
88
+ defaultFactory?: "tabId";
74
89
  };
75
90
  validation?: { min?: number; max?: number; pattern?: string };
76
91
  }
@@ -339,6 +354,24 @@ export interface DirectoryClient {
339
354
  revoke(inviteId: string): Promise<void>;
340
355
  resend(inviteId: string): Promise<unknown>;
341
356
  };
357
+ /** REQ-BANKID-AUTH — backs `useBankIdLink`. */
358
+ bankid: {
359
+ status(): Promise<{ linked: boolean; available: boolean }>;
360
+ startLink(): Promise<{
361
+ order_ref: string;
362
+ auto_start_token: string | null;
363
+ qr: string | null;
364
+ status: "pending";
365
+ }>;
366
+ collect(orderRef: string): Promise<{
367
+ status: "pending" | "complete" | "failed" | "cancelled";
368
+ qr?: string | null;
369
+ message?: string;
370
+ linked?: boolean;
371
+ }>;
372
+ cancel(orderRef: string): Promise<{ status: "cancelled" }>;
373
+ unlink(): Promise<{ linked: boolean }>;
374
+ };
342
375
  }
343
376
 
344
377
  /**
@@ -909,6 +942,45 @@ export interface GroupsApi {
909
942
  */
910
943
  export function useGroups(query?: GroupsQuery): GroupsApi;
911
944
 
945
+ // ----------------------------------------------------- useBankIdLink
946
+ //
947
+ // REQ-BANKID-AUTH — link / unlink a BankID identity to the signed-in app-user.
948
+ // Reads the injected directory-client at `ctx.directory.bankid`. Self-service,
949
+ // JWT-gated — no requestedScopes entry needed.
950
+
951
+ export interface BankIdLinkApi {
952
+ /** Whether the user currently has BankID linked (null until the status loads). */
953
+ linked: boolean | null;
954
+ /** Whether BankID linking can be used in this app (provider enabled + platform cert). */
955
+ available: boolean;
956
+ /** The active link order's state, or null when no order is in flight. */
957
+ status: "pending" | "complete" | "failed" | "cancelled" | null;
958
+ /** PNG data-URL of the animated BankID QR while pending (render with the Image primitive). */
959
+ qr: string | null;
960
+ message: string | null;
961
+ loading: boolean;
962
+ statusLoading: boolean;
963
+ error: DirectoryError | null;
964
+ /** Open a link order → sets status "pending" + qr. */
965
+ startLink(): Promise<unknown>;
966
+ /** Poll the open order; on completion sets linked=true. Drive on an interval while pending. */
967
+ refresh(): Promise<unknown>;
968
+ /** Abort the in-flight order. */
969
+ cancel(): Promise<void>;
970
+ /** Remove the link (rejects with DirectoryError code LAST_AUTH_METHOD when it is the only method). */
971
+ unlink(): Promise<{ linked: boolean }>;
972
+ /** Re-read { linked, available }. */
973
+ refetchStatus(): Promise<unknown>;
974
+ }
975
+
976
+ /**
977
+ * Link / unlink a BankID identity to the signed-in app-user and read the
978
+ * current link + availability state. Reads `ctx.directory.bankid`. No
979
+ * requestedScopes entry is required — the endpoints are gated by the app-user
980
+ * session the host holds. Hide the Link affordance when `available` is false.
981
+ */
982
+ export function useBankIdLink(): BankIdLinkApi;
983
+
912
984
  // ----------------------------------------------------- useRecordPermissions
913
985
  //
914
986
  // REQ-ACL-06 / REQ-ACL-RELINHERIT-05 — per-record VirtualPermission
package/dist/index.js CHANGED
@@ -23,6 +23,7 @@ export {
23
23
  useDirectory,
24
24
  useUsers,
25
25
  useGroups,
26
+ useBankIdLink,
26
27
  useRecordPermissions,
27
28
  useDatastoreSubscription,
28
29
  useWidgetEvent,
@@ -23,6 +23,7 @@ export {
23
23
  useDirectory,
24
24
  useUsers,
25
25
  useGroups,
26
+ useBankIdLink,
26
27
  useRecordPermissions,
27
28
  useDatastoreSubscription,
28
29
  useWidgetEvent,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.32.0",
3
+ "version": "0.34.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",