@colixsystems/widget-sdk 0.219.1 → 0.221.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 +63 -8
- package/dist/contract.cjs +78 -22
- package/dist/contract.js +78 -22
- package/dist/hooks.js +95 -11
- package/dist/index.d.ts +64 -7
- package/dist/index.js +1 -0
- package/dist/index.native.js +1 -0
- package/package.json +1 -1
- package/src/contract.js +78 -22
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
23
23
|
| **CORE** | `useWidgetStyle()` | `{ [styleField]: value }` | `ctx.props.style` — no scope. The author-set per-widget style values declared in `manifest.styleSchema`; apply each onto whatever element you choose. |
|
|
24
24
|
| **CORE** | `useUser()` | `{ id, email, displayName, photoUrl, roles, groupIds }` | `ctx.user` (host-built context, **camelCase** — not a wire payload; `id` null when anonymous) — no scope. `photoUrl` is the user's own profile photo as an absolute URL, or `null`. |
|
|
25
25
|
| **CORE** | `useUserPhoto()` | `{ url, canEdit, upload, remove, uploading, error }` | `ctx.user` + the optional `ctx.account` — no scope. Changes the signed-in user's OWN profile photo. `canEdit` is false when nobody is signed in or the host cannot change it; `upload` / `remove` then reject. |
|
|
26
|
+
| **CORE** | `useAnonymousAccount()` | `{ supported, canCreate, creating, error, create }` | The optional `ctx.account.anonymous` — no scope. Offers an account with a generated login code and password and no email. `create()` opens the app's own dialog, which confirms, creates the account, shows the pair once and signs the visitor in; the widget never receives the pair and `create()` resolves `{ created: true }`. `supported` is false where the workspace has not turned anonymous accounts on, on the Builder canvas, in a preview and for a badge counter; `canCreate` is false while someone is signed in. |
|
|
26
27
|
| **CORE** | `useNavigation()` | `{ goTo, goBack, push, replace, back, currentRoute, openLink }` | `ctx.navigation` — no scope. `goTo(pageIdOrSlug, params?)` accepts a page UUID (a `pageRef` prop) **or its slug** (a row's page-key field) — both hosts resolve either (`openLink` for a link of unknown shape; a known external URL can also use the `Linking` primitive) |
|
|
27
28
|
| **CORE** | `useFilePreview(file)` | `{ uri }` | No scope. A temporary address an `<Image>` can show for a file the person picked but has not uploaded yet (a photo in a composer). Native picks use their own uri; on the web the SDK creates and releases an object URL, which widgets may not do themselves. `uri` is null for no file. |
|
|
28
29
|
| **CORE** | `usePageBadge(count)` | `{ supported }` | `ctx.navigation.setBadge` — no scope. Puts `count` on the menu entry or tab of the page the widget is on (a chat's unread messages). The host sums the page's widgets and keeps the last count after the widget leaves the screen, until it reports again; `0` clears this widget's share. `supported` is false where the host draws no page badges (the Builder canvas). |
|
|
@@ -83,7 +84,65 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
83
84
|
|
|
84
85
|
## Status
|
|
85
86
|
|
|
86
|
-
`v0.
|
|
87
|
+
`v0.221.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**.
|
|
88
|
+
|
|
89
|
+
### What's new in 0.221.0 (contract 1.177.0)
|
|
90
|
+
|
|
91
|
+
**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`:
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
const { create, update } = useDatastoreMutation(tableId);
|
|
95
|
+
const { upload } = useFilestoreUpload({ spaceType: "record" });
|
|
96
|
+
|
|
97
|
+
const row = await create({ Title: title }); // without the file
|
|
98
|
+
const stored = await upload(file, { recordId: row.id }); // into that row's space
|
|
99
|
+
await update(row.id, { Receipt: stored.id });
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
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.
|
|
103
|
+
|
|
104
|
+
### What's new in 0.220.0 (contract 1.176.0)
|
|
105
|
+
|
|
106
|
+
**An app can let a visitor continue without an email — `useAnonymousAccount()`.** Some apps must let a person act without identifying themselves and still come back later: someone files a report and returns to follow it. The new hook lets a widget offer an account that has no email address. The server generates a login code and a password, and the visitor signs in again later with that pair on the app's sign-in screen ("Sign in with a login code").
|
|
107
|
+
|
|
108
|
+
```jsx
|
|
109
|
+
import { useAnonymousAccount, useUser, Button, Text, View } from "@colixsystems/widget-sdk";
|
|
110
|
+
|
|
111
|
+
export default function ContinueWithoutEmail() {
|
|
112
|
+
const user = useUser();
|
|
113
|
+
const { supported, canCreate, creating, error, create } = useAnonymousAccount();
|
|
114
|
+
|
|
115
|
+
if (!supported) {
|
|
116
|
+
return <Text>Continuing without an email is not available here.</Text>;
|
|
117
|
+
}
|
|
118
|
+
if (user.id) {
|
|
119
|
+
return <Text>You are signed in.</Text>;
|
|
120
|
+
}
|
|
121
|
+
if (!canCreate) return null;
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<View>
|
|
125
|
+
<Button
|
|
126
|
+
title="Continue without an email"
|
|
127
|
+
disabled={creating}
|
|
128
|
+
onPress={() => create().catch(() => {})}
|
|
129
|
+
/>
|
|
130
|
+
{error && error.code !== "CANCELLED" ? (
|
|
131
|
+
<Text>Could not create the account. Try again.</Text>
|
|
132
|
+
) : null}
|
|
133
|
+
</View>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
- **The widget only asks; the app does the rest.** `create()` takes no arguments and opens the app's own dialog. It asks the visitor to confirm, shows the captcha notice when a captcha will run, creates the account, shows the login code and password once with the warning that a lost password cannot be recovered, and signs the visitor in when they confirm they saved them (or opens the sign-in screen's two-factor step when the workspace requires one). Do not build a confirmation, a captcha notice or a credentials screen of your own.
|
|
139
|
+
- **The widget never receives the login code or the password.** `create()` resolves `{ created: true }` and nothing else. Widgets on a page share one context, so a credential that cannot be reset is kept out of it altogether. This is a contract boundary, not a sandbox: the SDK never hands the pair to a widget, but every widget on a page still runs inside the app.
|
|
140
|
+
- **Call `create()` from a user action.** A button's `onPress`, never on mount or in an effect. Nothing is created until the visitor confirms in the dialog.
|
|
141
|
+
- **Failures carry a `code`.** `create()` rejects with an Error whose `code` is the server's refusal (`ANONYMOUS_ACCOUNT_LIMIT_REACHED`, `RATE_LIMITED`, `CAPTCHA_FAILED`, …), `CAPTCHA_UNAVAILABLE` when the captcha could not run (there is no fallback), `ALREADY_SIGNED_IN`, `CANCELLED` when the visitor closes the dialog, or `UNSUPPORTED`. The dialog shows a failure itself and lets the visitor retry, so a rejection means they gave up; treat `CANCELLED` as no error. `error` holds the last one.
|
|
142
|
+
- **`supported` and `canCreate`.** `supported` is false where the workspace has not turned anonymous accounts on, on the Builder canvas, in a preview and for a badge counter: render a visible "not available" state. `canCreate` is false while someone is signed in. The hook never throws at render and needs no scope.
|
|
143
|
+
- **What the account can do.** It is not treated as a signed-in member: it gets only what the workspace grants to everyone or to the group chosen for these accounts, plus the records it owns. `useUser().email` is `null` for it.
|
|
144
|
+
|
|
145
|
+
A host backs the hook with the new optional `WidgetContext.account.anonymous` member, `{ canCreate, create() }`. `account`'s photo actions are now optional members, so a host may publish `account` with either or both. Additive.
|
|
87
146
|
|
|
88
147
|
### What's new in 0.219.1
|
|
89
148
|
|
|
@@ -1452,9 +1511,7 @@ const { edit } = useImageEditor();
|
|
|
1452
1511
|
const shot = await capture();
|
|
1453
1512
|
const small = await edit(shot.uri, [{ resize: { width: 800 } }], { format: "jpeg", compress: 0.8 });
|
|
1454
1513
|
|
|
1455
|
-
|
|
1456
|
-
fd.append("file", small.file);
|
|
1457
|
-
await ctx.assets.upload(fd);
|
|
1514
|
+
await upload(small.file); // upload from useFilestoreUpload()
|
|
1458
1515
|
```
|
|
1459
1516
|
|
|
1460
1517
|
`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? }`.
|
|
@@ -1668,12 +1725,10 @@ When you take the `useCanWrite`-only route, omit the USER column for a guest (`i
|
|
|
1668
1725
|
|
|
1669
1726
|
**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`).
|
|
1670
1727
|
|
|
1671
|
-
**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
|
|
1728
|
+
**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:
|
|
1672
1729
|
|
|
1673
1730
|
```js
|
|
1674
|
-
|
|
1675
|
-
fd.append("file", asset.file);
|
|
1676
|
-
await ctx.assets.upload(fd);
|
|
1731
|
+
await upload(asset.file); // upload from useFilestoreUpload()
|
|
1677
1732
|
```
|
|
1678
1733
|
|
|
1679
1734
|
`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
|
@@ -938,6 +938,38 @@ const HOOKS = [
|
|
|
938
938
|
requiredContextSlice: ["user", "account.setPhoto", "account.removePhoto"],
|
|
939
939
|
scopes: null,
|
|
940
940
|
},
|
|
941
|
+
{
|
|
942
|
+
name: "useAnonymousAccount",
|
|
943
|
+
signature: "useAnonymousAccount()",
|
|
944
|
+
description:
|
|
945
|
+
"Offer an account that needs NO email address: the server generates a login code and a password, and the person signs in " +
|
|
946
|
+
"later with that pair on the app's sign-in screen. For apps where a visitor must be able to act without identifying " +
|
|
947
|
+
"themselves and still come back (a whistleblower report). The widget ONLY ASKS: create() takes NO arguments and opens the APP'S " +
|
|
948
|
+
"OWN dialog, which asks the visitor to confirm, runs the captcha, creates the account, shows the login code and password " +
|
|
949
|
+
"ONCE with the warning that a lost password cannot be recovered, and signs the visitor in when they confirm they saved " +
|
|
950
|
+
"them (or opens the sign-in screen's two-factor step when the workspace requires one). The widget NEVER receives the " +
|
|
951
|
+
"login code or the password: create() resolves { created: true } and nothing else. Do NOT build your own confirmation, " +
|
|
952
|
+
"captcha notice or credentials screen, and never call an endpoint yourself. Call create() from a user action (a button's " +
|
|
953
|
+
"onPress), never on mount or in an effect. It rejects with an Error carrying code: the server's refusal " +
|
|
954
|
+
"(ANONYMOUS_ACCOUNT_LIMIT_REACHED, RATE_LIMITED, CAPTCHA_FAILED, …), CAPTCHA_UNAVAILABLE when the " +
|
|
955
|
+
"captcha could not run (there is no fallback), ALREADY_SIGNED_IN, CANCELLED when the visitor closes the dialog, or " +
|
|
956
|
+
"UNSUPPORTED. The dialog already shows a failure and lets the visitor retry, so a rejection means they gave up: treat " +
|
|
957
|
+
"CANCELLED as no error. supported is false — and create() rejects with UNSUPPORTED instead of the hook throwing at " +
|
|
958
|
+
"render — where the workspace has not turned anonymous accounts on, on the Builder canvas, in a preview pane, in a draft " +
|
|
959
|
+
"preview and for a badge counter: render a visible 'not available' state then. canCreate is false while someone is " +
|
|
960
|
+
"signed in: hide the button then. After create() resolves the visitor is signed in and the page re-renders, so read " +
|
|
961
|
+
"useUser() for the new state. Needs no scope. Identical on web (Player) and the Expo export.",
|
|
962
|
+
returnShape: {
|
|
963
|
+
supported: "boolean // the host offers anonymous accounts here",
|
|
964
|
+
canCreate: "boolean // supported AND nobody is signed in",
|
|
965
|
+
creating: "boolean // a create() call is in flight: the app's dialog is open",
|
|
966
|
+
error: "(Error & { code?: string, status?: number }) | null // the last create() failure; cleared by the next attempt",
|
|
967
|
+
create:
|
|
968
|
+
"() => Promise<{ created: true }> // takes no arguments; opens the app's dialog; never returns the login code or password",
|
|
969
|
+
},
|
|
970
|
+
requiredContextSlice: ["account.anonymous"],
|
|
971
|
+
scopes: null,
|
|
972
|
+
},
|
|
941
973
|
{
|
|
942
974
|
name: "useFill",
|
|
943
975
|
signature: "useFill()",
|
|
@@ -1337,7 +1369,7 @@ const HOOKS = [
|
|
|
1337
1369
|
"from the host context, builds a multipart FormData with the " +
|
|
1338
1370
|
"snake_case fields the backend reads verbatim (`space_type`, " +
|
|
1339
1371
|
"`owner_id`, `folder_id`, plus the binary `file`), and POSTs through " +
|
|
1340
|
-
"ctx.filestore.files.upload. `upload(file, { folderId?, compress?, fileName? })` " +
|
|
1372
|
+
"ctx.filestore.files.upload. `upload(file, { folderId?, recordId?, compress?, fileName? })` " +
|
|
1341
1373
|
"resolves to the created file row or throws the wire error. `file` must be a BLOB — a browser File on web, " +
|
|
1342
1374
|
"expo-file-system's File on native (what FilePicker / useCamera / useImageEditor hand back); Expo's fetch rejects " +
|
|
1343
1375
|
"React Native's legacy { uri, name, type } part with \"Unsupported FormDataPart implementation\". The stored name " +
|
|
@@ -1355,13 +1387,20 @@ const HOOKS = [
|
|
|
1355
1387
|
"ORIGINAL matters (a document archive, a photo the user re-downloads, " +
|
|
1356
1388
|
"anything with an exact-bytes requirement). " +
|
|
1357
1389
|
"ALWAYS pass spaceType explicitly — omitting it falls back to " +
|
|
1358
|
-
"'project', which is unreadable by a logged-out visitor.
|
|
1359
|
-
"
|
|
1360
|
-
"
|
|
1361
|
-
"
|
|
1362
|
-
"
|
|
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.",
|
|
1363
1402
|
returnShape: {
|
|
1364
|
-
upload: "(file, { folderId?, compress?, fileName? }) => Promise<FilestoreFile>",
|
|
1403
|
+
upload: "(file, { folderId?, recordId?, compress?, fileName? }) => Promise<FilestoreFile>",
|
|
1365
1404
|
uploading: "boolean",
|
|
1366
1405
|
error: "Error | null",
|
|
1367
1406
|
lastUploaded: "FilestoreFile | null",
|
|
@@ -2786,8 +2825,8 @@ const HOOKS = [
|
|
|
2786
2825
|
"common case and is deliberately NOT an error, so no try/catch is needed on the happy path. They reject with a " +
|
|
2787
2826
|
"CameraError whose .code is one of PERMISSION_DENIED | UNSUPPORTED | INTERNAL. `asset.file` is already the right " +
|
|
2788
2827
|
"upload part for the host — a BLOB on both (a File on web, expo-file-system's File on native), because Expo's fetch " +
|
|
2789
|
-
"implements the web FormData spec and rejects React Native's legacy { uri, name, type } part outright:
|
|
2790
|
-
"
|
|
2828
|
+
"implements the web FormData spec and rejects React Native's legacy { uri, name, type } part outright: pass it " +
|
|
2829
|
+
"straight to upload(asset.file) from useFilestoreUpload — one code path on both platforms. reset() clears the asset and releases it. Check " +
|
|
2791
2830
|
"`supported` before rendering a camera button. options: { allowsEditing, quality } are HINTS the host honours where it " +
|
|
2792
2831
|
"can — the Expo export applies both, the web camera applies quality only, so never depend on a cropped result. " +
|
|
2793
2832
|
"Behaviour is otherwise identical on web (a getUserMedia preview on EVERY host, phones included; a file input " +
|
|
@@ -2853,7 +2892,7 @@ const HOOKS = [
|
|
|
2853
2892
|
"Editing is IMPERATIVE — call edit() from an event handler, never during render. " +
|
|
2854
2893
|
"edit(uri, actions, options?) applies `actions` IN ORDER and resolves the SAME normalised asset shape useCamera() " +
|
|
2855
2894
|
"yields — { uri, name, mimeType, width, height, size, base64?, file } — so capture -> edit -> upload is ONE code path: " +
|
|
2856
|
-
"
|
|
2895
|
+
"pass result.file to upload() from useFilestoreUpload. " +
|
|
2857
2896
|
"Each action entry carries exactly one of { resize: { width?, height? } } (aspect preserved when only one is given), " +
|
|
2858
2897
|
"{ crop: { originX, originY, width, height } }, { rotate: degrees } (positive is clockwise), or " +
|
|
2859
2898
|
"{ flip: \"horizontal\" | \"vertical\" }. A crop rect must lie INSIDE the surface the earlier actions produced, and " +
|
|
@@ -2990,7 +3029,7 @@ const PRIMITIVES = [
|
|
|
2990
3029
|
},
|
|
2991
3030
|
// sc-1378 — `<FilePicker accept onPick>` lets a widget surface a native
|
|
2992
3031
|
// file-picker on the web Player so end users can upload files to the
|
|
2993
|
-
// backend (paired with useFilestoreUpload
|
|
3032
|
+
// backend (paired with useFilestoreUpload). Web wraps
|
|
2994
3033
|
// a hidden DOM `<input type="file">`; native renders a disabled trigger
|
|
2995
3034
|
// and exposes `FilePicker.isSupported = false` until a vetted Expo
|
|
2996
3035
|
// picker pin lands.
|
|
@@ -3565,26 +3604,31 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
3565
3604
|
},
|
|
3566
3605
|
user: {
|
|
3567
3606
|
description:
|
|
3568
|
-
"Signed-in user, host-built context object (camelCase: { id, email, displayName, photoUrl, roles, groupIds }) — NOT a wire payload, so it is the one camelCase island among the hooks. photoUrl is the user's own profile photo as an absolute URL, or null. Not a data-client.",
|
|
3607
|
+
"Signed-in user, host-built context object (camelCase: { id, email, displayName, photoUrl, roles, groupIds }) — NOT a wire payload, so it is the one camelCase island among the hooks. photoUrl is the user's own profile photo as an absolute URL, or null. email is null for an account created without one. Not a data-client.",
|
|
3569
3608
|
required: true,
|
|
3570
3609
|
fields: {
|
|
3571
3610
|
id: "string",
|
|
3572
|
-
email: "string",
|
|
3611
|
+
email: "string | null",
|
|
3573
3612
|
displayName: "string",
|
|
3574
3613
|
photoUrl: "string | null",
|
|
3575
3614
|
},
|
|
3576
3615
|
},
|
|
3577
|
-
// Optional so a host
|
|
3578
|
-
// useUserPhoto() reports canEdit false when it is absent.
|
|
3616
|
+
// Optional, and so is every member: a host publishes only what it can do.
|
|
3579
3617
|
account: {
|
|
3580
3618
|
description:
|
|
3581
|
-
"Optional self-service account actions
|
|
3582
|
-
"{ setPhoto(file) -> Promise<{ photoUrl }>, removePhoto() -> Promise<{ photoUrl: null }> }
|
|
3583
|
-
"
|
|
3584
|
-
"widget. setPhoto takes a browser File on web and expo-file-system's File on native.
|
|
3585
|
-
"
|
|
3619
|
+
"Optional self-service account actions; every member is optional too. " +
|
|
3620
|
+
"{ setPhoto(file) -> Promise<{ photoUrl }>, removePhoto() -> Promise<{ photoUrl: null }> } back useUserPhoto() and are " +
|
|
3621
|
+
"present only while a user is signed in. The host performs the request AND updates its own session, so user.photoUrl " +
|
|
3622
|
+
"changes for every widget. setPhoto takes a browser File on web and expo-file-system's File on native. " +
|
|
3623
|
+
"anonymous backs useAnonymousAccount() and is present only where the workspace offers accounts without an email: " +
|
|
3624
|
+
"{ canCreate: boolean, create() -> Promise<{ created: true }> }. create() opens the HOST'S OWN dialog: " +
|
|
3625
|
+
"it asks the visitor to confirm, runs the captcha, calls the server, shows the login code and password once, and signs " +
|
|
3626
|
+
"the visitor in when they confirm they saved them. The pair lives only in that dialog and never reaches the slice, a " +
|
|
3627
|
+
"widget, an error or storage. " +
|
|
3628
|
+
"The whole slice is absent on the Builder canvas, in the preview panes and for a badge counter, where both hooks " +
|
|
3629
|
+
"degrade (canEdit: false, supported: false) rather than throwing.",
|
|
3586
3630
|
required: false,
|
|
3587
|
-
fields: { setPhoto: "function", removePhoto: "function" },
|
|
3631
|
+
fields: { setPhoto: "function", removePhoto: "function", anonymous: "object" },
|
|
3588
3632
|
},
|
|
3589
3633
|
workspace: {
|
|
3590
3634
|
description:
|
|
@@ -5812,7 +5856,19 @@ const CONTRACT = deepFreeze({
|
|
|
5812
5856
|
// new `ctx.filestore.signingRounds`. `useFileSignature` also returns
|
|
5813
5857
|
// `hintCode` and `nameRefusal`, and its `cancel()` resolves to what
|
|
5814
5858
|
// became of the order instead of always reporting "cancelled".
|
|
5815
|
-
|
|
5859
|
+
// 1.176.0: additive (sc-10934) — `useAnonymousAccount()` asks the host to
|
|
5860
|
+
// create an account with a generated login code and password and no
|
|
5861
|
+
// email, through the new optional `ctx.account.anonymous`
|
|
5862
|
+
// (`{ canCreate, create }`). The host shows the pair in its own dialog;
|
|
5863
|
+
// a widget never receives it. `account`'s photo actions are now
|
|
5864
|
+
// optional members, and `user.email` is documented as nullable: an
|
|
5865
|
+
// account without an email has none.
|
|
5866
|
+
// 1.177.0: additive (sc-10937) — `useFilestoreUpload().upload(file,
|
|
5867
|
+
// { recordId })` names the record per call, so a widget can create a row
|
|
5868
|
+
// and attach to it in one handler. A FILE column's file goes in its
|
|
5869
|
+
// row's record space; useCamera / useImageEditor point at
|
|
5870
|
+
// useFilestoreUpload, since widget code cannot reach ctx.assets.
|
|
5871
|
+
version: "1.177.0",
|
|
5816
5872
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5817
5873
|
hooks: HOOKS,
|
|
5818
5874
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -938,6 +938,38 @@ const HOOKS = [
|
|
|
938
938
|
requiredContextSlice: ["user", "account.setPhoto", "account.removePhoto"],
|
|
939
939
|
scopes: null,
|
|
940
940
|
},
|
|
941
|
+
{
|
|
942
|
+
name: "useAnonymousAccount",
|
|
943
|
+
signature: "useAnonymousAccount()",
|
|
944
|
+
description:
|
|
945
|
+
"Offer an account that needs NO email address: the server generates a login code and a password, and the person signs in " +
|
|
946
|
+
"later with that pair on the app's sign-in screen. For apps where a visitor must be able to act without identifying " +
|
|
947
|
+
"themselves and still come back (a whistleblower report). The widget ONLY ASKS: create() takes NO arguments and opens the APP'S " +
|
|
948
|
+
"OWN dialog, which asks the visitor to confirm, runs the captcha, creates the account, shows the login code and password " +
|
|
949
|
+
"ONCE with the warning that a lost password cannot be recovered, and signs the visitor in when they confirm they saved " +
|
|
950
|
+
"them (or opens the sign-in screen's two-factor step when the workspace requires one). The widget NEVER receives the " +
|
|
951
|
+
"login code or the password: create() resolves { created: true } and nothing else. Do NOT build your own confirmation, " +
|
|
952
|
+
"captcha notice or credentials screen, and never call an endpoint yourself. Call create() from a user action (a button's " +
|
|
953
|
+
"onPress), never on mount or in an effect. It rejects with an Error carrying code: the server's refusal " +
|
|
954
|
+
"(ANONYMOUS_ACCOUNT_LIMIT_REACHED, RATE_LIMITED, CAPTCHA_FAILED, …), CAPTCHA_UNAVAILABLE when the " +
|
|
955
|
+
"captcha could not run (there is no fallback), ALREADY_SIGNED_IN, CANCELLED when the visitor closes the dialog, or " +
|
|
956
|
+
"UNSUPPORTED. The dialog already shows a failure and lets the visitor retry, so a rejection means they gave up: treat " +
|
|
957
|
+
"CANCELLED as no error. supported is false — and create() rejects with UNSUPPORTED instead of the hook throwing at " +
|
|
958
|
+
"render — where the workspace has not turned anonymous accounts on, on the Builder canvas, in a preview pane, in a draft " +
|
|
959
|
+
"preview and for a badge counter: render a visible 'not available' state then. canCreate is false while someone is " +
|
|
960
|
+
"signed in: hide the button then. After create() resolves the visitor is signed in and the page re-renders, so read " +
|
|
961
|
+
"useUser() for the new state. Needs no scope. Identical on web (Player) and the Expo export.",
|
|
962
|
+
returnShape: {
|
|
963
|
+
supported: "boolean // the host offers anonymous accounts here",
|
|
964
|
+
canCreate: "boolean // supported AND nobody is signed in",
|
|
965
|
+
creating: "boolean // a create() call is in flight: the app's dialog is open",
|
|
966
|
+
error: "(Error & { code?: string, status?: number }) | null // the last create() failure; cleared by the next attempt",
|
|
967
|
+
create:
|
|
968
|
+
"() => Promise<{ created: true }> // takes no arguments; opens the app's dialog; never returns the login code or password",
|
|
969
|
+
},
|
|
970
|
+
requiredContextSlice: ["account.anonymous"],
|
|
971
|
+
scopes: null,
|
|
972
|
+
},
|
|
941
973
|
{
|
|
942
974
|
name: "useFill",
|
|
943
975
|
signature: "useFill()",
|
|
@@ -1337,7 +1369,7 @@ const HOOKS = [
|
|
|
1337
1369
|
"from the host context, builds a multipart FormData with the " +
|
|
1338
1370
|
"snake_case fields the backend reads verbatim (`space_type`, " +
|
|
1339
1371
|
"`owner_id`, `folder_id`, plus the binary `file`), and POSTs through " +
|
|
1340
|
-
"ctx.filestore.files.upload. `upload(file, { folderId?, compress?, fileName? })` " +
|
|
1372
|
+
"ctx.filestore.files.upload. `upload(file, { folderId?, recordId?, compress?, fileName? })` " +
|
|
1341
1373
|
"resolves to the created file row or throws the wire error. `file` must be a BLOB — a browser File on web, " +
|
|
1342
1374
|
"expo-file-system's File on native (what FilePicker / useCamera / useImageEditor hand back); Expo's fetch rejects " +
|
|
1343
1375
|
"React Native's legacy { uri, name, type } part with \"Unsupported FormDataPart implementation\". The stored name " +
|
|
@@ -1355,13 +1387,20 @@ const HOOKS = [
|
|
|
1355
1387
|
"ORIGINAL matters (a document archive, a photo the user re-downloads, " +
|
|
1356
1388
|
"anything with an exact-bytes requirement). " +
|
|
1357
1389
|
"ALWAYS pass spaceType explicitly — omitting it falls back to " +
|
|
1358
|
-
"'project', which is unreadable by a logged-out visitor.
|
|
1359
|
-
"
|
|
1360
|
-
"
|
|
1361
|
-
"
|
|
1362
|
-
"
|
|
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.",
|
|
1363
1402
|
returnShape: {
|
|
1364
|
-
upload: "(file, { folderId?, compress?, fileName? }) => Promise<FilestoreFile>",
|
|
1403
|
+
upload: "(file, { folderId?, recordId?, compress?, fileName? }) => Promise<FilestoreFile>",
|
|
1365
1404
|
uploading: "boolean",
|
|
1366
1405
|
error: "Error | null",
|
|
1367
1406
|
lastUploaded: "FilestoreFile | null",
|
|
@@ -2786,8 +2825,8 @@ const HOOKS = [
|
|
|
2786
2825
|
"common case and is deliberately NOT an error, so no try/catch is needed on the happy path. They reject with a " +
|
|
2787
2826
|
"CameraError whose .code is one of PERMISSION_DENIED | UNSUPPORTED | INTERNAL. `asset.file` is already the right " +
|
|
2788
2827
|
"upload part for the host — a BLOB on both (a File on web, expo-file-system's File on native), because Expo's fetch " +
|
|
2789
|
-
"implements the web FormData spec and rejects React Native's legacy { uri, name, type } part outright:
|
|
2790
|
-
"
|
|
2828
|
+
"implements the web FormData spec and rejects React Native's legacy { uri, name, type } part outright: pass it " +
|
|
2829
|
+
"straight to upload(asset.file) from useFilestoreUpload — one code path on both platforms. reset() clears the asset and releases it. Check " +
|
|
2791
2830
|
"`supported` before rendering a camera button. options: { allowsEditing, quality } are HINTS the host honours where it " +
|
|
2792
2831
|
"can — the Expo export applies both, the web camera applies quality only, so never depend on a cropped result. " +
|
|
2793
2832
|
"Behaviour is otherwise identical on web (a getUserMedia preview on EVERY host, phones included; a file input " +
|
|
@@ -2853,7 +2892,7 @@ const HOOKS = [
|
|
|
2853
2892
|
"Editing is IMPERATIVE — call edit() from an event handler, never during render. " +
|
|
2854
2893
|
"edit(uri, actions, options?) applies `actions` IN ORDER and resolves the SAME normalised asset shape useCamera() " +
|
|
2855
2894
|
"yields — { uri, name, mimeType, width, height, size, base64?, file } — so capture -> edit -> upload is ONE code path: " +
|
|
2856
|
-
"
|
|
2895
|
+
"pass result.file to upload() from useFilestoreUpload. " +
|
|
2857
2896
|
"Each action entry carries exactly one of { resize: { width?, height? } } (aspect preserved when only one is given), " +
|
|
2858
2897
|
"{ crop: { originX, originY, width, height } }, { rotate: degrees } (positive is clockwise), or " +
|
|
2859
2898
|
"{ flip: \"horizontal\" | \"vertical\" }. A crop rect must lie INSIDE the surface the earlier actions produced, and " +
|
|
@@ -2990,7 +3029,7 @@ const PRIMITIVES = [
|
|
|
2990
3029
|
},
|
|
2991
3030
|
// sc-1378 — `<FilePicker accept onPick>` lets a widget surface a native
|
|
2992
3031
|
// file-picker on the web Player so end users can upload files to the
|
|
2993
|
-
// backend (paired with useFilestoreUpload
|
|
3032
|
+
// backend (paired with useFilestoreUpload). Web wraps
|
|
2994
3033
|
// a hidden DOM `<input type="file">`; native renders a disabled trigger
|
|
2995
3034
|
// and exposes `FilePicker.isSupported = false` until a vetted Expo
|
|
2996
3035
|
// picker pin lands.
|
|
@@ -3561,26 +3600,31 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
3561
3600
|
},
|
|
3562
3601
|
user: {
|
|
3563
3602
|
description:
|
|
3564
|
-
"Signed-in user, host-built context object (camelCase: { id, email, displayName, photoUrl, roles, groupIds }) — NOT a wire payload, so it is the one camelCase island among the hooks. photoUrl is the user's own profile photo as an absolute URL, or null. Not a data-client.",
|
|
3603
|
+
"Signed-in user, host-built context object (camelCase: { id, email, displayName, photoUrl, roles, groupIds }) — NOT a wire payload, so it is the one camelCase island among the hooks. photoUrl is the user's own profile photo as an absolute URL, or null. email is null for an account created without one. Not a data-client.",
|
|
3565
3604
|
required: true,
|
|
3566
3605
|
fields: {
|
|
3567
3606
|
id: "string",
|
|
3568
|
-
email: "string",
|
|
3607
|
+
email: "string | null",
|
|
3569
3608
|
displayName: "string",
|
|
3570
3609
|
photoUrl: "string | null",
|
|
3571
3610
|
},
|
|
3572
3611
|
},
|
|
3573
|
-
// Optional so a host
|
|
3574
|
-
// useUserPhoto() reports canEdit false when it is absent.
|
|
3612
|
+
// Optional, and so is every member: a host publishes only what it can do.
|
|
3575
3613
|
account: {
|
|
3576
3614
|
description:
|
|
3577
|
-
"Optional self-service account actions
|
|
3578
|
-
"{ setPhoto(file) -> Promise<{ photoUrl }>, removePhoto() -> Promise<{ photoUrl: null }> }
|
|
3579
|
-
"
|
|
3580
|
-
"widget. setPhoto takes a browser File on web and expo-file-system's File on native.
|
|
3581
|
-
"
|
|
3615
|
+
"Optional self-service account actions; every member is optional too. " +
|
|
3616
|
+
"{ setPhoto(file) -> Promise<{ photoUrl }>, removePhoto() -> Promise<{ photoUrl: null }> } back useUserPhoto() and are " +
|
|
3617
|
+
"present only while a user is signed in. The host performs the request AND updates its own session, so user.photoUrl " +
|
|
3618
|
+
"changes for every widget. setPhoto takes a browser File on web and expo-file-system's File on native. " +
|
|
3619
|
+
"anonymous backs useAnonymousAccount() and is present only where the workspace offers accounts without an email: " +
|
|
3620
|
+
"{ canCreate: boolean, create() -> Promise<{ created: true }> }. create() opens the HOST'S OWN dialog: " +
|
|
3621
|
+
"it asks the visitor to confirm, runs the captcha, calls the server, shows the login code and password once, and signs " +
|
|
3622
|
+
"the visitor in when they confirm they saved them. The pair lives only in that dialog and never reaches the slice, a " +
|
|
3623
|
+
"widget, an error or storage. " +
|
|
3624
|
+
"The whole slice is absent on the Builder canvas, in the preview panes and for a badge counter, where both hooks " +
|
|
3625
|
+
"degrade (canEdit: false, supported: false) rather than throwing.",
|
|
3582
3626
|
required: false,
|
|
3583
|
-
fields: { setPhoto: "function", removePhoto: "function" },
|
|
3627
|
+
fields: { setPhoto: "function", removePhoto: "function", anonymous: "object" },
|
|
3584
3628
|
},
|
|
3585
3629
|
workspace: {
|
|
3586
3630
|
description:
|
|
@@ -5808,7 +5852,19 @@ const CONTRACT = deepFreeze({
|
|
|
5808
5852
|
// new `ctx.filestore.signingRounds`. `useFileSignature` also returns
|
|
5809
5853
|
// `hintCode` and `nameRefusal`, and its `cancel()` resolves to what
|
|
5810
5854
|
// became of the order instead of always reporting "cancelled".
|
|
5811
|
-
|
|
5855
|
+
// 1.176.0: additive (sc-10934) — `useAnonymousAccount()` asks the host to
|
|
5856
|
+
// create an account with a generated login code and password and no
|
|
5857
|
+
// email, through the new optional `ctx.account.anonymous`
|
|
5858
|
+
// (`{ canCreate, create }`). The host shows the pair in its own dialog;
|
|
5859
|
+
// a widget never receives it. `account`'s photo actions are now
|
|
5860
|
+
// optional members, and `user.email` is documented as nullable: an
|
|
5861
|
+
// account without an email has none.
|
|
5862
|
+
// 1.177.0: additive (sc-10937) — `useFilestoreUpload().upload(file,
|
|
5863
|
+
// { recordId })` names the record per call, so a widget can create a row
|
|
5864
|
+
// and attach to it in one handler. A FILE column's file goes in its
|
|
5865
|
+
// row's record space; useCamera / useImageEditor point at
|
|
5866
|
+
// useFilestoreUpload, since widget code cannot reach ctx.assets.
|
|
5867
|
+
version: "1.177.0",
|
|
5812
5868
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5813
5869
|
hooks: HOOKS,
|
|
5814
5870
|
primitives: PRIMITIVES,
|
package/dist/hooks.js
CHANGED
|
@@ -391,6 +391,80 @@ export function useUserPhoto() {
|
|
|
391
391
|
return { url, canEdit, upload, remove, uploading, error };
|
|
392
392
|
}
|
|
393
393
|
|
|
394
|
+
const ANONYMOUS_ACCOUNT_UNSUPPORTED =
|
|
395
|
+
"Creating an account without an email is not available here";
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Offer an account that needs no email address. The server generates a login
|
|
399
|
+
* code and a password; the person signs in later with that pair. Returns
|
|
400
|
+
* `{ supported, canCreate, creating, error, create }`.
|
|
401
|
+
*
|
|
402
|
+
* A widget only asks. `create()` takes no arguments and opens the app's own dialog
|
|
403
|
+
* (`ctx.account.anonymous`), which asks the visitor to confirm, runs the
|
|
404
|
+
* captcha, creates the account, shows the login code and password once and
|
|
405
|
+
* signs the visitor in when they confirm they saved them. The widget never
|
|
406
|
+
* receives the pair: `create()` resolves `{ created: true }`. It rejects with
|
|
407
|
+
* an Error carrying `code` — the server's refusal, `CAPTCHA_UNAVAILABLE`,
|
|
408
|
+
* `ALREADY_SIGNED_IN`, `CANCELLED` when the visitor closes the dialog, or
|
|
409
|
+
* `UNSUPPORTED`.
|
|
410
|
+
*
|
|
411
|
+
* Call `create()` from a user action (a button press), never on mount.
|
|
412
|
+
* `ctx.account.anonymous` is optional: where it is absent (the workspace has
|
|
413
|
+
* the feature off, the Builder canvas, a preview, a badge counter)
|
|
414
|
+
* `supported` is false and `create()` rejects instead of the hook throwing at
|
|
415
|
+
* render. `canCreate` is false while someone is signed in.
|
|
416
|
+
*/
|
|
417
|
+
export function useAnonymousAccount() {
|
|
418
|
+
const ctx = useWidgetContextOrThrow("useAnonymousAccount");
|
|
419
|
+
const member = ctx.account ? ctx.account.anonymous : null;
|
|
420
|
+
const supported = Boolean(member) && typeof member.create === "function";
|
|
421
|
+
|
|
422
|
+
const hostRef = useRef(null);
|
|
423
|
+
hostRef.current = supported ? member : null;
|
|
424
|
+
const inFlightRef = useRef(null);
|
|
425
|
+
const [creating, setCreating] = useState(false);
|
|
426
|
+
const [error, setError] = useState(null);
|
|
427
|
+
|
|
428
|
+
const create = useCallback(() => {
|
|
429
|
+
if (inFlightRef.current) return inFlightRef.current;
|
|
430
|
+
const host = hostRef.current;
|
|
431
|
+
if (!host) {
|
|
432
|
+
const err = Object.assign(new Error(ANONYMOUS_ACCOUNT_UNSUPPORTED), {
|
|
433
|
+
code: "UNSUPPORTED",
|
|
434
|
+
});
|
|
435
|
+
setError(err);
|
|
436
|
+
return Promise.reject(err);
|
|
437
|
+
}
|
|
438
|
+
setCreating(true);
|
|
439
|
+
setError(null);
|
|
440
|
+
const attempt = Promise.resolve()
|
|
441
|
+
.then(() => host.create())
|
|
442
|
+
.then(
|
|
443
|
+
() => {
|
|
444
|
+
inFlightRef.current = null;
|
|
445
|
+
setCreating(false);
|
|
446
|
+
return { created: true };
|
|
447
|
+
},
|
|
448
|
+
(err) => {
|
|
449
|
+
inFlightRef.current = null;
|
|
450
|
+
setCreating(false);
|
|
451
|
+
setError(err);
|
|
452
|
+
throw err;
|
|
453
|
+
},
|
|
454
|
+
);
|
|
455
|
+
inFlightRef.current = attempt;
|
|
456
|
+
return attempt;
|
|
457
|
+
}, []);
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
supported,
|
|
461
|
+
canCreate: supported && member.canCreate === true,
|
|
462
|
+
creating,
|
|
463
|
+
error,
|
|
464
|
+
create,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
394
468
|
/**
|
|
395
469
|
* Returns `true` when the host has sized this widget to fill a box of
|
|
396
470
|
* definite height — a widget in the section a fit-to-screen page stretches to
|
|
@@ -4626,7 +4700,10 @@ export function useFilestoreFile(fileId) {
|
|
|
4626
4700
|
* same way the read hooks do (tenant for PROJECT, app user for PERSONAL, the
|
|
4627
4701
|
* record for RECORD) and posts a multipart FormData to `ctx.filestore.files.upload`.
|
|
4628
4702
|
*
|
|
4629
|
-
* `upload(file, {
|
|
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
|
|
4630
4707
|
* Player and expo-file-system's `File` on the Expo export, as `<FilePicker>`,
|
|
4631
4708
|
* `useCamera()` and `useImageEditor()` all hand back. Never React Native's
|
|
4632
4709
|
* legacy `{ uri, name, type }` part: the native host streams the file off
|
|
@@ -4660,7 +4737,8 @@ export function useFilestoreUpload(options) {
|
|
|
4660
4737
|
folderId: defaultFolderId = null,
|
|
4661
4738
|
compress: defaultCompress = true,
|
|
4662
4739
|
} = options || {};
|
|
4663
|
-
const
|
|
4740
|
+
const isRecordSpace = String(spaceType || "project").toUpperCase() === "RECORD";
|
|
4741
|
+
const defaultOwnerId = _filestoreOwnerId(ctx, spaceType, recordId);
|
|
4664
4742
|
|
|
4665
4743
|
const [uploading, setUploading] = useState(false);
|
|
4666
4744
|
const [error, setError] = useState(null);
|
|
@@ -4672,19 +4750,25 @@ export function useFilestoreUpload(options) {
|
|
|
4672
4750
|
const upload = useCallback(
|
|
4673
4751
|
async (file, overrides) => {
|
|
4674
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
|
+
}
|
|
4675
4765
|
if (!ownerId) {
|
|
4676
4766
|
const err = new Error("Sign in to upload");
|
|
4677
4767
|
setError(err);
|
|
4678
4768
|
throw err;
|
|
4679
4769
|
}
|
|
4680
|
-
const folderId =
|
|
4681
|
-
|
|
4682
|
-
? overrides.folderId
|
|
4683
|
-
: defaultFolderId;
|
|
4684
|
-
const compress =
|
|
4685
|
-
overrides && Object.prototype.hasOwnProperty.call(overrides, "compress")
|
|
4686
|
-
? overrides.compress
|
|
4687
|
-
: defaultCompress;
|
|
4770
|
+
const folderId = hasOverride("folderId") ? overrides.folderId : defaultFolderId;
|
|
4771
|
+
const compress = hasOverride("compress") ? overrides.compress : defaultCompress;
|
|
4688
4772
|
const form = new FormData();
|
|
4689
4773
|
form.append("space_type", String(spaceType || "project").toUpperCase());
|
|
4690
4774
|
form.append("owner_id", ownerId);
|
|
@@ -4726,7 +4810,7 @@ export function useFilestoreUpload(options) {
|
|
|
4726
4810
|
throw err;
|
|
4727
4811
|
}
|
|
4728
4812
|
},
|
|
4729
|
-
[
|
|
4813
|
+
[defaultOwnerId, isRecordSpace, defaultFolderId, defaultCompress, spaceType],
|
|
4730
4814
|
);
|
|
4731
4815
|
|
|
4732
4816
|
return { upload, uploading, error, lastUploaded };
|
package/dist/index.d.ts
CHANGED
|
@@ -744,14 +744,18 @@ export interface WidgetContext<TProps = unknown> {
|
|
|
744
744
|
groupIds: string[];
|
|
745
745
|
};
|
|
746
746
|
/**
|
|
747
|
-
* Optional self-service account actions
|
|
748
|
-
*
|
|
749
|
-
*
|
|
750
|
-
*
|
|
747
|
+
* Optional self-service account actions; every member is optional too.
|
|
748
|
+
* `setPhoto` / `removePhoto` back `useUserPhoto()` and are present only
|
|
749
|
+
* while a user is signed in: the host performs the request and updates its
|
|
750
|
+
* own session, so `user.photoUrl` changes for every widget. `anonymous`
|
|
751
|
+
* backs `useAnonymousAccount()` and is present only where the workspace
|
|
752
|
+
* offers accounts without an email. The whole slice is absent on the
|
|
753
|
+
* Builder canvas, in the preview panes and for a badge counter.
|
|
751
754
|
*/
|
|
752
755
|
account?: {
|
|
753
|
-
setPhoto(file: UserPhotoFile): Promise<{ photoUrl: string | null }>;
|
|
754
|
-
removePhoto(): Promise<{ photoUrl: null }>;
|
|
756
|
+
setPhoto?(file: UserPhotoFile): Promise<{ photoUrl: string | null }>;
|
|
757
|
+
removePhoto?(): Promise<{ photoUrl: null }>;
|
|
758
|
+
anonymous?: AnonymousAccountHost;
|
|
755
759
|
};
|
|
756
760
|
workspace: {
|
|
757
761
|
id: string;
|
|
@@ -1947,6 +1951,59 @@ export interface UserPhotoApi {
|
|
|
1947
1951
|
*/
|
|
1948
1952
|
export function useUserPhoto(): UserPhotoApi;
|
|
1949
1953
|
|
|
1954
|
+
/** A `create()` failure. `code` names the refusal. */
|
|
1955
|
+
export type AnonymousAccountError = Error & {
|
|
1956
|
+
/**
|
|
1957
|
+
* The server's refusal (`ANONYMOUS_ACCOUNT_LIMIT_REACHED`, `RATE_LIMITED`,
|
|
1958
|
+
* `CAPTCHA_FAILED`, …), or `CAPTCHA_UNAVAILABLE`
|
|
1959
|
+
* (the captcha could not run), `ALREADY_SIGNED_IN`, `CANCELLED` (the
|
|
1960
|
+
* visitor closed the dialog) or `UNSUPPORTED`. Absent when the failure has
|
|
1961
|
+
* no code.
|
|
1962
|
+
*/
|
|
1963
|
+
code?: string;
|
|
1964
|
+
/** The HTTP status of a server refusal; 0 when there was no response. */
|
|
1965
|
+
status?: number;
|
|
1966
|
+
};
|
|
1967
|
+
|
|
1968
|
+
/** `WidgetContext.account.anonymous`: what a host provides for `useAnonymousAccount()`. */
|
|
1969
|
+
export interface AnonymousAccountHost {
|
|
1970
|
+
/** True only while nobody is signed in and no two-factor step is pending. */
|
|
1971
|
+
canCreate: boolean;
|
|
1972
|
+
/**
|
|
1973
|
+
* Opens the host's own dialog, which confirms, creates the account, shows
|
|
1974
|
+
* the login code and password once and signs the visitor in. The pair
|
|
1975
|
+
* never leaves that dialog.
|
|
1976
|
+
*/
|
|
1977
|
+
create(): Promise<{ created: true }>;
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
export interface AnonymousAccountApi {
|
|
1981
|
+
/** False where the host does not offer accounts without an email. */
|
|
1982
|
+
supported: boolean;
|
|
1983
|
+
/** True when `supported` and nobody is signed in. */
|
|
1984
|
+
canCreate: boolean;
|
|
1985
|
+
/** True while a `create()` call is in flight: the app's dialog is open. */
|
|
1986
|
+
creating: boolean;
|
|
1987
|
+
/** The last `create()` failure; cleared by the next attempt. */
|
|
1988
|
+
error: AnonymousAccountError | null;
|
|
1989
|
+
/**
|
|
1990
|
+
* Ask the app to create the account. Call it from a user action. The app
|
|
1991
|
+
* shows the login code and password itself; the widget never receives
|
|
1992
|
+
* them. Rejects with an `AnonymousAccountError`.
|
|
1993
|
+
*/
|
|
1994
|
+
create(): Promise<{ created: true }>;
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
/**
|
|
1998
|
+
* Offer an account that needs no email address: the server generates a
|
|
1999
|
+
* login code and a password, and the person signs in later with that pair.
|
|
2000
|
+
* The app confirms, shows the pair once and signs the visitor in, all in its
|
|
2001
|
+
* own dialog. Never throws at render: where the host does not offer it (the
|
|
2002
|
+
* workspace has it off, the Builder canvas, a preview, a badge counter)
|
|
2003
|
+
* `supported` is false and `create()` rejects with `code: "UNSUPPORTED"`.
|
|
2004
|
+
*/
|
|
2005
|
+
export function useAnonymousAccount(): AnonymousAccountApi;
|
|
2006
|
+
|
|
1950
2007
|
/**
|
|
1951
2008
|
* REQ-LAY-08 — returns `true` when the host has sized this widget to fill a
|
|
1952
2009
|
* box of definite height (a widget in the section a fit-to-screen page
|
|
@@ -2395,7 +2452,7 @@ export interface CameraAsset {
|
|
|
2395
2452
|
/**
|
|
2396
2453
|
* Ready-to-upload part — a Blob on both hosts: a `File` on web,
|
|
2397
2454
|
* expo-file-system's `File` on native. Expo's fetch rejects any other shape.
|
|
2398
|
-
*
|
|
2455
|
+
* Pass it to `upload(file)` from `useFilestoreUpload`.
|
|
2399
2456
|
*/
|
|
2400
2457
|
file: unknown;
|
|
2401
2458
|
}
|
package/dist/index.js
CHANGED
package/dist/index.native.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.221.0",
|
|
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
|
@@ -938,6 +938,38 @@ const HOOKS = [
|
|
|
938
938
|
requiredContextSlice: ["user", "account.setPhoto", "account.removePhoto"],
|
|
939
939
|
scopes: null,
|
|
940
940
|
},
|
|
941
|
+
{
|
|
942
|
+
name: "useAnonymousAccount",
|
|
943
|
+
signature: "useAnonymousAccount()",
|
|
944
|
+
description:
|
|
945
|
+
"Offer an account that needs NO email address: the server generates a login code and a password, and the person signs in " +
|
|
946
|
+
"later with that pair on the app's sign-in screen. For apps where a visitor must be able to act without identifying " +
|
|
947
|
+
"themselves and still come back (a whistleblower report). The widget ONLY ASKS: create() takes NO arguments and opens the APP'S " +
|
|
948
|
+
"OWN dialog, which asks the visitor to confirm, runs the captcha, creates the account, shows the login code and password " +
|
|
949
|
+
"ONCE with the warning that a lost password cannot be recovered, and signs the visitor in when they confirm they saved " +
|
|
950
|
+
"them (or opens the sign-in screen's two-factor step when the workspace requires one). The widget NEVER receives the " +
|
|
951
|
+
"login code or the password: create() resolves { created: true } and nothing else. Do NOT build your own confirmation, " +
|
|
952
|
+
"captcha notice or credentials screen, and never call an endpoint yourself. Call create() from a user action (a button's " +
|
|
953
|
+
"onPress), never on mount or in an effect. It rejects with an Error carrying code: the server's refusal " +
|
|
954
|
+
"(ANONYMOUS_ACCOUNT_LIMIT_REACHED, RATE_LIMITED, CAPTCHA_FAILED, …), CAPTCHA_UNAVAILABLE when the " +
|
|
955
|
+
"captcha could not run (there is no fallback), ALREADY_SIGNED_IN, CANCELLED when the visitor closes the dialog, or " +
|
|
956
|
+
"UNSUPPORTED. The dialog already shows a failure and lets the visitor retry, so a rejection means they gave up: treat " +
|
|
957
|
+
"CANCELLED as no error. supported is false — and create() rejects with UNSUPPORTED instead of the hook throwing at " +
|
|
958
|
+
"render — where the workspace has not turned anonymous accounts on, on the Builder canvas, in a preview pane, in a draft " +
|
|
959
|
+
"preview and for a badge counter: render a visible 'not available' state then. canCreate is false while someone is " +
|
|
960
|
+
"signed in: hide the button then. After create() resolves the visitor is signed in and the page re-renders, so read " +
|
|
961
|
+
"useUser() for the new state. Needs no scope. Identical on web (Player) and the Expo export.",
|
|
962
|
+
returnShape: {
|
|
963
|
+
supported: "boolean // the host offers anonymous accounts here",
|
|
964
|
+
canCreate: "boolean // supported AND nobody is signed in",
|
|
965
|
+
creating: "boolean // a create() call is in flight: the app's dialog is open",
|
|
966
|
+
error: "(Error & { code?: string, status?: number }) | null // the last create() failure; cleared by the next attempt",
|
|
967
|
+
create:
|
|
968
|
+
"() => Promise<{ created: true }> // takes no arguments; opens the app's dialog; never returns the login code or password",
|
|
969
|
+
},
|
|
970
|
+
requiredContextSlice: ["account.anonymous"],
|
|
971
|
+
scopes: null,
|
|
972
|
+
},
|
|
941
973
|
{
|
|
942
974
|
name: "useFill",
|
|
943
975
|
signature: "useFill()",
|
|
@@ -1337,7 +1369,7 @@ const HOOKS = [
|
|
|
1337
1369
|
"from the host context, builds a multipart FormData with the " +
|
|
1338
1370
|
"snake_case fields the backend reads verbatim (`space_type`, " +
|
|
1339
1371
|
"`owner_id`, `folder_id`, plus the binary `file`), and POSTs through " +
|
|
1340
|
-
"ctx.filestore.files.upload. `upload(file, { folderId?, compress?, fileName? })` " +
|
|
1372
|
+
"ctx.filestore.files.upload. `upload(file, { folderId?, recordId?, compress?, fileName? })` " +
|
|
1341
1373
|
"resolves to the created file row or throws the wire error. `file` must be a BLOB — a browser File on web, " +
|
|
1342
1374
|
"expo-file-system's File on native (what FilePicker / useCamera / useImageEditor hand back); Expo's fetch rejects " +
|
|
1343
1375
|
"React Native's legacy { uri, name, type } part with \"Unsupported FormDataPart implementation\". The stored name " +
|
|
@@ -1355,13 +1387,20 @@ const HOOKS = [
|
|
|
1355
1387
|
"ORIGINAL matters (a document archive, a photo the user re-downloads, " +
|
|
1356
1388
|
"anything with an exact-bytes requirement). " +
|
|
1357
1389
|
"ALWAYS pass spaceType explicitly — omitting it falls back to " +
|
|
1358
|
-
"'project', which is unreadable by a logged-out visitor.
|
|
1359
|
-
"
|
|
1360
|
-
"
|
|
1361
|
-
"
|
|
1362
|
-
"
|
|
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.",
|
|
1363
1402
|
returnShape: {
|
|
1364
|
-
upload: "(file, { folderId?, compress?, fileName? }) => Promise<FilestoreFile>",
|
|
1403
|
+
upload: "(file, { folderId?, recordId?, compress?, fileName? }) => Promise<FilestoreFile>",
|
|
1365
1404
|
uploading: "boolean",
|
|
1366
1405
|
error: "Error | null",
|
|
1367
1406
|
lastUploaded: "FilestoreFile | null",
|
|
@@ -2786,8 +2825,8 @@ const HOOKS = [
|
|
|
2786
2825
|
"common case and is deliberately NOT an error, so no try/catch is needed on the happy path. They reject with a " +
|
|
2787
2826
|
"CameraError whose .code is one of PERMISSION_DENIED | UNSUPPORTED | INTERNAL. `asset.file` is already the right " +
|
|
2788
2827
|
"upload part for the host — a BLOB on both (a File on web, expo-file-system's File on native), because Expo's fetch " +
|
|
2789
|
-
"implements the web FormData spec and rejects React Native's legacy { uri, name, type } part outright:
|
|
2790
|
-
"
|
|
2828
|
+
"implements the web FormData spec and rejects React Native's legacy { uri, name, type } part outright: pass it " +
|
|
2829
|
+
"straight to upload(asset.file) from useFilestoreUpload — one code path on both platforms. reset() clears the asset and releases it. Check " +
|
|
2791
2830
|
"`supported` before rendering a camera button. options: { allowsEditing, quality } are HINTS the host honours where it " +
|
|
2792
2831
|
"can — the Expo export applies both, the web camera applies quality only, so never depend on a cropped result. " +
|
|
2793
2832
|
"Behaviour is otherwise identical on web (a getUserMedia preview on EVERY host, phones included; a file input " +
|
|
@@ -2853,7 +2892,7 @@ const HOOKS = [
|
|
|
2853
2892
|
"Editing is IMPERATIVE — call edit() from an event handler, never during render. " +
|
|
2854
2893
|
"edit(uri, actions, options?) applies `actions` IN ORDER and resolves the SAME normalised asset shape useCamera() " +
|
|
2855
2894
|
"yields — { uri, name, mimeType, width, height, size, base64?, file } — so capture -> edit -> upload is ONE code path: " +
|
|
2856
|
-
"
|
|
2895
|
+
"pass result.file to upload() from useFilestoreUpload. " +
|
|
2857
2896
|
"Each action entry carries exactly one of { resize: { width?, height? } } (aspect preserved when only one is given), " +
|
|
2858
2897
|
"{ crop: { originX, originY, width, height } }, { rotate: degrees } (positive is clockwise), or " +
|
|
2859
2898
|
"{ flip: \"horizontal\" | \"vertical\" }. A crop rect must lie INSIDE the surface the earlier actions produced, and " +
|
|
@@ -2990,7 +3029,7 @@ const PRIMITIVES = [
|
|
|
2990
3029
|
},
|
|
2991
3030
|
// sc-1378 — `<FilePicker accept onPick>` lets a widget surface a native
|
|
2992
3031
|
// file-picker on the web Player so end users can upload files to the
|
|
2993
|
-
// backend (paired with useFilestoreUpload
|
|
3032
|
+
// backend (paired with useFilestoreUpload). Web wraps
|
|
2994
3033
|
// a hidden DOM `<input type="file">`; native renders a disabled trigger
|
|
2995
3034
|
// and exposes `FilePicker.isSupported = false` until a vetted Expo
|
|
2996
3035
|
// picker pin lands.
|
|
@@ -3561,26 +3600,31 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
3561
3600
|
},
|
|
3562
3601
|
user: {
|
|
3563
3602
|
description:
|
|
3564
|
-
"Signed-in user, host-built context object (camelCase: { id, email, displayName, photoUrl, roles, groupIds }) — NOT a wire payload, so it is the one camelCase island among the hooks. photoUrl is the user's own profile photo as an absolute URL, or null. Not a data-client.",
|
|
3603
|
+
"Signed-in user, host-built context object (camelCase: { id, email, displayName, photoUrl, roles, groupIds }) — NOT a wire payload, so it is the one camelCase island among the hooks. photoUrl is the user's own profile photo as an absolute URL, or null. email is null for an account created without one. Not a data-client.",
|
|
3565
3604
|
required: true,
|
|
3566
3605
|
fields: {
|
|
3567
3606
|
id: "string",
|
|
3568
|
-
email: "string",
|
|
3607
|
+
email: "string | null",
|
|
3569
3608
|
displayName: "string",
|
|
3570
3609
|
photoUrl: "string | null",
|
|
3571
3610
|
},
|
|
3572
3611
|
},
|
|
3573
|
-
// Optional so a host
|
|
3574
|
-
// useUserPhoto() reports canEdit false when it is absent.
|
|
3612
|
+
// Optional, and so is every member: a host publishes only what it can do.
|
|
3575
3613
|
account: {
|
|
3576
3614
|
description:
|
|
3577
|
-
"Optional self-service account actions
|
|
3578
|
-
"{ setPhoto(file) -> Promise<{ photoUrl }>, removePhoto() -> Promise<{ photoUrl: null }> }
|
|
3579
|
-
"
|
|
3580
|
-
"widget. setPhoto takes a browser File on web and expo-file-system's File on native.
|
|
3581
|
-
"
|
|
3615
|
+
"Optional self-service account actions; every member is optional too. " +
|
|
3616
|
+
"{ setPhoto(file) -> Promise<{ photoUrl }>, removePhoto() -> Promise<{ photoUrl: null }> } back useUserPhoto() and are " +
|
|
3617
|
+
"present only while a user is signed in. The host performs the request AND updates its own session, so user.photoUrl " +
|
|
3618
|
+
"changes for every widget. setPhoto takes a browser File on web and expo-file-system's File on native. " +
|
|
3619
|
+
"anonymous backs useAnonymousAccount() and is present only where the workspace offers accounts without an email: " +
|
|
3620
|
+
"{ canCreate: boolean, create() -> Promise<{ created: true }> }. create() opens the HOST'S OWN dialog: " +
|
|
3621
|
+
"it asks the visitor to confirm, runs the captcha, calls the server, shows the login code and password once, and signs " +
|
|
3622
|
+
"the visitor in when they confirm they saved them. The pair lives only in that dialog and never reaches the slice, a " +
|
|
3623
|
+
"widget, an error or storage. " +
|
|
3624
|
+
"The whole slice is absent on the Builder canvas, in the preview panes and for a badge counter, where both hooks " +
|
|
3625
|
+
"degrade (canEdit: false, supported: false) rather than throwing.",
|
|
3582
3626
|
required: false,
|
|
3583
|
-
fields: { setPhoto: "function", removePhoto: "function" },
|
|
3627
|
+
fields: { setPhoto: "function", removePhoto: "function", anonymous: "object" },
|
|
3584
3628
|
},
|
|
3585
3629
|
workspace: {
|
|
3586
3630
|
description:
|
|
@@ -5808,7 +5852,19 @@ const CONTRACT = deepFreeze({
|
|
|
5808
5852
|
// new `ctx.filestore.signingRounds`. `useFileSignature` also returns
|
|
5809
5853
|
// `hintCode` and `nameRefusal`, and its `cancel()` resolves to what
|
|
5810
5854
|
// became of the order instead of always reporting "cancelled".
|
|
5811
|
-
|
|
5855
|
+
// 1.176.0: additive (sc-10934) — `useAnonymousAccount()` asks the host to
|
|
5856
|
+
// create an account with a generated login code and password and no
|
|
5857
|
+
// email, through the new optional `ctx.account.anonymous`
|
|
5858
|
+
// (`{ canCreate, create }`). The host shows the pair in its own dialog;
|
|
5859
|
+
// a widget never receives it. `account`'s photo actions are now
|
|
5860
|
+
// optional members, and `user.email` is documented as nullable: an
|
|
5861
|
+
// account without an email has none.
|
|
5862
|
+
// 1.177.0: additive (sc-10937) — `useFilestoreUpload().upload(file,
|
|
5863
|
+
// { recordId })` names the record per call, so a widget can create a row
|
|
5864
|
+
// and attach to it in one handler. A FILE column's file goes in its
|
|
5865
|
+
// row's record space; useCamera / useImageEditor point at
|
|
5866
|
+
// useFilestoreUpload, since widget code cannot reach ctx.assets.
|
|
5867
|
+
version: "1.177.0",
|
|
5812
5868
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5813
5869
|
hooks: HOOKS,
|
|
5814
5870
|
primitives: PRIMITIVES,
|