@colixsystems/widget-sdk 0.215.0 → 0.219.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
@@ -83,7 +83,53 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
83
83
 
84
84
  ## Status
85
85
 
86
- `v0.215.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**.
86
+ `v0.219.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**.
87
+
88
+ ### What's new in 0.219.0 (contract 1.175.0)
89
+
90
+ **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.
91
+
92
+ | Hook | Returns | Use it for |
93
+ | --- | --- | --- |
94
+ | `useSigningRounds(fileIds)` | `{ byFileId, roundsEnabled, loading, error, refresh }` | A file list: what to show beside each file, in one request for the whole list. |
95
+ | `useSigningRound(fileId)` | `{ round, loading, error, refresh, prepare, preparedFile, confirm, cancel, busy }` | One file: its round, and starting, confirming or cancelling it. |
96
+
97
+ - `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.
98
+ - **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`.
99
+ - **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.
100
+ - **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.
101
+ - **`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.
102
+ - **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`.
103
+ - **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.
104
+ - **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.
105
+
106
+ ```jsx
107
+ const { byFileId, roundsEnabled } = useSigningRounds(files.map((f) => f.id));
108
+ const { round, prepare, preparedFile, confirm, error, busy } = useSigningRound(file.id);
109
+
110
+ const overview = byFileId[file.id];
111
+ const canStart = roundsEnabled === true && overview && overview.can_prepare;
112
+
113
+ async function start(signerIds) {
114
+ await prepare(signerIds);
115
+ const { url } = await preparedFile(); // show the document at `url`, then:
116
+ await confirm();
117
+ }
118
+ // error.errors.map((e) => `${e.app_user_id}: ${e.message}`)
119
+ ```
120
+
121
+ **`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.
122
+
123
+ `index.d.ts` now declares `useFileSignature`, `useFileSignatures` and `useFileRoster`. Requires `@colixsystems/filestore-client@0.12.0`.
124
+
125
+ ### What's new in 0.218.0
126
+
127
+ **Host helpers for the app logo's dark-bar variant.** An app can now carry a second logo for dark navigation bars, stored beside the app logo as `themeConfig.appLogoDarkAssetId` / `appLogoDarkUrl`. Two helpers on `@colixsystems/widget-sdk/host` decide which one a bar draws, and both hosts ask them, so the rail, the drawer, the app header and the site header pick the same logo in the web Player and in the exported app:
128
+
129
+ - `isDarkSurface({ surface, backdrop, textColor, theme })` — `true` when the bar reads as dark. Pass the colour the bar paints as `surface`; a see-through `#RRGGBBAA` is composited over `backdrop`, or over the page in `theme`, first. A bar that paints nothing passes the `textColor` authored to sit on it instead: light text names a dark backdrop.
130
+ - `resolveAppLogo(logo, darkSurfaceLogo, onDark)` — the logo to draw. The variant is used only beside an app logo; it never stands in for a missing one.
131
+
132
+ These are host-integration helpers, not something a widget imports. Nothing changes for widget authors.
87
133
 
88
134
  ### What's new in 0.215.0 (contract 1.174.0)
89
135
 
package/dist/contract.cjs CHANGED
@@ -1511,7 +1511,15 @@ const HOOKS = [
1511
1511
  "polls (fresh QR while pending — render with the Image primitive), " +
1512
1512
  "cancel() aborts, verify() returns the offline verdict. Pass " +
1513
1513
  "existingSignatureId for an already-signed file to seed the complete " +
1514
- "state + verify() without re-signing. Reads " +
1514
+ "state + verify() without re-signing. autoStartToken is set only while " +
1515
+ "opening BankID can still help (null once the user has signed or the " +
1516
+ "order has ended). hintCode says where the order " +
1517
+ "stands; 'sealing' means the user has signed and the signature is being " +
1518
+ "added to the document, so there is nothing to scan and it can no longer " +
1519
+ "be cancelled. cancel() resolves to what became of the order, which is " +
1520
+ "then still pending or already complete: read status after it, never " +
1521
+ "assume 'cancelled'. nameRefusal is set when the order failed because " +
1522
+ "the signer's name cannot be shown on the document. Reads " +
1515
1523
  "ctx.filestore.signatures.{initiate,status,cancel,verify}.",
1516
1524
  returnShape: {
1517
1525
  status: "'pending' | 'complete' | 'failed' | 'cancelled' | null",
@@ -1520,12 +1528,15 @@ const HOOKS = [
1520
1528
  message: "string | null",
1521
1529
  signerName: "string | null",
1522
1530
  signedAt: "string | null",
1531
+ hintCode: "string | null // where the order stands, e.g. 'sealing'",
1532
+ nameRefusal:
1533
+ "{ reason: 'empty' | 'too_long' | 'unsupported_characters' | 'mixed_scripts', characters: string[] } | null",
1523
1534
  verdict: "{ valid, checks, content_status, signed_by_others_since, seal, ... } | null",
1524
1535
  loading: "boolean",
1525
1536
  error: "PermissionError | null // { code, message, retryable }",
1526
- initiate: "() => Promise<{ signature_id, qr, auto_start_token, status }>",
1537
+ initiate: "() => Promise<{ signature_id, qr, auto_start_token, status, hint_code?, message? }>",
1527
1538
  refresh: "() => Promise<void>",
1528
- cancel: "() => Promise<void>",
1539
+ cancel: "() => Promise<{ status, hint_code?, message?, signer_name?, signed_at? } | undefined>",
1529
1540
  verify: "() => Promise<verdict>",
1530
1541
  },
1531
1542
  requiredContextSlice: ["filestore.signatures"],
@@ -1572,6 +1583,91 @@ const HOOKS = [
1572
1583
  requiredContextSlice: ["filestore.signatures"],
1573
1584
  scopes: ["files.read:*"],
1574
1585
  },
1586
+ {
1587
+ name: "useSigningRounds",
1588
+ signature: "useSigningRounds(fileIds)",
1589
+ description:
1590
+ "What the signed-in user may know about the signing round of each file " +
1591
+ "in a list, in one batched request. A signing round fixes a PDF and a " +
1592
+ "list of signers: it is prepared and confirmed by someone who manages " +
1593
+ "the file's folder, then each signer signs once with useFileSignature(), " +
1594
+ "in any order (there are no turns; the list only orders the places on " +
1595
+ "the signature page). byFileId maps a file id to { file_id, can_manage, " +
1596
+ "can_prepare, round, locked }; a file the user cannot see is absent. " +
1597
+ "round is null when there is none, and for a user who is neither a " +
1598
+ "manager nor one of its signers; a signer sees it once it is confirmed, " +
1599
+ "with their own place (my_place) and can_sign (the round is open and " +
1600
+ "their place is unsigned), and signed_count is null " +
1601
+ "for them. locked is 'signing_round' while the file cannot be changed, " +
1602
+ "'signed' once its content cannot be replaced. roundsEnabled is false " +
1603
+ "when the workspace cannot start signing rounds (offer no way to start " +
1604
+ "one), and null until the first answer. can_prepare is about the file " +
1605
+ "and the user, roundsEnabled is about the workspace: offer 'prepare' " +
1606
+ "only when both are true. Ids are matched without regard to case. " +
1607
+ "Refetches only when the set of ids or the signed-in user changes; " +
1608
+ "until the new answer arrives loading is true and byFileId is empty. " +
1609
+ "Reads ctx.filestore.signingRounds.overview.",
1610
+ returnShape: {
1611
+ byFileId:
1612
+ "{ [fileId]: { file_id, can_manage: boolean, can_prepare: boolean, " +
1613
+ "round: { id, status: 'DRAFT' | 'OPEN' | 'COMPLETED', signer_count, signed_count: number | null, " +
1614
+ "my_place: { position, signed, signed_at } | null, can_sign: boolean } | null, " +
1615
+ "locked: 'signing_round' | 'signed' | null } }",
1616
+ roundsEnabled: "boolean | null",
1617
+ loading: "boolean",
1618
+ error: "PermissionError | null // { code, message, retryable, errors, retryAfter }",
1619
+ refresh: "() => Promise<void>",
1620
+ },
1621
+ requiredContextSlice: ["filestore.signingRounds"],
1622
+ scopes: ["files.read:*"],
1623
+ },
1624
+ {
1625
+ name: "useSigningRound",
1626
+ signature: "useSigningRound(fileId)",
1627
+ description:
1628
+ "One file's current signing round, and the actions of someone who " +
1629
+ "manages the file's folder. prepare(signerIds) drafts a round for those " +
1630
+ "users, their places on the signature page in the order given (take the " +
1631
+ "ids from useFileRoster()); it can take " +
1632
+ "up to a minute. preparedFile() returns a five-minute link to the " +
1633
+ "prepared document; show it to the user before confirming. confirm() " +
1634
+ "opens the round for exactly the document preparedFile() last returned, " +
1635
+ "and rejects with code PREVIEW_REQUIRED when it was not called first. " +
1636
+ "cancel() ends a draft or an open round and resolves to the cancelled " +
1637
+ "round (status 'CANCELLED'); round is then null. ONE ACTION RUNS AT A " +
1638
+ "TIME: while busy is true any other action is refused at once with code " +
1639
+ "ACTION_IN_PROGRESS (it is not queued), so disable your buttons on busy. " +
1640
+ "round is the full round as the " +
1641
+ "user may see it: a manager sees every signer's progress, a signer sees " +
1642
+ "every signer's name and position and their own progress only, anyone " +
1643
+ "else sees null. A refused action sets error and rejects with it: code " +
1644
+ "and message come from the server (show message as it is), errors has " +
1645
+ "one entry per refused signer ({ app_user_id, message, reason, " +
1646
+ "characters }), retryAfter is the seconds to wait. The hook's own " +
1647
+ "refusals (PREVIEW_REQUIRED, ACTION_IN_PROGRESS, NO_SIGNING_ROUND) carry " +
1648
+ "a message that can be shown the same way. When a refusal means the " +
1649
+ "round is no longer what was shown (it expired, was closed, or the " +
1650
+ "document changed) the hook reads the round again by itself. Code " +
1651
+ "SIGNING_ROUNDS_NOT_ENABLED means the workspace cannot start signing " +
1652
+ "rounds; check roundsEnabled from useSigningRounds() before offering to " +
1653
+ "start one. When fileId or the signed-in user changes, round is null " +
1654
+ "and loading true until the new answer. Reads ctx.filestore.signingRounds.",
1655
+ returnShape: {
1656
+ round:
1657
+ "{ id, file_id, status: 'DRAFT' | 'OPEN' | 'COMPLETED', created_at, confirmed_at, completed_at, expires_at, " +
1658
+ "signers: { name, position, app_user_id?, signed?, signed_at?, signed_as? }[] } | null",
1659
+ loading: "boolean",
1660
+ error: "PermissionError | null // { code, message, retryable, errors, retryAfter }",
1661
+ refresh: "() => Promise<void>",
1662
+ prepare: "(signerIds: string[]) => Promise<round> // status 'DRAFT'",
1663
+ preparedFile: "() => Promise<{ url, sha256, expires_at, size_bytes }>",
1664
+ confirm: "() => Promise<round> // status 'OPEN'",
1665
+ cancel: "() => Promise<round> // the round it ended, status 'CANCELLED'",
1666
+ busy: "boolean // an action is running; another one is refused with ACTION_IN_PROGRESS",
1667
+ },
1668
+ requiredContextSlice: ["filestore.signingRounds"],
1669
+ scopes: ["files.write:*"],
1670
+ },
1575
1671
  {
1576
1672
  name: "useFolderPermissions",
1577
1673
  signature: "useFolderPermissions(folderId, { enabled })",
@@ -3592,10 +3688,12 @@ const WIDGET_CONTEXT_SHAPE = {
3592
3688
  "Injected @colixsystems/filestore-client instance — the end-user file archive (project / personal / public spaces) + BankID file signing. " +
3593
3689
  "{ files: { list(query) -> Promise<{ data, meta }>, get(id), upload(formData), update(id, body), copy(id, body), remove(id), preview(id) }, " +
3594
3690
  "folders: { list, create, update, remove }, shares: { ... }, trash: { ... }, " +
3595
- "signatures: { initiate(fileId), status(id), cancel(id), verify(id) }, objectUrl(token), fetchObject(token) }. " +
3596
- "Backs useFilestoreFiles() + useFilestoreFolders() + useFilestoreCreateFolder() + useFilestoreCopy() + useFileSignature(). Optional — a host without an end-user filestore omits it and those hooks throw a clear error.",
3691
+ "signatures: { list(fileIds), roster(fileId, query), initiate(fileId), status(id), cancel(id), verify(id) }, " +
3692
+ "signingRounds: { overview(fileIds), list(fileId, query), get(roundId), prepare(fileId, signerIds), preparedFile(roundId), confirm(roundId, preparedSha256), cancel(roundId) }, " +
3693
+ "objectUrl(token), fetchObject(token) }. " +
3694
+ "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
3695
  required: false,
3598
- fields: { files: "object", folders: "object", shares: "object", signatures: "object" },
3696
+ fields: { files: "object", folders: "object", shares: "object", signatures: "object", signingRounds: "object" },
3599
3697
  },
3600
3698
  renderer: {
3601
3699
  description:
@@ -5707,7 +5805,12 @@ const CONTRACT = deepFreeze({
5707
5805
  // binds no widget style fields: the component reads it on both hosts,
5708
5806
  // which a scope now states as `sdkComponents`.
5709
5807
  // Additive: an unthemed chip is the muted pill widgets drew by hand.
5710
- version: "1.174.0",
5808
+ // 1.175.0: additive (sc-10410) — `useSigningRounds(fileIds)` and
5809
+ // `useSigningRound(fileId)` read and run PDF signing rounds through the
5810
+ // new `ctx.filestore.signingRounds`. `useFileSignature` also returns
5811
+ // `hintCode` and `nameRefusal`, and its `cancel()` resolves to what
5812
+ // became of the order instead of always reporting "cancelled".
5813
+ version: "1.175.0",
5711
5814
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
5712
5815
  hooks: HOOKS,
5713
5816
  primitives: PRIMITIVES,
package/dist/contract.js CHANGED
@@ -1511,7 +1511,15 @@ const HOOKS = [
1511
1511
  "polls (fresh QR while pending — render with the Image primitive), " +
1512
1512
  "cancel() aborts, verify() returns the offline verdict. Pass " +
1513
1513
  "existingSignatureId for an already-signed file to seed the complete " +
1514
- "state + verify() without re-signing. Reads " +
1514
+ "state + verify() without re-signing. autoStartToken is set only while " +
1515
+ "opening BankID can still help (null once the user has signed or the " +
1516
+ "order has ended). hintCode says where the order " +
1517
+ "stands; 'sealing' means the user has signed and the signature is being " +
1518
+ "added to the document, so there is nothing to scan and it can no longer " +
1519
+ "be cancelled. cancel() resolves to what became of the order, which is " +
1520
+ "then still pending or already complete: read status after it, never " +
1521
+ "assume 'cancelled'. nameRefusal is set when the order failed because " +
1522
+ "the signer's name cannot be shown on the document. Reads " +
1515
1523
  "ctx.filestore.signatures.{initiate,status,cancel,verify}.",
1516
1524
  returnShape: {
1517
1525
  status: "'pending' | 'complete' | 'failed' | 'cancelled' | null",
@@ -1520,12 +1528,15 @@ const HOOKS = [
1520
1528
  message: "string | null",
1521
1529
  signerName: "string | null",
1522
1530
  signedAt: "string | null",
1531
+ hintCode: "string | null // where the order stands, e.g. 'sealing'",
1532
+ nameRefusal:
1533
+ "{ reason: 'empty' | 'too_long' | 'unsupported_characters' | 'mixed_scripts', characters: string[] } | null",
1523
1534
  verdict: "{ valid, checks, content_status, signed_by_others_since, seal, ... } | null",
1524
1535
  loading: "boolean",
1525
1536
  error: "PermissionError | null // { code, message, retryable }",
1526
- initiate: "() => Promise<{ signature_id, qr, auto_start_token, status }>",
1537
+ initiate: "() => Promise<{ signature_id, qr, auto_start_token, status, hint_code?, message? }>",
1527
1538
  refresh: "() => Promise<void>",
1528
- cancel: "() => Promise<void>",
1539
+ cancel: "() => Promise<{ status, hint_code?, message?, signer_name?, signed_at? } | undefined>",
1529
1540
  verify: "() => Promise<verdict>",
1530
1541
  },
1531
1542
  requiredContextSlice: ["filestore.signatures"],
@@ -1572,6 +1583,91 @@ const HOOKS = [
1572
1583
  requiredContextSlice: ["filestore.signatures"],
1573
1584
  scopes: ["files.read:*"],
1574
1585
  },
1586
+ {
1587
+ name: "useSigningRounds",
1588
+ signature: "useSigningRounds(fileIds)",
1589
+ description:
1590
+ "What the signed-in user may know about the signing round of each file " +
1591
+ "in a list, in one batched request. A signing round fixes a PDF and a " +
1592
+ "list of signers: it is prepared and confirmed by someone who manages " +
1593
+ "the file's folder, then each signer signs once with useFileSignature(), " +
1594
+ "in any order (there are no turns; the list only orders the places on " +
1595
+ "the signature page). byFileId maps a file id to { file_id, can_manage, " +
1596
+ "can_prepare, round, locked }; a file the user cannot see is absent. " +
1597
+ "round is null when there is none, and for a user who is neither a " +
1598
+ "manager nor one of its signers; a signer sees it once it is confirmed, " +
1599
+ "with their own place (my_place) and can_sign (the round is open and " +
1600
+ "their place is unsigned), and signed_count is null " +
1601
+ "for them. locked is 'signing_round' while the file cannot be changed, " +
1602
+ "'signed' once its content cannot be replaced. roundsEnabled is false " +
1603
+ "when the workspace cannot start signing rounds (offer no way to start " +
1604
+ "one), and null until the first answer. can_prepare is about the file " +
1605
+ "and the user, roundsEnabled is about the workspace: offer 'prepare' " +
1606
+ "only when both are true. Ids are matched without regard to case. " +
1607
+ "Refetches only when the set of ids or the signed-in user changes; " +
1608
+ "until the new answer arrives loading is true and byFileId is empty. " +
1609
+ "Reads ctx.filestore.signingRounds.overview.",
1610
+ returnShape: {
1611
+ byFileId:
1612
+ "{ [fileId]: { file_id, can_manage: boolean, can_prepare: boolean, " +
1613
+ "round: { id, status: 'DRAFT' | 'OPEN' | 'COMPLETED', signer_count, signed_count: number | null, " +
1614
+ "my_place: { position, signed, signed_at } | null, can_sign: boolean } | null, " +
1615
+ "locked: 'signing_round' | 'signed' | null } }",
1616
+ roundsEnabled: "boolean | null",
1617
+ loading: "boolean",
1618
+ error: "PermissionError | null // { code, message, retryable, errors, retryAfter }",
1619
+ refresh: "() => Promise<void>",
1620
+ },
1621
+ requiredContextSlice: ["filestore.signingRounds"],
1622
+ scopes: ["files.read:*"],
1623
+ },
1624
+ {
1625
+ name: "useSigningRound",
1626
+ signature: "useSigningRound(fileId)",
1627
+ description:
1628
+ "One file's current signing round, and the actions of someone who " +
1629
+ "manages the file's folder. prepare(signerIds) drafts a round for those " +
1630
+ "users, their places on the signature page in the order given (take the " +
1631
+ "ids from useFileRoster()); it can take " +
1632
+ "up to a minute. preparedFile() returns a five-minute link to the " +
1633
+ "prepared document; show it to the user before confirming. confirm() " +
1634
+ "opens the round for exactly the document preparedFile() last returned, " +
1635
+ "and rejects with code PREVIEW_REQUIRED when it was not called first. " +
1636
+ "cancel() ends a draft or an open round and resolves to the cancelled " +
1637
+ "round (status 'CANCELLED'); round is then null. ONE ACTION RUNS AT A " +
1638
+ "TIME: while busy is true any other action is refused at once with code " +
1639
+ "ACTION_IN_PROGRESS (it is not queued), so disable your buttons on busy. " +
1640
+ "round is the full round as the " +
1641
+ "user may see it: a manager sees every signer's progress, a signer sees " +
1642
+ "every signer's name and position and their own progress only, anyone " +
1643
+ "else sees null. A refused action sets error and rejects with it: code " +
1644
+ "and message come from the server (show message as it is), errors has " +
1645
+ "one entry per refused signer ({ app_user_id, message, reason, " +
1646
+ "characters }), retryAfter is the seconds to wait. The hook's own " +
1647
+ "refusals (PREVIEW_REQUIRED, ACTION_IN_PROGRESS, NO_SIGNING_ROUND) carry " +
1648
+ "a message that can be shown the same way. When a refusal means the " +
1649
+ "round is no longer what was shown (it expired, was closed, or the " +
1650
+ "document changed) the hook reads the round again by itself. Code " +
1651
+ "SIGNING_ROUNDS_NOT_ENABLED means the workspace cannot start signing " +
1652
+ "rounds; check roundsEnabled from useSigningRounds() before offering to " +
1653
+ "start one. When fileId or the signed-in user changes, round is null " +
1654
+ "and loading true until the new answer. Reads ctx.filestore.signingRounds.",
1655
+ returnShape: {
1656
+ round:
1657
+ "{ id, file_id, status: 'DRAFT' | 'OPEN' | 'COMPLETED', created_at, confirmed_at, completed_at, expires_at, " +
1658
+ "signers: { name, position, app_user_id?, signed?, signed_at?, signed_as? }[] } | null",
1659
+ loading: "boolean",
1660
+ error: "PermissionError | null // { code, message, retryable, errors, retryAfter }",
1661
+ refresh: "() => Promise<void>",
1662
+ prepare: "(signerIds: string[]) => Promise<round> // status 'DRAFT'",
1663
+ preparedFile: "() => Promise<{ url, sha256, expires_at, size_bytes }>",
1664
+ confirm: "() => Promise<round> // status 'OPEN'",
1665
+ cancel: "() => Promise<round> // the round it ended, status 'CANCELLED'",
1666
+ busy: "boolean // an action is running; another one is refused with ACTION_IN_PROGRESS",
1667
+ },
1668
+ requiredContextSlice: ["filestore.signingRounds"],
1669
+ scopes: ["files.write:*"],
1670
+ },
1575
1671
  {
1576
1672
  name: "useFolderPermissions",
1577
1673
  signature: "useFolderPermissions(folderId, { enabled })",
@@ -3588,10 +3684,12 @@ const WIDGET_CONTEXT_SHAPE = {
3588
3684
  "Injected @colixsystems/filestore-client instance — the end-user file archive (project / personal / public spaces) + BankID file signing. " +
3589
3685
  "{ files: { list(query) -> Promise<{ data, meta }>, get(id), upload(formData), update(id, body), copy(id, body), remove(id), preview(id) }, " +
3590
3686
  "folders: { list, create, update, remove }, shares: { ... }, trash: { ... }, " +
3591
- "signatures: { initiate(fileId), status(id), cancel(id), verify(id) }, objectUrl(token), fetchObject(token) }. " +
3592
- "Backs useFilestoreFiles() + useFilestoreFolders() + useFilestoreCreateFolder() + useFilestoreCopy() + useFileSignature(). Optional — a host without an end-user filestore omits it and those hooks throw a clear error.",
3687
+ "signatures: { list(fileIds), roster(fileId, query), initiate(fileId), status(id), cancel(id), verify(id) }, " +
3688
+ "signingRounds: { overview(fileIds), list(fileId, query), get(roundId), prepare(fileId, signerIds), preparedFile(roundId), confirm(roundId, preparedSha256), cancel(roundId) }, " +
3689
+ "objectUrl(token), fetchObject(token) }. " +
3690
+ "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
3691
  required: false,
3594
- fields: { files: "object", folders: "object", shares: "object", signatures: "object" },
3692
+ fields: { files: "object", folders: "object", shares: "object", signatures: "object", signingRounds: "object" },
3595
3693
  },
3596
3694
  renderer: {
3597
3695
  description:
@@ -5703,7 +5801,12 @@ const CONTRACT = deepFreeze({
5703
5801
  // binds no widget style fields: the component reads it on both hosts,
5704
5802
  // which a scope now states as `sdkComponents`.
5705
5803
  // Additive: an unthemed chip is the muted pill widgets drew by hand.
5706
- version: "1.174.0",
5804
+ // 1.175.0: additive (sc-10410) — `useSigningRounds(fileIds)` and
5805
+ // `useSigningRound(fileId)` read and run PDF signing rounds through the
5806
+ // new `ctx.filestore.signingRounds`. `useFileSignature` also returns
5807
+ // `hintCode` and `nameRefusal`, and its `cancel()` resolves to what
5808
+ // became of the order instead of always reporting "cancelled".
5809
+ version: "1.175.0",
5707
5810
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
5708
5811
  hooks: HOOKS,
5709
5812
  primitives: PRIMITIVES,
package/dist/hooks.js CHANGED
@@ -3775,7 +3775,7 @@ export class PermissionError extends Error {
3775
3775
  }
3776
3776
  }
3777
3777
 
3778
- function toPermissionError(err) {
3778
+ export function toPermissionError(err) {
3779
3779
  // See the note in toDatastoreError — a client rejection is read from the
3780
3780
  // envelope below rather than matching this instanceof.
3781
3781
  if (err instanceof PermissionError) return err;
@@ -5172,149 +5172,6 @@ export function useFilestoreDelete() {
5172
5172
  return { remove, removing, error };
5173
5173
  }
5174
5174
 
5175
- /**
5176
- * Drive a BankID signing flow for one Filestore file. Returns
5177
- * `{ status, qr, autoStartToken, message, signerName, signedAt, verdict,
5178
- * loading, error, initiate, refresh, cancel, verify }`.
5179
- *
5180
- * `initiate()` opens a sign order against the file (the backend hashes its
5181
- * bytes SERVER-SIDE and binds the digest into the BankID signature); `refresh()`
5182
- * polls the order (surfacing a fresh animated-QR frame while pending — render
5183
- * it with the `Image` primitive); `cancel()` aborts it; `verify()` re-runs the
5184
- * offline verification and returns the verdict (`{ valid, content_status, … }`).
5185
- * Reads `ctx.filestore.signatures.{initiate,status,cancel,verify}`. When
5186
- * `fileId` is null/empty the hook collapses to a no-op.
5187
- *
5188
- * Pass `existingSignatureId` for a file that is ALREADY signed: the hook seeds
5189
- * that order id (status `complete`) so the widget can show the signed state and
5190
- * `verify()` it WITHOUT opening a new order — re-signing then requires an
5191
- * explicit `initiate()`.
5192
- */
5193
- export function useFileSignature(fileId, existingSignatureId = null) {
5194
- const ctx = useWidgetContextOrThrow("useFileSignature");
5195
- if (!ctx.filestore || !ctx.filestore.signatures) {
5196
- throw new Error(
5197
- "useFileSignature: host did not inject a filestore client (ctx.filestore.signatures)",
5198
- );
5199
- }
5200
- const ready = Boolean(fileId);
5201
- const sigRef = useRef(ctx.filestore.signatures);
5202
- sigRef.current = ctx.filestore.signatures;
5203
- const fileIdRef = useRef(fileId);
5204
- fileIdRef.current = fileId;
5205
- // Seed the order from an already-completed signature so verify() works
5206
- // without initiating; initiate() (an explicit re-sign) overwrites it.
5207
- const orderRef = useRef(existingSignatureId || null);
5208
-
5209
- const [status, setStatus] = useState(existingSignatureId ? "complete" : null);
5210
- const [qr, setQr] = useState(null);
5211
- const [autoStartToken, setAutoStartToken] = useState(null);
5212
- const [message, setMessage] = useState(null);
5213
- const [signerName, setSignerName] = useState(null);
5214
- const [signedAt, setSignedAt] = useState(null);
5215
- const [verdict, setVerdict] = useState(null);
5216
- const [loading, setLoading] = useState(false);
5217
- const [error, setError] = useState(null);
5218
-
5219
- const initiate = useCallback(async () => {
5220
- if (!fileIdRef.current) return null;
5221
- setLoading(true);
5222
- setError(null);
5223
- setVerdict(null);
5224
- try {
5225
- const res = await sigRef.current.initiate(fileIdRef.current);
5226
- orderRef.current = res && res.signature_id ? res.signature_id : null;
5227
- setStatus(res && res.status ? res.status : "pending");
5228
- setQr(res && res.qr ? res.qr : null);
5229
- setAutoStartToken(res && res.auto_start_token ? res.auto_start_token : null);
5230
- setSignerName(null);
5231
- setSignedAt(null);
5232
- setMessage(null);
5233
- setLoading(false);
5234
- return res;
5235
- } catch (err) {
5236
- setError(toPermissionError(err));
5237
- setLoading(false);
5238
- throw toPermissionError(err);
5239
- }
5240
- }, []);
5241
-
5242
- const refresh = useCallback(async () => {
5243
- const id = orderRef.current;
5244
- if (!id) return;
5245
- try {
5246
- const res = await sigRef.current.status(id);
5247
- if (res) {
5248
- if (res.status != null) setStatus(res.status);
5249
- if (res.qr !== undefined) setQr(res.qr || null);
5250
- if (res.message !== undefined) setMessage(res.message || null);
5251
- if (res.signer_name !== undefined) setSignerName(res.signer_name || null);
5252
- if (res.signed_at !== undefined) setSignedAt(res.signed_at || null);
5253
- }
5254
- } catch (err) {
5255
- setError(toPermissionError(err));
5256
- }
5257
- }, []);
5258
-
5259
- const cancel = useCallback(async () => {
5260
- const id = orderRef.current;
5261
- if (!id) return;
5262
- try {
5263
- await sigRef.current.cancel(id);
5264
- setStatus("cancelled");
5265
- setQr(null);
5266
- } catch (err) {
5267
- throw toPermissionError(err);
5268
- }
5269
- }, []);
5270
-
5271
- const verify = useCallback(async () => {
5272
- const id = orderRef.current;
5273
- if (!id) return null;
5274
- try {
5275
- const v = await sigRef.current.verify(id);
5276
- setVerdict(v);
5277
- return v;
5278
- } catch (err) {
5279
- setError(toPermissionError(err));
5280
- throw toPermissionError(err);
5281
- }
5282
- }, []);
5283
-
5284
- if (!ready) {
5285
- return {
5286
- status: null,
5287
- qr: null,
5288
- autoStartToken: null,
5289
- message: null,
5290
- signerName: null,
5291
- signedAt: null,
5292
- verdict: null,
5293
- loading: false,
5294
- error: null,
5295
- initiate: async () => null,
5296
- refresh: async () => undefined,
5297
- cancel: async () => undefined,
5298
- verify: async () => null,
5299
- };
5300
- }
5301
- return {
5302
- status,
5303
- qr,
5304
- autoStartToken,
5305
- message,
5306
- signerName,
5307
- signedAt,
5308
- verdict,
5309
- loading,
5310
- error,
5311
- initiate,
5312
- refresh,
5313
- cancel,
5314
- verify,
5315
- };
5316
- }
5317
-
5318
5175
  /**
5319
5176
  * Batch "is this file signed?" lookup for a set of file ids. Returns
5320
5177
  * `{ signaturesByFileId, loading, error, refetch }` where `signaturesByFileId`
package/dist/host.d.ts CHANGED
@@ -397,6 +397,25 @@ export interface TopBarTokens {
397
397
  */
398
398
  export function resolveTopBarTokens(theme: unknown): TopBarTokens;
399
399
 
400
+ /**
401
+ * Whether a chrome surface reads as dark. Pass the colour the bar paints
402
+ * (`#RRGGBBAA` is composited over `backdrop`, else over the page in `theme`),
403
+ * or, for a bar that paints nothing, the colour of the text authored to sit on it.
404
+ */
405
+ export function isDarkSurface(input?: {
406
+ surface?: string | null;
407
+ backdrop?: string | null;
408
+ textColor?: string | null;
409
+ theme?: unknown;
410
+ } | null): boolean;
411
+
412
+ /** The logo a surface draws: the dark-surface variant only beside an app logo. */
413
+ export function resolveAppLogo<T>(
414
+ logo: T | null | undefined,
415
+ darkSurfaceLogo: T | null | undefined,
416
+ onDark: boolean,
417
+ ): T | null;
418
+
400
419
  /** sc-8023 — the arrangeable contents of the sidebar layout's TOP STRIPE: the
401
420
  * row across the top of the content, outside the panel and above it. */
402
421
  export type TopStripeSlot = "back" | "menu" | "brand" | "notifications";
package/dist/host.js CHANGED
@@ -110,6 +110,8 @@ export {
110
110
  resolveSidebarUnfold,
111
111
  resolveLocalNavModel,
112
112
  drawsLocalNavAnywhere,
113
+ isDarkSurface,
114
+ resolveAppLogo,
113
115
  } from "./navigation.js";
114
116
 
115
117
  // sc-5717: the codec behind `useWidgetRoute()` — how a widget's own internal