@colixsystems/widget-sdk 0.220.0 → 0.221.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 CHANGED
@@ -84,7 +84,26 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
84
84
 
85
85
  ## Status
86
86
 
87
- `v0.220.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
+ `v0.221.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**.
88
+
89
+ ### What's new in 0.221.1 (contract 1.177.1)
90
+
91
+ **`useFileSignature`: one failed poll no longer outlives the next answer.** `error` is the last call that failed, and a `refresh()` or `verify()` that succeeds now clears it. While `status` is `'pending'` an `error` means a poll failed: keep polling and keep showing the order. Fix only; nothing was added or removed.
92
+
93
+ ### What's new in 0.221.0 (contract 1.177.0)
94
+
95
+ **A row's file goes in the row's record space, and `upload()` names the record per call.** A file a widget keeps in a `FILE` column belongs in that row's `record` space: exactly the people who may read the row may read the file, so a private table's attachments stay private, and a table that lets everyone read still serves them to logged-out visitors. A `public` file, by contrast, is readable by anyone holding its link whatever the table's permissions say. A row must exist before a file can be attached to it, and a create form only learns the new row's id inside its submit handler, after the hook rendered. So `upload(file, { recordId })` now takes the record per call, the way it already took `folderId`:
96
+
97
+ ```js
98
+ const { create, update } = useDatastoreMutation(tableId);
99
+ const { upload } = useFilestoreUpload({ spaceType: "record" });
100
+
101
+ const row = await create({ Title: title }); // without the file
102
+ const stored = await upload(file, { recordId: row.id }); // into that row's space
103
+ await update(row.id, { Receipt: stored.id });
104
+ ```
105
+
106
+ A per-call `recordId` wins over the hook's; outside a `record` space it is ignored. A record upload that names no record now rejects with `useFilestoreUpload: a record space needs a recordId` instead of the misleading `Sign in to upload`. A logged-out visitor cannot upload into a record space, so ask them to sign in to attach a file. `useCamera()` and `useImageEditor()` now point at `useFilestoreUpload` for the upload: their descriptions named `ctx.assets.upload`, which widget code cannot reach. Additive.
88
107
 
89
108
  ### What's new in 0.220.0 (contract 1.176.0)
90
109
 
@@ -1496,9 +1515,7 @@ const { edit } = useImageEditor();
1496
1515
  const shot = await capture();
1497
1516
  const small = await edit(shot.uri, [{ resize: { width: 800 } }], { format: "jpeg", compress: 0.8 });
1498
1517
 
1499
- const fd = new FormData();
1500
- fd.append("file", small.file);
1501
- await ctx.assets.upload(fd);
1518
+ await upload(small.file); // upload from useFilestoreUpload()
1502
1519
  ```
1503
1520
 
1504
1521
  `edit(uri, actions, options?)` applies `actions` in order and resolves the SAME normalised asset shape `useCamera()` yields, so capture → edit → upload is one code path on both hosts. Actions are `{ resize: { width?, height? } }`, `{ crop: { originX, originY, width, height } }`, `{ rotate: degrees }` and `{ flip: "horizontal" | "vertical" }`; output is `{ format: "jpeg" | "png" | "webp", compress, base64? }`.
@@ -1712,12 +1729,10 @@ When you take the `useCanWrite`-only route, omit the USER column for a guest (`i
1712
1729
 
1713
1730
  **Dismissing the picker resolves `null`, not an error.** Backing out is the most common outcome, so it is deliberately not a rejection — your happy path needs no `try/catch`. A genuine failure (permission refused, no host broker) rejects with a structured `CameraError` (new named export) carrying a stable `.code` (`PERMISSION_DENIED` / `UNSUPPORTED` / `INTERNAL`).
1714
1731
 
1715
- **One upload path on both hosts.** The resolved asset is normalised to `{ uri, name, mimeType, width, height, size, file }`, where `uri` is directly displayable (`<Image source={{ uri }} />`) and `file` is already the right upload part for the platform — a browser `File` on web, expo-file-system's `File` on native. (Never React Native's legacy `{ uri, name, type }` part: the native host streams the file off disk, and Expo's fetch rejects that part outright anyway. Do not branch on the part's type — an expo-file-system `File` is NOT `instanceof Blob` at runtime.) So the same three lines work everywhere:
1732
+ **One upload path on both hosts.** The resolved asset is normalised to `{ uri, name, mimeType, width, height, size, file }`, where `uri` is directly displayable (`<Image source={{ uri }} />`) and `file` is already the right upload part for the platform — a browser `File` on web, expo-file-system's `File` on native. (Never React Native's legacy `{ uri, name, type }` part: the native host streams the file off disk, and Expo's fetch rejects that part outright anyway. Do not branch on the part's type — an expo-file-system `File` is NOT `instanceof Blob` at runtime.) So the same line works everywhere:
1716
1733
 
1717
1734
  ```js
1718
- const fd = new FormData();
1719
- fd.append("file", asset.file);
1720
- await ctx.assets.upload(fd);
1735
+ await upload(asset.file); // upload from useFilestoreUpload()
1721
1736
  ```
1722
1737
 
1723
1738
  `options` (`{ allowsEditing, quality }`) are **hints**: the Expo export applies both, the web camera applies `quality` only — so never depend on a cropped result. `reset()` clears the asset and releases it (on web that revokes the blob URL, which otherwise leaks for the life of the document). **Gate your camera button on `supported`** — a host that brokers no camera reports `false` rather than throwing. The web Player brokers it via `getUserMedia` — a live preview with a shutter — on every web host: it is the only camera a DESKTOP browser will open, and a phone reaches the same preview rather than its own camera app. It falls back to a file input (`capture="environment"`) only where getUserMedia is absent up front: an insecure origin or an older browser. Once the permission prompt has been shown the user gesture is spent, so a camera that then fails to open rejects (`PERMISSION_DENIED` if refused, `UNSUPPORTED` if there is no usable device) rather than falling back to a file dialog the browser would refuse to open. The Expo export uses `expo-image-picker`, whose config plugin declares the camera and photo-library permissions the runtime needs.
package/dist/contract.cjs CHANGED
@@ -1369,7 +1369,7 @@ const HOOKS = [
1369
1369
  "from the host context, builds a multipart FormData with the " +
1370
1370
  "snake_case fields the backend reads verbatim (`space_type`, " +
1371
1371
  "`owner_id`, `folder_id`, plus the binary `file`), and POSTs through " +
1372
- "ctx.filestore.files.upload. `upload(file, { folderId?, compress?, fileName? })` " +
1372
+ "ctx.filestore.files.upload. `upload(file, { folderId?, recordId?, compress?, fileName? })` " +
1373
1373
  "resolves to the created file row or throws the wire error. `file` must be a BLOB — a browser File on web, " +
1374
1374
  "expo-file-system's File on native (what FilePicker / useCamera / useImageEditor hand back); Expo's fetch rejects " +
1375
1375
  "React Native's legacy { uri, name, type } part with \"Unsupported FormDataPart implementation\". The stored name " +
@@ -1387,13 +1387,20 @@ const HOOKS = [
1387
1387
  "ORIGINAL matters (a document archive, a photo the user re-downloads, " +
1388
1388
  "anything with an exact-bytes requirement). " +
1389
1389
  "ALWAYS pass spaceType explicitly — omitting it falls back to " +
1390
- "'project', which is unreadable by a logged-out visitor. Choose it by " +
1391
- "who must SEE the file: 'public' for content the app displays to " +
1392
- "anyone browsing (product, catalogue, gallery, cover images), " +
1393
- "'project' for content restricted to signed-in workspace users, " +
1394
- "'personal' for a file private to the uploading app user.",
1390
+ "'project', which is unreadable by a logged-out visitor. A file that " +
1391
+ "belongs to a datastore row — anything stored in a FILE column — goes " +
1392
+ "in that row's 'record' space, so exactly the people who may read the " +
1393
+ "row may read the file (logged-out visitors too, when the table lets " +
1394
+ "everyone read). A file can only be attached to a row that exists: " +
1395
+ "create the row, call upload(file, { recordId: row.id }) — the per-call " +
1396
+ "recordId names the row the hook could not know at render — then write " +
1397
+ "the returned id into the FILE column with update(). A logged-out " +
1398
+ "visitor cannot upload into a record space. For a file that belongs to " +
1399
+ "no row: 'public' for content the app shows to anyone browsing, " +
1400
+ "'project' for signed-in workspace users only, 'personal' for a file " +
1401
+ "private to the uploading app user.",
1395
1402
  returnShape: {
1396
- upload: "(file, { folderId?, compress?, fileName? }) => Promise<FilestoreFile>",
1403
+ upload: "(file, { folderId?, recordId?, compress?, fileName? }) => Promise<FilestoreFile>",
1397
1404
  uploading: "boolean",
1398
1405
  error: "Error | null",
1399
1406
  lastUploaded: "FilestoreFile | null",
@@ -1552,7 +1559,10 @@ const HOOKS = [
1552
1559
  "be cancelled. cancel() resolves to what became of the order, which is " +
1553
1560
  "then still pending or already complete: read status after it, never " +
1554
1561
  "assume 'cancelled'. nameRefusal is set when the order failed because " +
1555
- "the signer's name cannot be shown on the document. Reads " +
1562
+ "the signer's name cannot be shown on the document. error is the last " +
1563
+ "call that failed, and a refresh() or verify() that succeeds clears it: " +
1564
+ "while status is 'pending' an error means one poll failed, so keep " +
1565
+ "polling and keep showing the order. Reads " +
1556
1566
  "ctx.filestore.signatures.{initiate,status,cancel,verify}.",
1557
1567
  returnShape: {
1558
1568
  status: "'pending' | 'complete' | 'failed' | 'cancelled' | null",
@@ -2818,8 +2828,8 @@ const HOOKS = [
2818
2828
  "common case and is deliberately NOT an error, so no try/catch is needed on the happy path. They reject with a " +
2819
2829
  "CameraError whose .code is one of PERMISSION_DENIED | UNSUPPORTED | INTERNAL. `asset.file` is already the right " +
2820
2830
  "upload part for the host — a BLOB on both (a File on web, expo-file-system's File on native), because Expo's fetch " +
2821
- "implements the web FormData spec and rejects React Native's legacy { uri, name, type } part outright: append it to a FormData as `file` and pass " +
2822
- "that to ctx.assets.upload(fd) — one code path on both platforms. reset() clears the asset and releases it. Check " +
2831
+ "implements the web FormData spec and rejects React Native's legacy { uri, name, type } part outright: pass it " +
2832
+ "straight to upload(asset.file) from useFilestoreUpload — one code path on both platforms. reset() clears the asset and releases it. Check " +
2823
2833
  "`supported` before rendering a camera button. options: { allowsEditing, quality } are HINTS the host honours where it " +
2824
2834
  "can — the Expo export applies both, the web camera applies quality only, so never depend on a cropped result. " +
2825
2835
  "Behaviour is otherwise identical on web (a getUserMedia preview on EVERY host, phones included; a file input " +
@@ -2885,7 +2895,7 @@ const HOOKS = [
2885
2895
  "Editing is IMPERATIVE — call edit() from an event handler, never during render. " +
2886
2896
  "edit(uri, actions, options?) applies `actions` IN ORDER and resolves the SAME normalised asset shape useCamera() " +
2887
2897
  "yields — { uri, name, mimeType, width, height, size, base64?, file } — so capture -> edit -> upload is ONE code path: " +
2888
- "append result.file to a FormData as `file` and pass it to ctx.assets.upload(fd). " +
2898
+ "pass result.file to upload() from useFilestoreUpload. " +
2889
2899
  "Each action entry carries exactly one of { resize: { width?, height? } } (aspect preserved when only one is given), " +
2890
2900
  "{ crop: { originX, originY, width, height } }, { rotate: degrees } (positive is clockwise), or " +
2891
2901
  "{ flip: \"horizontal\" | \"vertical\" }. A crop rect must lie INSIDE the surface the earlier actions produced, and " +
@@ -3022,7 +3032,7 @@ const PRIMITIVES = [
3022
3032
  },
3023
3033
  // sc-1378 — `<FilePicker accept onPick>` lets a widget surface a native
3024
3034
  // file-picker on the web Player so end users can upload files to the
3025
- // backend (paired with useFilestoreUpload or useAssets.upload). Web wraps
3035
+ // backend (paired with useFilestoreUpload). Web wraps
3026
3036
  // a hidden DOM `<input type="file">`; native renders a disabled trigger
3027
3037
  // and exposes `FilePicker.isSupported = false` until a vetted Expo
3028
3038
  // picker pin lands.
@@ -5856,7 +5866,15 @@ const CONTRACT = deepFreeze({
5856
5866
  // a widget never receives it. `account`'s photo actions are now
5857
5867
  // optional members, and `user.email` is documented as nullable: an
5858
5868
  // account without an email has none.
5859
- version: "1.176.0",
5869
+ // 1.177.0: additive (sc-10937) — `useFilestoreUpload().upload(file,
5870
+ // { recordId })` names the record per call, so a widget can create a row
5871
+ // and attach to it in one handler. A FILE column's file goes in its
5872
+ // row's record space; useCamera / useImageEditor point at
5873
+ // useFilestoreUpload, since widget code cannot reach ctx.assets.
5874
+ // 1.177.1: fix (sc-10410) — a `refresh()` or `verify()` that succeeds
5875
+ // clears `useFileSignature`'s `error`; it used to keep one failed poll's
5876
+ // error for the rest of the order.
5877
+ version: "1.177.1",
5860
5878
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
5861
5879
  hooks: HOOKS,
5862
5880
  primitives: PRIMITIVES,
package/dist/contract.js CHANGED
@@ -1369,7 +1369,7 @@ const HOOKS = [
1369
1369
  "from the host context, builds a multipart FormData with the " +
1370
1370
  "snake_case fields the backend reads verbatim (`space_type`, " +
1371
1371
  "`owner_id`, `folder_id`, plus the binary `file`), and POSTs through " +
1372
- "ctx.filestore.files.upload. `upload(file, { folderId?, compress?, fileName? })` " +
1372
+ "ctx.filestore.files.upload. `upload(file, { folderId?, recordId?, compress?, fileName? })` " +
1373
1373
  "resolves to the created file row or throws the wire error. `file` must be a BLOB — a browser File on web, " +
1374
1374
  "expo-file-system's File on native (what FilePicker / useCamera / useImageEditor hand back); Expo's fetch rejects " +
1375
1375
  "React Native's legacy { uri, name, type } part with \"Unsupported FormDataPart implementation\". The stored name " +
@@ -1387,13 +1387,20 @@ const HOOKS = [
1387
1387
  "ORIGINAL matters (a document archive, a photo the user re-downloads, " +
1388
1388
  "anything with an exact-bytes requirement). " +
1389
1389
  "ALWAYS pass spaceType explicitly — omitting it falls back to " +
1390
- "'project', which is unreadable by a logged-out visitor. Choose it by " +
1391
- "who must SEE the file: 'public' for content the app displays to " +
1392
- "anyone browsing (product, catalogue, gallery, cover images), " +
1393
- "'project' for content restricted to signed-in workspace users, " +
1394
- "'personal' for a file private to the uploading app user.",
1390
+ "'project', which is unreadable by a logged-out visitor. A file that " +
1391
+ "belongs to a datastore row — anything stored in a FILE column — goes " +
1392
+ "in that row's 'record' space, so exactly the people who may read the " +
1393
+ "row may read the file (logged-out visitors too, when the table lets " +
1394
+ "everyone read). A file can only be attached to a row that exists: " +
1395
+ "create the row, call upload(file, { recordId: row.id }) — the per-call " +
1396
+ "recordId names the row the hook could not know at render — then write " +
1397
+ "the returned id into the FILE column with update(). A logged-out " +
1398
+ "visitor cannot upload into a record space. For a file that belongs to " +
1399
+ "no row: 'public' for content the app shows to anyone browsing, " +
1400
+ "'project' for signed-in workspace users only, 'personal' for a file " +
1401
+ "private to the uploading app user.",
1395
1402
  returnShape: {
1396
- upload: "(file, { folderId?, compress?, fileName? }) => Promise<FilestoreFile>",
1403
+ upload: "(file, { folderId?, recordId?, compress?, fileName? }) => Promise<FilestoreFile>",
1397
1404
  uploading: "boolean",
1398
1405
  error: "Error | null",
1399
1406
  lastUploaded: "FilestoreFile | null",
@@ -1552,7 +1559,10 @@ const HOOKS = [
1552
1559
  "be cancelled. cancel() resolves to what became of the order, which is " +
1553
1560
  "then still pending or already complete: read status after it, never " +
1554
1561
  "assume 'cancelled'. nameRefusal is set when the order failed because " +
1555
- "the signer's name cannot be shown on the document. Reads " +
1562
+ "the signer's name cannot be shown on the document. error is the last " +
1563
+ "call that failed, and a refresh() or verify() that succeeds clears it: " +
1564
+ "while status is 'pending' an error means one poll failed, so keep " +
1565
+ "polling and keep showing the order. Reads " +
1556
1566
  "ctx.filestore.signatures.{initiate,status,cancel,verify}.",
1557
1567
  returnShape: {
1558
1568
  status: "'pending' | 'complete' | 'failed' | 'cancelled' | null",
@@ -2818,8 +2828,8 @@ const HOOKS = [
2818
2828
  "common case and is deliberately NOT an error, so no try/catch is needed on the happy path. They reject with a " +
2819
2829
  "CameraError whose .code is one of PERMISSION_DENIED | UNSUPPORTED | INTERNAL. `asset.file` is already the right " +
2820
2830
  "upload part for the host — a BLOB on both (a File on web, expo-file-system's File on native), because Expo's fetch " +
2821
- "implements the web FormData spec and rejects React Native's legacy { uri, name, type } part outright: append it to a FormData as `file` and pass " +
2822
- "that to ctx.assets.upload(fd) — one code path on both platforms. reset() clears the asset and releases it. Check " +
2831
+ "implements the web FormData spec and rejects React Native's legacy { uri, name, type } part outright: pass it " +
2832
+ "straight to upload(asset.file) from useFilestoreUpload — one code path on both platforms. reset() clears the asset and releases it. Check " +
2823
2833
  "`supported` before rendering a camera button. options: { allowsEditing, quality } are HINTS the host honours where it " +
2824
2834
  "can — the Expo export applies both, the web camera applies quality only, so never depend on a cropped result. " +
2825
2835
  "Behaviour is otherwise identical on web (a getUserMedia preview on EVERY host, phones included; a file input " +
@@ -2885,7 +2895,7 @@ const HOOKS = [
2885
2895
  "Editing is IMPERATIVE — call edit() from an event handler, never during render. " +
2886
2896
  "edit(uri, actions, options?) applies `actions` IN ORDER and resolves the SAME normalised asset shape useCamera() " +
2887
2897
  "yields — { uri, name, mimeType, width, height, size, base64?, file } — so capture -> edit -> upload is ONE code path: " +
2888
- "append result.file to a FormData as `file` and pass it to ctx.assets.upload(fd). " +
2898
+ "pass result.file to upload() from useFilestoreUpload. " +
2889
2899
  "Each action entry carries exactly one of { resize: { width?, height? } } (aspect preserved when only one is given), " +
2890
2900
  "{ crop: { originX, originY, width, height } }, { rotate: degrees } (positive is clockwise), or " +
2891
2901
  "{ flip: \"horizontal\" | \"vertical\" }. A crop rect must lie INSIDE the surface the earlier actions produced, and " +
@@ -3022,7 +3032,7 @@ const PRIMITIVES = [
3022
3032
  },
3023
3033
  // sc-1378 — `<FilePicker accept onPick>` lets a widget surface a native
3024
3034
  // file-picker on the web Player so end users can upload files to the
3025
- // backend (paired with useFilestoreUpload or useAssets.upload). Web wraps
3035
+ // backend (paired with useFilestoreUpload). Web wraps
3026
3036
  // a hidden DOM `<input type="file">`; native renders a disabled trigger
3027
3037
  // and exposes `FilePicker.isSupported = false` until a vetted Expo
3028
3038
  // picker pin lands.
@@ -5852,7 +5862,15 @@ const CONTRACT = deepFreeze({
5852
5862
  // a widget never receives it. `account`'s photo actions are now
5853
5863
  // optional members, and `user.email` is documented as nullable: an
5854
5864
  // account without an email has none.
5855
- version: "1.176.0",
5865
+ // 1.177.0: additive (sc-10937) — `useFilestoreUpload().upload(file,
5866
+ // { recordId })` names the record per call, so a widget can create a row
5867
+ // and attach to it in one handler. A FILE column's file goes in its
5868
+ // row's record space; useCamera / useImageEditor point at
5869
+ // useFilestoreUpload, since widget code cannot reach ctx.assets.
5870
+ // 1.177.1: fix (sc-10410) — a `refresh()` or `verify()` that succeeds
5871
+ // clears `useFileSignature`'s `error`; it used to keep one failed poll's
5872
+ // error for the rest of the order.
5873
+ version: "1.177.1",
5856
5874
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
5857
5875
  hooks: HOOKS,
5858
5876
  primitives: PRIMITIVES,
package/dist/hooks.js CHANGED
@@ -4700,7 +4700,10 @@ export function useFilestoreFile(fileId) {
4700
4700
  * same way the read hooks do (tenant for PROJECT, app user for PERSONAL, the
4701
4701
  * record for RECORD) and posts a multipart FormData to `ctx.filestore.files.upload`.
4702
4702
  *
4703
- * `upload(file, { folderId?, fileName? })` takes a browser `File` on the web
4703
+ * sc-10937: `upload(file, { recordId })` names the record per call, so a
4704
+ * widget can create a row and attach to it in the same handler.
4705
+ *
4706
+ * `upload(file, { folderId?, recordId?, fileName? })` takes a browser `File` on the web
4704
4707
  * Player and expo-file-system's `File` on the Expo export, as `<FilePicker>`,
4705
4708
  * `useCamera()` and `useImageEditor()` all hand back. Never React Native's
4706
4709
  * legacy `{ uri, name, type }` part: the native host streams the file off
@@ -4734,7 +4737,8 @@ export function useFilestoreUpload(options) {
4734
4737
  folderId: defaultFolderId = null,
4735
4738
  compress: defaultCompress = true,
4736
4739
  } = options || {};
4737
- const ownerId = _filestoreOwnerId(ctx, spaceType, recordId);
4740
+ const isRecordSpace = String(spaceType || "project").toUpperCase() === "RECORD";
4741
+ const defaultOwnerId = _filestoreOwnerId(ctx, spaceType, recordId);
4738
4742
 
4739
4743
  const [uploading, setUploading] = useState(false);
4740
4744
  const [error, setError] = useState(null);
@@ -4746,19 +4750,25 @@ export function useFilestoreUpload(options) {
4746
4750
  const upload = useCallback(
4747
4751
  async (file, overrides) => {
4748
4752
  if (!file) throw new Error("useFilestoreUpload: file is required");
4753
+ const hasOverride = (key) =>
4754
+ Boolean(overrides) && Object.prototype.hasOwnProperty.call(overrides, key);
4755
+ const ownerId =
4756
+ isRecordSpace && hasOverride("recordId")
4757
+ ? overrides.recordId || null
4758
+ : defaultOwnerId;
4759
+ if (isRecordSpace && !ownerId) {
4760
+ // Not "Sign in": the widget forgot which row to attach to.
4761
+ const err = new Error("useFilestoreUpload: a record space needs a recordId");
4762
+ setError(err);
4763
+ throw err;
4764
+ }
4749
4765
  if (!ownerId) {
4750
4766
  const err = new Error("Sign in to upload");
4751
4767
  setError(err);
4752
4768
  throw err;
4753
4769
  }
4754
- const folderId =
4755
- overrides && Object.prototype.hasOwnProperty.call(overrides, "folderId")
4756
- ? overrides.folderId
4757
- : defaultFolderId;
4758
- const compress =
4759
- overrides && Object.prototype.hasOwnProperty.call(overrides, "compress")
4760
- ? overrides.compress
4761
- : defaultCompress;
4770
+ const folderId = hasOverride("folderId") ? overrides.folderId : defaultFolderId;
4771
+ const compress = hasOverride("compress") ? overrides.compress : defaultCompress;
4762
4772
  const form = new FormData();
4763
4773
  form.append("space_type", String(spaceType || "project").toUpperCase());
4764
4774
  form.append("owner_id", ownerId);
@@ -4800,7 +4810,7 @@ export function useFilestoreUpload(options) {
4800
4810
  throw err;
4801
4811
  }
4802
4812
  },
4803
- [ownerId, defaultFolderId, defaultCompress, spaceType],
4813
+ [defaultOwnerId, isRecordSpace, defaultFolderId, defaultCompress, spaceType],
4804
4814
  );
4805
4815
 
4806
4816
  return { upload, uploading, error, lastUploaded };
package/dist/index.d.ts CHANGED
@@ -2452,7 +2452,7 @@ export interface CameraAsset {
2452
2452
  /**
2453
2453
  * Ready-to-upload part — a Blob on both hosts: a `File` on web,
2454
2454
  * expo-file-system's `File` on native. Expo's fetch rejects any other shape.
2455
- * Append it to a FormData and hand that to `ctx.assets.upload(...)`.
2455
+ * Pass it to `upload(file)` from `useFilestoreUpload`.
2456
2456
  */
2457
2457
  file: unknown;
2458
2458
  }
@@ -40,7 +40,9 @@ const userIdOf = (ctx) => (ctx.user && ctx.user.id) || "";
40
40
  * with `hintCode: "sealing"`. `hintCode` is where the order stands;
41
41
  * `nameRefusal` (`{ reason, characters }`) is set when it failed because the
42
42
  * signer's name cannot be shown. `autoStartToken` is set only while opening
43
- * BankID can still help. Reads
43
+ * BankID can still help. `error` is the last call that failed; a `refresh()`
44
+ * or `verify()` that succeeds clears it, so one failed poll does not outlive
45
+ * the next answer. Reads
44
46
  * `ctx.filestore.signatures.{initiate,status,cancel,verify}`. When `fileId` is
45
47
  * null/empty the hook collapses to a no-op.
46
48
  *
@@ -128,7 +130,8 @@ export function useFileSignature(fileId, existingSignatureId = null) {
128
130
  const res = await sigRef.current.status(id);
129
131
  // A cancel or a new order since this poll was sent: its answer is old news.
130
132
  if (answersRef.current !== asked) return;
131
- apply(forScope, (base) => ({ order: signOrderStateAfterAnswer(base.order, res) }));
133
+ // An answer means the order can be reached again: an earlier failure is over.
134
+ apply(forScope, (base) => ({ order: signOrderStateAfterAnswer(base.order, res), error: null }));
132
135
  } catch (err) {
133
136
  if (answersRef.current !== asked) return;
134
137
  apply(forScope, () => ({ error: toPermissionError(err) }));
@@ -156,7 +159,7 @@ export function useFileSignature(fileId, existingSignatureId = null) {
156
159
  if (!id) return null;
157
160
  try {
158
161
  const v = await sigRef.current.verify(id);
159
- apply(forScope, () => ({ verdict: v }));
162
+ apply(forScope, () => ({ verdict: v, error: null }));
160
163
  return v;
161
164
  } catch (err) {
162
165
  const refusal = toPermissionError(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.220.0",
3
+ "version": "0.221.1",
4
4
  "description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
5
5
  "homepage": "https://github.com/Colix-AB/AppStudio",
6
6
  "type": "module",
package/src/contract.js CHANGED
@@ -1369,7 +1369,7 @@ const HOOKS = [
1369
1369
  "from the host context, builds a multipart FormData with the " +
1370
1370
  "snake_case fields the backend reads verbatim (`space_type`, " +
1371
1371
  "`owner_id`, `folder_id`, plus the binary `file`), and POSTs through " +
1372
- "ctx.filestore.files.upload. `upload(file, { folderId?, compress?, fileName? })` " +
1372
+ "ctx.filestore.files.upload. `upload(file, { folderId?, recordId?, compress?, fileName? })` " +
1373
1373
  "resolves to the created file row or throws the wire error. `file` must be a BLOB — a browser File on web, " +
1374
1374
  "expo-file-system's File on native (what FilePicker / useCamera / useImageEditor hand back); Expo's fetch rejects " +
1375
1375
  "React Native's legacy { uri, name, type } part with \"Unsupported FormDataPart implementation\". The stored name " +
@@ -1387,13 +1387,20 @@ const HOOKS = [
1387
1387
  "ORIGINAL matters (a document archive, a photo the user re-downloads, " +
1388
1388
  "anything with an exact-bytes requirement). " +
1389
1389
  "ALWAYS pass spaceType explicitly — omitting it falls back to " +
1390
- "'project', which is unreadable by a logged-out visitor. Choose it by " +
1391
- "who must SEE the file: 'public' for content the app displays to " +
1392
- "anyone browsing (product, catalogue, gallery, cover images), " +
1393
- "'project' for content restricted to signed-in workspace users, " +
1394
- "'personal' for a file private to the uploading app user.",
1390
+ "'project', which is unreadable by a logged-out visitor. A file that " +
1391
+ "belongs to a datastore row — anything stored in a FILE column — goes " +
1392
+ "in that row's 'record' space, so exactly the people who may read the " +
1393
+ "row may read the file (logged-out visitors too, when the table lets " +
1394
+ "everyone read). A file can only be attached to a row that exists: " +
1395
+ "create the row, call upload(file, { recordId: row.id }) — the per-call " +
1396
+ "recordId names the row the hook could not know at render — then write " +
1397
+ "the returned id into the FILE column with update(). A logged-out " +
1398
+ "visitor cannot upload into a record space. For a file that belongs to " +
1399
+ "no row: 'public' for content the app shows to anyone browsing, " +
1400
+ "'project' for signed-in workspace users only, 'personal' for a file " +
1401
+ "private to the uploading app user.",
1395
1402
  returnShape: {
1396
- upload: "(file, { folderId?, compress?, fileName? }) => Promise<FilestoreFile>",
1403
+ upload: "(file, { folderId?, recordId?, compress?, fileName? }) => Promise<FilestoreFile>",
1397
1404
  uploading: "boolean",
1398
1405
  error: "Error | null",
1399
1406
  lastUploaded: "FilestoreFile | null",
@@ -1552,7 +1559,10 @@ const HOOKS = [
1552
1559
  "be cancelled. cancel() resolves to what became of the order, which is " +
1553
1560
  "then still pending or already complete: read status after it, never " +
1554
1561
  "assume 'cancelled'. nameRefusal is set when the order failed because " +
1555
- "the signer's name cannot be shown on the document. Reads " +
1562
+ "the signer's name cannot be shown on the document. error is the last " +
1563
+ "call that failed, and a refresh() or verify() that succeeds clears it: " +
1564
+ "while status is 'pending' an error means one poll failed, so keep " +
1565
+ "polling and keep showing the order. Reads " +
1556
1566
  "ctx.filestore.signatures.{initiate,status,cancel,verify}.",
1557
1567
  returnShape: {
1558
1568
  status: "'pending' | 'complete' | 'failed' | 'cancelled' | null",
@@ -2818,8 +2828,8 @@ const HOOKS = [
2818
2828
  "common case and is deliberately NOT an error, so no try/catch is needed on the happy path. They reject with a " +
2819
2829
  "CameraError whose .code is one of PERMISSION_DENIED | UNSUPPORTED | INTERNAL. `asset.file` is already the right " +
2820
2830
  "upload part for the host — a BLOB on both (a File on web, expo-file-system's File on native), because Expo's fetch " +
2821
- "implements the web FormData spec and rejects React Native's legacy { uri, name, type } part outright: append it to a FormData as `file` and pass " +
2822
- "that to ctx.assets.upload(fd) — one code path on both platforms. reset() clears the asset and releases it. Check " +
2831
+ "implements the web FormData spec and rejects React Native's legacy { uri, name, type } part outright: pass it " +
2832
+ "straight to upload(asset.file) from useFilestoreUpload — one code path on both platforms. reset() clears the asset and releases it. Check " +
2823
2833
  "`supported` before rendering a camera button. options: { allowsEditing, quality } are HINTS the host honours where it " +
2824
2834
  "can — the Expo export applies both, the web camera applies quality only, so never depend on a cropped result. " +
2825
2835
  "Behaviour is otherwise identical on web (a getUserMedia preview on EVERY host, phones included; a file input " +
@@ -2885,7 +2895,7 @@ const HOOKS = [
2885
2895
  "Editing is IMPERATIVE — call edit() from an event handler, never during render. " +
2886
2896
  "edit(uri, actions, options?) applies `actions` IN ORDER and resolves the SAME normalised asset shape useCamera() " +
2887
2897
  "yields — { uri, name, mimeType, width, height, size, base64?, file } — so capture -> edit -> upload is ONE code path: " +
2888
- "append result.file to a FormData as `file` and pass it to ctx.assets.upload(fd). " +
2898
+ "pass result.file to upload() from useFilestoreUpload. " +
2889
2899
  "Each action entry carries exactly one of { resize: { width?, height? } } (aspect preserved when only one is given), " +
2890
2900
  "{ crop: { originX, originY, width, height } }, { rotate: degrees } (positive is clockwise), or " +
2891
2901
  "{ flip: \"horizontal\" | \"vertical\" }. A crop rect must lie INSIDE the surface the earlier actions produced, and " +
@@ -3022,7 +3032,7 @@ const PRIMITIVES = [
3022
3032
  },
3023
3033
  // sc-1378 — `<FilePicker accept onPick>` lets a widget surface a native
3024
3034
  // file-picker on the web Player so end users can upload files to the
3025
- // backend (paired with useFilestoreUpload or useAssets.upload). Web wraps
3035
+ // backend (paired with useFilestoreUpload). Web wraps
3026
3036
  // a hidden DOM `<input type="file">`; native renders a disabled trigger
3027
3037
  // and exposes `FilePicker.isSupported = false` until a vetted Expo
3028
3038
  // picker pin lands.
@@ -5852,7 +5862,15 @@ const CONTRACT = deepFreeze({
5852
5862
  // a widget never receives it. `account`'s photo actions are now
5853
5863
  // optional members, and `user.email` is documented as nullable: an
5854
5864
  // account without an email has none.
5855
- version: "1.176.0",
5865
+ // 1.177.0: additive (sc-10937) — `useFilestoreUpload().upload(file,
5866
+ // { recordId })` names the record per call, so a widget can create a row
5867
+ // and attach to it in one handler. A FILE column's file goes in its
5868
+ // row's record space; useCamera / useImageEditor point at
5869
+ // useFilestoreUpload, since widget code cannot reach ctx.assets.
5870
+ // 1.177.1: fix (sc-10410) — a `refresh()` or `verify()` that succeeds
5871
+ // clears `useFileSignature`'s `error`; it used to keep one failed poll's
5872
+ // error for the rest of the order.
5873
+ version: "1.177.1",
5856
5874
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
5857
5875
  hooks: HOOKS,
5858
5876
  primitives: PRIMITIVES,