@colixsystems/widget-sdk 0.218.0 → 0.219.1
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 +43 -2
- package/dist/contract.cjs +123 -18
- package/dist/contract.js +123 -18
- package/dist/hooks.js +10 -152
- package/dist/index.d.ts +243 -7
- package/dist/index.js +1 -1
- package/dist/index.native.js +1 -1
- package/dist/signing-hooks.js +449 -0
- package/dist/signing-state.js +140 -0
- package/package.json +1 -1
- package/src/contract.js +123 -18
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
32
32
|
| **CORE** | `useWidgetEvent(name)` | `(payload?) => void` | `ctx.events.emit` — no scope. The hook IS the emitter: `const emitSlot = useWidgetEvent("slotChosen")`, then `emitSlot(payload)`. Never destructure the result — there is no `emit` member. A sibling's input reads the payload, and a page action the author binds to the event (Run action, Navigate) runs with it — fired from one control, only that control runs it. |
|
|
33
33
|
| **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. |
|
|
34
34
|
| **CORE** | `useChildRenderer()` | `{ renderNode(node) }` | `ctx.renderer` — no scope (prefer the `WidgetTree` component) |
|
|
35
|
-
| **CORE** | `useFill()` | `boolean` | `ctx.fill` — no scope. `true` when the host sized this widget to fill
|
|
35
|
+
| **CORE** | `useFill()` | `boolean` | `ctx.fill` — no scope. `true` when the host sized this widget to fill a box of definite height: a widget in the section a fit-to-screen page stretches to the bottom of the screen, or one given a fixed slot height. Media-style widgets, and a chat or feed that scrolls its own content, switch to a `flex: 1` / `height: "100%"` layout and scroll inside it; others ignore it. Defaults `false`. |
|
|
36
36
|
| **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. |
|
|
37
37
|
| **CORE** | `isNarrowWidth(width)` | `boolean` | No context slice, no scope. True when a MEASURED width is below `NARROW_WIDTH_PX` (480) — the one threshold every widget switches at, so a page reflows together rather than raggedly. An unmeasured width (0) is NOT narrow, so nothing flashes through the narrow form on first paint. |
|
|
38
38
|
| **CORE** | `widthTier(width)` | `"narrow" \| "medium" \| "wide"` | No context slice, no scope. Which of THREE layouts a measured width calls for: narrow below `NARROW_WIDTH_PX` (480), wide at or above `WIDE_WIDTH_PX` (960), medium between. `widthTier(w) === "narrow"` exactly when `isNarrowWidth(w)`; an unmeasured width (0) is "wide". |
|
|
@@ -83,7 +83,48 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
83
83
|
|
|
84
84
|
## Status
|
|
85
85
|
|
|
86
|
-
`v0.
|
|
86
|
+
`v0.219.1` — 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**.
|
|
87
|
+
|
|
88
|
+
### What's new in 0.219.1
|
|
89
|
+
|
|
90
|
+
**`useFill()` says when it is true today.** It described a page-grid tile with a "Fill tile height" choice, a layout that no longer exists. A host now fills a widget in two cases: the widget sits in the section a fit-to-screen page stretches to the bottom of the screen, or the widget has a fixed slot height. A chat or feed that scrolls its own content is one of the widgets that should honour it. A documentation correction; `CONTRACT.version` is unchanged.
|
|
91
|
+
|
|
92
|
+
### What's new in 0.219.0 (contract 1.175.0)
|
|
93
|
+
|
|
94
|
+
**Signing rounds — `useSigningRounds(fileIds)` and `useSigningRound(fileId)`.** A signing round is how a PDF gets signed by a fixed list of people. Each signer gets a place on the signature page, in the order they were listed; they may sign in any order. Someone who manages the file's folder prepares the document for the signers they pick, looks at the prepared document, and confirms it; from then on each signer signs once with BankID through `useFileSignature()`, and the round is complete when the last one has signed. While a round is being prepared or signed the file cannot be changed, and once it is complete its content cannot be replaced.
|
|
95
|
+
|
|
96
|
+
| Hook | Returns | Use it for |
|
|
97
|
+
| --- | --- | --- |
|
|
98
|
+
| `useSigningRounds(fileIds)` | `{ byFileId, roundsEnabled, loading, error, refresh }` | A file list: what to show beside each file, in one request for the whole list. |
|
|
99
|
+
| `useSigningRound(fileId)` | `{ round, loading, error, refresh, prepare, preparedFile, confirm, cancel, busy }` | One file: its round, and starting, confirming or cancelling it. |
|
|
100
|
+
|
|
101
|
+
- `byFileId[fileId]` is `{ file_id, can_manage, can_prepare, round, locked }`. A file the user cannot see has no entry. `can_manage` says the user manages the file's folder; `can_prepare` says a round can be offered for it (a shared PDF with no round yet — preparing can still be refused once the document is read). `locked` is `"signing_round"` while the file cannot be changed and `"signed"` once its content cannot be replaced.
|
|
102
|
+
- **Who sees what.** `round` is `{ id, status, signer_count, signed_count, my_place, can_sign }`. Someone who manages the folder gets it in every state (`DRAFT`, `OPEN`, `COMPLETED`), with `signed_count`. A signer gets it once it is confirmed, with their own place in `my_place` (`{ position, signed, signed_at }`) and `can_sign` while the round is open and their place is unsigned; `signed_count` is `null` for them, because a signer does not learn how many of the others have signed. Anyone else gets `round: null` and learns only `locked`.
|
|
103
|
+
- **A workspace may not have signing rounds turned on.** `roundsEnabled` is then `false` (it is `null` until the first answer): offer no way to start a round. If `prepare()` is called anyway it is refused with code `SIGNING_ROUNDS_NOT_ENABLED`. Rounds that already exist keep working either way.
|
|
104
|
+
- **Starting a round.** `prepare(signerIds)` takes user ids in the order their places should appear on the signature page — the people from `useFileRoster(fileId)` — and can take up to a minute. `preparedFile()` then returns `{ url, sha256, expires_at, size_bytes }`: a link, good for five minutes, to the document as the signers will see it. Show it, then call `confirm()`, which opens the round for exactly that document; called before `preparedFile()` it is refused with code `PREVIEW_REQUIRED`. `cancel()` ends a draft or an open round and unlocks the file.
|
|
105
|
+
- **`can_prepare` and `roundsEnabled` answer different questions.** `can_prepare` is about the file and the user; `roundsEnabled` is about the workspace. Offer "prepare" only when both are true.
|
|
106
|
+
- **One action at a time.** While `busy` is true, calling another action is refused at once with code `ACTION_IN_PROGRESS`; it is not queued. Disable your buttons on `busy`.
|
|
107
|
+
- **What you see is always the current file's.** When `fileId` (or the set of ids, or the signed-in user) changes, the hooks return nothing of the previous one and `loading: true` until the new answer arrives. Ids are matched without regard to case. An answer that arrives late never overwrites a newer one.
|
|
108
|
+
- **Refusals.** A refused action sets `error` and rejects with the same object: `code` and `message` come from the server (show `message` as it is), `errors` has one entry per refused signer — `{ app_user_id, message, reason, characters }`, where `reason` is `empty`, `too_long`, `unsupported_characters` or `mixed_scripts` when the signer's name cannot be shown on the signature page — and `retryAfter` is the number of seconds to wait when the server is busy. The hook's own refusals (`PREVIEW_REQUIRED`, `ACTION_IN_PROGRESS`, `NO_SIGNING_ROUND`) carry a `message` you can show the same way. When a refusal means the round is no longer what you were showing (it expired, was closed, or the document changed), the hook reads the round again by itself.
|
|
109
|
+
|
|
110
|
+
```jsx
|
|
111
|
+
const { byFileId, roundsEnabled } = useSigningRounds(files.map((f) => f.id));
|
|
112
|
+
const { round, prepare, preparedFile, confirm, error, busy } = useSigningRound(file.id);
|
|
113
|
+
|
|
114
|
+
const overview = byFileId[file.id];
|
|
115
|
+
const canStart = roundsEnabled === true && overview && overview.can_prepare;
|
|
116
|
+
|
|
117
|
+
async function start(signerIds) {
|
|
118
|
+
await prepare(signerIds);
|
|
119
|
+
const { url } = await preparedFile(); // show the document at `url`, then:
|
|
120
|
+
await confirm();
|
|
121
|
+
}
|
|
122
|
+
// error.errors.map((e) => `${e.app_user_id}: ${e.message}`)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**`useFileSignature` tells you more, and no longer guesses.** It also returns `hintCode` (where the order stands: `"sealing"` means the user has signed and the signature is being added to the document, so there is nothing to scan) and `nameRefusal` (`{ reason, characters }`, set when the order failed because the signer's name cannot be shown). `cancel()` now resolves to what became of the order instead of always reporting `cancelled`: when the user had already signed, `status` stays `pending` with `hintCode: "sealing"`, or is `complete` — keep polling with `refresh()`. An order that is picked up again keeps its `message`. `autoStartToken` is cleared once the user has signed or the order has ended, a late poll can no longer undo a cancel, and the order is forgotten when `fileId` changes.
|
|
126
|
+
|
|
127
|
+
`index.d.ts` now declares `useFileSignature`, `useFileSignatures` and `useFileRoster`. Requires `@colixsystems/filestore-client@0.12.0`.
|
|
87
128
|
|
|
88
129
|
### What's new in 0.218.0
|
|
89
130
|
|
package/dist/contract.cjs
CHANGED
|
@@ -942,13 +942,14 @@ const HOOKS = [
|
|
|
942
942
|
name: "useFill",
|
|
943
943
|
signature: "useFill()",
|
|
944
944
|
description:
|
|
945
|
-
"Returns true when the host has sized this widget to fill
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
"
|
|
949
|
-
"Chart, Map, Video
|
|
950
|
-
|
|
951
|
-
"
|
|
945
|
+
"Returns true when the host has sized this widget to fill a box of " +
|
|
946
|
+
"definite height — a widget in the section a fit-to-screen page " +
|
|
947
|
+
"stretches to the bottom of the screen, or a widget given a fixed slot " +
|
|
948
|
+
"height. When true, a widget that has a meaningful filled form (Image, " +
|
|
949
|
+
"Chart, Map, Video, a chat or feed that scrolls its own content) should " +
|
|
950
|
+
"switch from its intrinsic height to a stretch style (flex: 1 / " +
|
|
951
|
+
'height: "100%") and scroll inside it; widgets with no useful filled ' +
|
|
952
|
+
"form may ignore it. Defaults to false wherever the host has not opted the " +
|
|
952
953
|
"widget into filling, so calling it is always safe. The SAME value is " +
|
|
953
954
|
"injected by the web Player and the native export (widget-parity skill), so a " +
|
|
954
955
|
"widget's fill behaviour is identical on both platforms.",
|
|
@@ -1511,7 +1512,15 @@ const HOOKS = [
|
|
|
1511
1512
|
"polls (fresh QR while pending — render with the Image primitive), " +
|
|
1512
1513
|
"cancel() aborts, verify() returns the offline verdict. Pass " +
|
|
1513
1514
|
"existingSignatureId for an already-signed file to seed the complete " +
|
|
1514
|
-
"state + verify() without re-signing.
|
|
1515
|
+
"state + verify() without re-signing. autoStartToken is set only while " +
|
|
1516
|
+
"opening BankID can still help (null once the user has signed or the " +
|
|
1517
|
+
"order has ended). hintCode says where the order " +
|
|
1518
|
+
"stands; 'sealing' means the user has signed and the signature is being " +
|
|
1519
|
+
"added to the document, so there is nothing to scan and it can no longer " +
|
|
1520
|
+
"be cancelled. cancel() resolves to what became of the order, which is " +
|
|
1521
|
+
"then still pending or already complete: read status after it, never " +
|
|
1522
|
+
"assume 'cancelled'. nameRefusal is set when the order failed because " +
|
|
1523
|
+
"the signer's name cannot be shown on the document. Reads " +
|
|
1515
1524
|
"ctx.filestore.signatures.{initiate,status,cancel,verify}.",
|
|
1516
1525
|
returnShape: {
|
|
1517
1526
|
status: "'pending' | 'complete' | 'failed' | 'cancelled' | null",
|
|
@@ -1520,12 +1529,15 @@ const HOOKS = [
|
|
|
1520
1529
|
message: "string | null",
|
|
1521
1530
|
signerName: "string | null",
|
|
1522
1531
|
signedAt: "string | null",
|
|
1532
|
+
hintCode: "string | null // where the order stands, e.g. 'sealing'",
|
|
1533
|
+
nameRefusal:
|
|
1534
|
+
"{ reason: 'empty' | 'too_long' | 'unsupported_characters' | 'mixed_scripts', characters: string[] } | null",
|
|
1523
1535
|
verdict: "{ valid, checks, content_status, signed_by_others_since, seal, ... } | null",
|
|
1524
1536
|
loading: "boolean",
|
|
1525
1537
|
error: "PermissionError | null // { code, message, retryable }",
|
|
1526
|
-
initiate: "() => Promise<{ signature_id, qr, auto_start_token, status }>",
|
|
1538
|
+
initiate: "() => Promise<{ signature_id, qr, auto_start_token, status, hint_code?, message? }>",
|
|
1527
1539
|
refresh: "() => Promise<void>",
|
|
1528
|
-
cancel: "() => Promise<
|
|
1540
|
+
cancel: "() => Promise<{ status, hint_code?, message?, signer_name?, signed_at? } | undefined>",
|
|
1529
1541
|
verify: "() => Promise<verdict>",
|
|
1530
1542
|
},
|
|
1531
1543
|
requiredContextSlice: ["filestore.signatures"],
|
|
@@ -1572,6 +1584,91 @@ const HOOKS = [
|
|
|
1572
1584
|
requiredContextSlice: ["filestore.signatures"],
|
|
1573
1585
|
scopes: ["files.read:*"],
|
|
1574
1586
|
},
|
|
1587
|
+
{
|
|
1588
|
+
name: "useSigningRounds",
|
|
1589
|
+
signature: "useSigningRounds(fileIds)",
|
|
1590
|
+
description:
|
|
1591
|
+
"What the signed-in user may know about the signing round of each file " +
|
|
1592
|
+
"in a list, in one batched request. A signing round fixes a PDF and a " +
|
|
1593
|
+
"list of signers: it is prepared and confirmed by someone who manages " +
|
|
1594
|
+
"the file's folder, then each signer signs once with useFileSignature(), " +
|
|
1595
|
+
"in any order (there are no turns; the list only orders the places on " +
|
|
1596
|
+
"the signature page). byFileId maps a file id to { file_id, can_manage, " +
|
|
1597
|
+
"can_prepare, round, locked }; a file the user cannot see is absent. " +
|
|
1598
|
+
"round is null when there is none, and for a user who is neither a " +
|
|
1599
|
+
"manager nor one of its signers; a signer sees it once it is confirmed, " +
|
|
1600
|
+
"with their own place (my_place) and can_sign (the round is open and " +
|
|
1601
|
+
"their place is unsigned), and signed_count is null " +
|
|
1602
|
+
"for them. locked is 'signing_round' while the file cannot be changed, " +
|
|
1603
|
+
"'signed' once its content cannot be replaced. roundsEnabled is false " +
|
|
1604
|
+
"when the workspace cannot start signing rounds (offer no way to start " +
|
|
1605
|
+
"one), and null until the first answer. can_prepare is about the file " +
|
|
1606
|
+
"and the user, roundsEnabled is about the workspace: offer 'prepare' " +
|
|
1607
|
+
"only when both are true. Ids are matched without regard to case. " +
|
|
1608
|
+
"Refetches only when the set of ids or the signed-in user changes; " +
|
|
1609
|
+
"until the new answer arrives loading is true and byFileId is empty. " +
|
|
1610
|
+
"Reads ctx.filestore.signingRounds.overview.",
|
|
1611
|
+
returnShape: {
|
|
1612
|
+
byFileId:
|
|
1613
|
+
"{ [fileId]: { file_id, can_manage: boolean, can_prepare: boolean, " +
|
|
1614
|
+
"round: { id, status: 'DRAFT' | 'OPEN' | 'COMPLETED', signer_count, signed_count: number | null, " +
|
|
1615
|
+
"my_place: { position, signed, signed_at } | null, can_sign: boolean } | null, " +
|
|
1616
|
+
"locked: 'signing_round' | 'signed' | null } }",
|
|
1617
|
+
roundsEnabled: "boolean | null",
|
|
1618
|
+
loading: "boolean",
|
|
1619
|
+
error: "PermissionError | null // { code, message, retryable, errors, retryAfter }",
|
|
1620
|
+
refresh: "() => Promise<void>",
|
|
1621
|
+
},
|
|
1622
|
+
requiredContextSlice: ["filestore.signingRounds"],
|
|
1623
|
+
scopes: ["files.read:*"],
|
|
1624
|
+
},
|
|
1625
|
+
{
|
|
1626
|
+
name: "useSigningRound",
|
|
1627
|
+
signature: "useSigningRound(fileId)",
|
|
1628
|
+
description:
|
|
1629
|
+
"One file's current signing round, and the actions of someone who " +
|
|
1630
|
+
"manages the file's folder. prepare(signerIds) drafts a round for those " +
|
|
1631
|
+
"users, their places on the signature page in the order given (take the " +
|
|
1632
|
+
"ids from useFileRoster()); it can take " +
|
|
1633
|
+
"up to a minute. preparedFile() returns a five-minute link to the " +
|
|
1634
|
+
"prepared document; show it to the user before confirming. confirm() " +
|
|
1635
|
+
"opens the round for exactly the document preparedFile() last returned, " +
|
|
1636
|
+
"and rejects with code PREVIEW_REQUIRED when it was not called first. " +
|
|
1637
|
+
"cancel() ends a draft or an open round and resolves to the cancelled " +
|
|
1638
|
+
"round (status 'CANCELLED'); round is then null. ONE ACTION RUNS AT A " +
|
|
1639
|
+
"TIME: while busy is true any other action is refused at once with code " +
|
|
1640
|
+
"ACTION_IN_PROGRESS (it is not queued), so disable your buttons on busy. " +
|
|
1641
|
+
"round is the full round as the " +
|
|
1642
|
+
"user may see it: a manager sees every signer's progress, a signer sees " +
|
|
1643
|
+
"every signer's name and position and their own progress only, anyone " +
|
|
1644
|
+
"else sees null. A refused action sets error and rejects with it: code " +
|
|
1645
|
+
"and message come from the server (show message as it is), errors has " +
|
|
1646
|
+
"one entry per refused signer ({ app_user_id, message, reason, " +
|
|
1647
|
+
"characters }), retryAfter is the seconds to wait. The hook's own " +
|
|
1648
|
+
"refusals (PREVIEW_REQUIRED, ACTION_IN_PROGRESS, NO_SIGNING_ROUND) carry " +
|
|
1649
|
+
"a message that can be shown the same way. When a refusal means the " +
|
|
1650
|
+
"round is no longer what was shown (it expired, was closed, or the " +
|
|
1651
|
+
"document changed) the hook reads the round again by itself. Code " +
|
|
1652
|
+
"SIGNING_ROUNDS_NOT_ENABLED means the workspace cannot start signing " +
|
|
1653
|
+
"rounds; check roundsEnabled from useSigningRounds() before offering to " +
|
|
1654
|
+
"start one. When fileId or the signed-in user changes, round is null " +
|
|
1655
|
+
"and loading true until the new answer. Reads ctx.filestore.signingRounds.",
|
|
1656
|
+
returnShape: {
|
|
1657
|
+
round:
|
|
1658
|
+
"{ id, file_id, status: 'DRAFT' | 'OPEN' | 'COMPLETED', created_at, confirmed_at, completed_at, expires_at, " +
|
|
1659
|
+
"signers: { name, position, app_user_id?, signed?, signed_at?, signed_as? }[] } | null",
|
|
1660
|
+
loading: "boolean",
|
|
1661
|
+
error: "PermissionError | null // { code, message, retryable, errors, retryAfter }",
|
|
1662
|
+
refresh: "() => Promise<void>",
|
|
1663
|
+
prepare: "(signerIds: string[]) => Promise<round> // status 'DRAFT'",
|
|
1664
|
+
preparedFile: "() => Promise<{ url, sha256, expires_at, size_bytes }>",
|
|
1665
|
+
confirm: "() => Promise<round> // status 'OPEN'",
|
|
1666
|
+
cancel: "() => Promise<round> // the round it ended, status 'CANCELLED'",
|
|
1667
|
+
busy: "boolean // an action is running; another one is refused with ACTION_IN_PROGRESS",
|
|
1668
|
+
},
|
|
1669
|
+
requiredContextSlice: ["filestore.signingRounds"],
|
|
1670
|
+
scopes: ["files.write:*"],
|
|
1671
|
+
},
|
|
1575
1672
|
{
|
|
1576
1673
|
name: "useFolderPermissions",
|
|
1577
1674
|
signature: "useFolderPermissions(folderId, { enabled })",
|
|
@@ -3453,15 +3550,16 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
3453
3550
|
},
|
|
3454
3551
|
},
|
|
3455
3552
|
// REQ-LAY-08 — optional host layout hint backing useFill(). True when the
|
|
3456
|
-
// host sized this widget to fill
|
|
3457
|
-
// page
|
|
3553
|
+
// host sized this widget to fill a box of definite height (a fit-to-screen
|
|
3554
|
+
// page's stretched section, or a fixed slot height).
|
|
3458
3555
|
// Optional: a host that never fills a widget simply omits it and useFill()
|
|
3459
3556
|
// returns false, so existing hosts need no change.
|
|
3460
3557
|
fill: {
|
|
3461
3558
|
description:
|
|
3462
3559
|
"Optional host layout hint (boolean). True when the host sized this " +
|
|
3463
|
-
"widget to fill
|
|
3464
|
-
"
|
|
3560
|
+
"widget to fill a box of definite height (a fit-to-screen page's " +
|
|
3561
|
+
"stretched section, or a fixed slot height). Backs useFill(); defaults " +
|
|
3562
|
+
"to false when absent.",
|
|
3465
3563
|
required: false,
|
|
3466
3564
|
fields: {},
|
|
3467
3565
|
},
|
|
@@ -3592,10 +3690,12 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
3592
3690
|
"Injected @colixsystems/filestore-client instance — the end-user file archive (project / personal / public spaces) + BankID file signing. " +
|
|
3593
3691
|
"{ files: { list(query) -> Promise<{ data, meta }>, get(id), upload(formData), update(id, body), copy(id, body), remove(id), preview(id) }, " +
|
|
3594
3692
|
"folders: { list, create, update, remove }, shares: { ... }, trash: { ... }, " +
|
|
3595
|
-
"signatures: { initiate(fileId), status(id), cancel(id), verify(id) },
|
|
3596
|
-
"
|
|
3693
|
+
"signatures: { list(fileIds), roster(fileId, query), initiate(fileId), status(id), cancel(id), verify(id) }, " +
|
|
3694
|
+
"signingRounds: { overview(fileIds), list(fileId, query), get(roundId), prepare(fileId, signerIds), preparedFile(roundId), confirm(roundId, preparedSha256), cancel(roundId) }, " +
|
|
3695
|
+
"objectUrl(token), fetchObject(token) }. " +
|
|
3696
|
+
"Backs useFilestoreFiles() + useFilestoreFolders() + useFilestoreCreateFolder() + useFilestoreCopy() + useFileSignature() + useFileSignatures() + useFileRoster() + useSigningRounds() + useSigningRound(). Optional — a host without an end-user filestore omits it and those hooks throw a clear error.",
|
|
3597
3697
|
required: false,
|
|
3598
|
-
fields: { files: "object", folders: "object", shares: "object", signatures: "object" },
|
|
3698
|
+
fields: { files: "object", folders: "object", shares: "object", signatures: "object", signingRounds: "object" },
|
|
3599
3699
|
},
|
|
3600
3700
|
renderer: {
|
|
3601
3701
|
description:
|
|
@@ -5707,7 +5807,12 @@ const CONTRACT = deepFreeze({
|
|
|
5707
5807
|
// binds no widget style fields: the component reads it on both hosts,
|
|
5708
5808
|
// which a scope now states as `sdkComponents`.
|
|
5709
5809
|
// Additive: an unthemed chip is the muted pill widgets drew by hand.
|
|
5710
|
-
|
|
5810
|
+
// 1.175.0: additive (sc-10410) — `useSigningRounds(fileIds)` and
|
|
5811
|
+
// `useSigningRound(fileId)` read and run PDF signing rounds through the
|
|
5812
|
+
// new `ctx.filestore.signingRounds`. `useFileSignature` also returns
|
|
5813
|
+
// `hintCode` and `nameRefusal`, and its `cancel()` resolves to what
|
|
5814
|
+
// became of the order instead of always reporting "cancelled".
|
|
5815
|
+
version: "1.175.0",
|
|
5711
5816
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5712
5817
|
hooks: HOOKS,
|
|
5713
5818
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -942,13 +942,14 @@ const HOOKS = [
|
|
|
942
942
|
name: "useFill",
|
|
943
943
|
signature: "useFill()",
|
|
944
944
|
description:
|
|
945
|
-
"Returns true when the host has sized this widget to fill
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
"
|
|
949
|
-
"Chart, Map, Video
|
|
950
|
-
|
|
951
|
-
"
|
|
945
|
+
"Returns true when the host has sized this widget to fill a box of " +
|
|
946
|
+
"definite height — a widget in the section a fit-to-screen page " +
|
|
947
|
+
"stretches to the bottom of the screen, or a widget given a fixed slot " +
|
|
948
|
+
"height. When true, a widget that has a meaningful filled form (Image, " +
|
|
949
|
+
"Chart, Map, Video, a chat or feed that scrolls its own content) should " +
|
|
950
|
+
"switch from its intrinsic height to a stretch style (flex: 1 / " +
|
|
951
|
+
'height: "100%") and scroll inside it; widgets with no useful filled ' +
|
|
952
|
+
"form may ignore it. Defaults to false wherever the host has not opted the " +
|
|
952
953
|
"widget into filling, so calling it is always safe. The SAME value is " +
|
|
953
954
|
"injected by the web Player and the native export (widget-parity skill), so a " +
|
|
954
955
|
"widget's fill behaviour is identical on both platforms.",
|
|
@@ -1511,7 +1512,15 @@ const HOOKS = [
|
|
|
1511
1512
|
"polls (fresh QR while pending — render with the Image primitive), " +
|
|
1512
1513
|
"cancel() aborts, verify() returns the offline verdict. Pass " +
|
|
1513
1514
|
"existingSignatureId for an already-signed file to seed the complete " +
|
|
1514
|
-
"state + verify() without re-signing.
|
|
1515
|
+
"state + verify() without re-signing. autoStartToken is set only while " +
|
|
1516
|
+
"opening BankID can still help (null once the user has signed or the " +
|
|
1517
|
+
"order has ended). hintCode says where the order " +
|
|
1518
|
+
"stands; 'sealing' means the user has signed and the signature is being " +
|
|
1519
|
+
"added to the document, so there is nothing to scan and it can no longer " +
|
|
1520
|
+
"be cancelled. cancel() resolves to what became of the order, which is " +
|
|
1521
|
+
"then still pending or already complete: read status after it, never " +
|
|
1522
|
+
"assume 'cancelled'. nameRefusal is set when the order failed because " +
|
|
1523
|
+
"the signer's name cannot be shown on the document. Reads " +
|
|
1515
1524
|
"ctx.filestore.signatures.{initiate,status,cancel,verify}.",
|
|
1516
1525
|
returnShape: {
|
|
1517
1526
|
status: "'pending' | 'complete' | 'failed' | 'cancelled' | null",
|
|
@@ -1520,12 +1529,15 @@ const HOOKS = [
|
|
|
1520
1529
|
message: "string | null",
|
|
1521
1530
|
signerName: "string | null",
|
|
1522
1531
|
signedAt: "string | null",
|
|
1532
|
+
hintCode: "string | null // where the order stands, e.g. 'sealing'",
|
|
1533
|
+
nameRefusal:
|
|
1534
|
+
"{ reason: 'empty' | 'too_long' | 'unsupported_characters' | 'mixed_scripts', characters: string[] } | null",
|
|
1523
1535
|
verdict: "{ valid, checks, content_status, signed_by_others_since, seal, ... } | null",
|
|
1524
1536
|
loading: "boolean",
|
|
1525
1537
|
error: "PermissionError | null // { code, message, retryable }",
|
|
1526
|
-
initiate: "() => Promise<{ signature_id, qr, auto_start_token, status }>",
|
|
1538
|
+
initiate: "() => Promise<{ signature_id, qr, auto_start_token, status, hint_code?, message? }>",
|
|
1527
1539
|
refresh: "() => Promise<void>",
|
|
1528
|
-
cancel: "() => Promise<
|
|
1540
|
+
cancel: "() => Promise<{ status, hint_code?, message?, signer_name?, signed_at? } | undefined>",
|
|
1529
1541
|
verify: "() => Promise<verdict>",
|
|
1530
1542
|
},
|
|
1531
1543
|
requiredContextSlice: ["filestore.signatures"],
|
|
@@ -1572,6 +1584,91 @@ const HOOKS = [
|
|
|
1572
1584
|
requiredContextSlice: ["filestore.signatures"],
|
|
1573
1585
|
scopes: ["files.read:*"],
|
|
1574
1586
|
},
|
|
1587
|
+
{
|
|
1588
|
+
name: "useSigningRounds",
|
|
1589
|
+
signature: "useSigningRounds(fileIds)",
|
|
1590
|
+
description:
|
|
1591
|
+
"What the signed-in user may know about the signing round of each file " +
|
|
1592
|
+
"in a list, in one batched request. A signing round fixes a PDF and a " +
|
|
1593
|
+
"list of signers: it is prepared and confirmed by someone who manages " +
|
|
1594
|
+
"the file's folder, then each signer signs once with useFileSignature(), " +
|
|
1595
|
+
"in any order (there are no turns; the list only orders the places on " +
|
|
1596
|
+
"the signature page). byFileId maps a file id to { file_id, can_manage, " +
|
|
1597
|
+
"can_prepare, round, locked }; a file the user cannot see is absent. " +
|
|
1598
|
+
"round is null when there is none, and for a user who is neither a " +
|
|
1599
|
+
"manager nor one of its signers; a signer sees it once it is confirmed, " +
|
|
1600
|
+
"with their own place (my_place) and can_sign (the round is open and " +
|
|
1601
|
+
"their place is unsigned), and signed_count is null " +
|
|
1602
|
+
"for them. locked is 'signing_round' while the file cannot be changed, " +
|
|
1603
|
+
"'signed' once its content cannot be replaced. roundsEnabled is false " +
|
|
1604
|
+
"when the workspace cannot start signing rounds (offer no way to start " +
|
|
1605
|
+
"one), and null until the first answer. can_prepare is about the file " +
|
|
1606
|
+
"and the user, roundsEnabled is about the workspace: offer 'prepare' " +
|
|
1607
|
+
"only when both are true. Ids are matched without regard to case. " +
|
|
1608
|
+
"Refetches only when the set of ids or the signed-in user changes; " +
|
|
1609
|
+
"until the new answer arrives loading is true and byFileId is empty. " +
|
|
1610
|
+
"Reads ctx.filestore.signingRounds.overview.",
|
|
1611
|
+
returnShape: {
|
|
1612
|
+
byFileId:
|
|
1613
|
+
"{ [fileId]: { file_id, can_manage: boolean, can_prepare: boolean, " +
|
|
1614
|
+
"round: { id, status: 'DRAFT' | 'OPEN' | 'COMPLETED', signer_count, signed_count: number | null, " +
|
|
1615
|
+
"my_place: { position, signed, signed_at } | null, can_sign: boolean } | null, " +
|
|
1616
|
+
"locked: 'signing_round' | 'signed' | null } }",
|
|
1617
|
+
roundsEnabled: "boolean | null",
|
|
1618
|
+
loading: "boolean",
|
|
1619
|
+
error: "PermissionError | null // { code, message, retryable, errors, retryAfter }",
|
|
1620
|
+
refresh: "() => Promise<void>",
|
|
1621
|
+
},
|
|
1622
|
+
requiredContextSlice: ["filestore.signingRounds"],
|
|
1623
|
+
scopes: ["files.read:*"],
|
|
1624
|
+
},
|
|
1625
|
+
{
|
|
1626
|
+
name: "useSigningRound",
|
|
1627
|
+
signature: "useSigningRound(fileId)",
|
|
1628
|
+
description:
|
|
1629
|
+
"One file's current signing round, and the actions of someone who " +
|
|
1630
|
+
"manages the file's folder. prepare(signerIds) drafts a round for those " +
|
|
1631
|
+
"users, their places on the signature page in the order given (take the " +
|
|
1632
|
+
"ids from useFileRoster()); it can take " +
|
|
1633
|
+
"up to a minute. preparedFile() returns a five-minute link to the " +
|
|
1634
|
+
"prepared document; show it to the user before confirming. confirm() " +
|
|
1635
|
+
"opens the round for exactly the document preparedFile() last returned, " +
|
|
1636
|
+
"and rejects with code PREVIEW_REQUIRED when it was not called first. " +
|
|
1637
|
+
"cancel() ends a draft or an open round and resolves to the cancelled " +
|
|
1638
|
+
"round (status 'CANCELLED'); round is then null. ONE ACTION RUNS AT A " +
|
|
1639
|
+
"TIME: while busy is true any other action is refused at once with code " +
|
|
1640
|
+
"ACTION_IN_PROGRESS (it is not queued), so disable your buttons on busy. " +
|
|
1641
|
+
"round is the full round as the " +
|
|
1642
|
+
"user may see it: a manager sees every signer's progress, a signer sees " +
|
|
1643
|
+
"every signer's name and position and their own progress only, anyone " +
|
|
1644
|
+
"else sees null. A refused action sets error and rejects with it: code " +
|
|
1645
|
+
"and message come from the server (show message as it is), errors has " +
|
|
1646
|
+
"one entry per refused signer ({ app_user_id, message, reason, " +
|
|
1647
|
+
"characters }), retryAfter is the seconds to wait. The hook's own " +
|
|
1648
|
+
"refusals (PREVIEW_REQUIRED, ACTION_IN_PROGRESS, NO_SIGNING_ROUND) carry " +
|
|
1649
|
+
"a message that can be shown the same way. When a refusal means the " +
|
|
1650
|
+
"round is no longer what was shown (it expired, was closed, or the " +
|
|
1651
|
+
"document changed) the hook reads the round again by itself. Code " +
|
|
1652
|
+
"SIGNING_ROUNDS_NOT_ENABLED means the workspace cannot start signing " +
|
|
1653
|
+
"rounds; check roundsEnabled from useSigningRounds() before offering to " +
|
|
1654
|
+
"start one. When fileId or the signed-in user changes, round is null " +
|
|
1655
|
+
"and loading true until the new answer. Reads ctx.filestore.signingRounds.",
|
|
1656
|
+
returnShape: {
|
|
1657
|
+
round:
|
|
1658
|
+
"{ id, file_id, status: 'DRAFT' | 'OPEN' | 'COMPLETED', created_at, confirmed_at, completed_at, expires_at, " +
|
|
1659
|
+
"signers: { name, position, app_user_id?, signed?, signed_at?, signed_as? }[] } | null",
|
|
1660
|
+
loading: "boolean",
|
|
1661
|
+
error: "PermissionError | null // { code, message, retryable, errors, retryAfter }",
|
|
1662
|
+
refresh: "() => Promise<void>",
|
|
1663
|
+
prepare: "(signerIds: string[]) => Promise<round> // status 'DRAFT'",
|
|
1664
|
+
preparedFile: "() => Promise<{ url, sha256, expires_at, size_bytes }>",
|
|
1665
|
+
confirm: "() => Promise<round> // status 'OPEN'",
|
|
1666
|
+
cancel: "() => Promise<round> // the round it ended, status 'CANCELLED'",
|
|
1667
|
+
busy: "boolean // an action is running; another one is refused with ACTION_IN_PROGRESS",
|
|
1668
|
+
},
|
|
1669
|
+
requiredContextSlice: ["filestore.signingRounds"],
|
|
1670
|
+
scopes: ["files.write:*"],
|
|
1671
|
+
},
|
|
1575
1672
|
{
|
|
1576
1673
|
name: "useFolderPermissions",
|
|
1577
1674
|
signature: "useFolderPermissions(folderId, { enabled })",
|
|
@@ -3449,15 +3546,16 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
3449
3546
|
},
|
|
3450
3547
|
},
|
|
3451
3548
|
// REQ-LAY-08 — optional host layout hint backing useFill(). True when the
|
|
3452
|
-
// host sized this widget to fill
|
|
3453
|
-
// page
|
|
3549
|
+
// host sized this widget to fill a box of definite height (a fit-to-screen
|
|
3550
|
+
// page's stretched section, or a fixed slot height).
|
|
3454
3551
|
// Optional: a host that never fills a widget simply omits it and useFill()
|
|
3455
3552
|
// returns false, so existing hosts need no change.
|
|
3456
3553
|
fill: {
|
|
3457
3554
|
description:
|
|
3458
3555
|
"Optional host layout hint (boolean). True when the host sized this " +
|
|
3459
|
-
"widget to fill
|
|
3460
|
-
"
|
|
3556
|
+
"widget to fill a box of definite height (a fit-to-screen page's " +
|
|
3557
|
+
"stretched section, or a fixed slot height). Backs useFill(); defaults " +
|
|
3558
|
+
"to false when absent.",
|
|
3461
3559
|
required: false,
|
|
3462
3560
|
fields: {},
|
|
3463
3561
|
},
|
|
@@ -3588,10 +3686,12 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
3588
3686
|
"Injected @colixsystems/filestore-client instance — the end-user file archive (project / personal / public spaces) + BankID file signing. " +
|
|
3589
3687
|
"{ files: { list(query) -> Promise<{ data, meta }>, get(id), upload(formData), update(id, body), copy(id, body), remove(id), preview(id) }, " +
|
|
3590
3688
|
"folders: { list, create, update, remove }, shares: { ... }, trash: { ... }, " +
|
|
3591
|
-
"signatures: { initiate(fileId), status(id), cancel(id), verify(id) },
|
|
3592
|
-
"
|
|
3689
|
+
"signatures: { list(fileIds), roster(fileId, query), initiate(fileId), status(id), cancel(id), verify(id) }, " +
|
|
3690
|
+
"signingRounds: { overview(fileIds), list(fileId, query), get(roundId), prepare(fileId, signerIds), preparedFile(roundId), confirm(roundId, preparedSha256), cancel(roundId) }, " +
|
|
3691
|
+
"objectUrl(token), fetchObject(token) }. " +
|
|
3692
|
+
"Backs useFilestoreFiles() + useFilestoreFolders() + useFilestoreCreateFolder() + useFilestoreCopy() + useFileSignature() + useFileSignatures() + useFileRoster() + useSigningRounds() + useSigningRound(). Optional — a host without an end-user filestore omits it and those hooks throw a clear error.",
|
|
3593
3693
|
required: false,
|
|
3594
|
-
fields: { files: "object", folders: "object", shares: "object", signatures: "object" },
|
|
3694
|
+
fields: { files: "object", folders: "object", shares: "object", signatures: "object", signingRounds: "object" },
|
|
3595
3695
|
},
|
|
3596
3696
|
renderer: {
|
|
3597
3697
|
description:
|
|
@@ -5703,7 +5803,12 @@ const CONTRACT = deepFreeze({
|
|
|
5703
5803
|
// binds no widget style fields: the component reads it on both hosts,
|
|
5704
5804
|
// which a scope now states as `sdkComponents`.
|
|
5705
5805
|
// Additive: an unthemed chip is the muted pill widgets drew by hand.
|
|
5706
|
-
|
|
5806
|
+
// 1.175.0: additive (sc-10410) — `useSigningRounds(fileIds)` and
|
|
5807
|
+
// `useSigningRound(fileId)` read and run PDF signing rounds through the
|
|
5808
|
+
// new `ctx.filestore.signingRounds`. `useFileSignature` also returns
|
|
5809
|
+
// `hintCode` and `nameRefusal`, and its `cancel()` resolves to what
|
|
5810
|
+
// became of the order instead of always reporting "cancelled".
|
|
5811
|
+
version: "1.175.0",
|
|
5707
5812
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5708
5813
|
hooks: HOOKS,
|
|
5709
5814
|
primitives: PRIMITIVES,
|