@wix/himalaya-cli 0.1475.0 → 0.1476.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.
@@ -3,8 +3,9 @@
3
3
  **The rule every recipe follows:** a screen that loads or writes data shows a
4
4
  **loading state**, a **classified error state**, and a **recovery action where the
5
5
  user can fix the failure** — retry for an offline or a server failure, and for a failed
6
- write; Sign in when the user is signed out; nothing for a permission failure, an empty result
7
- or a field error, where the same request gives the same answer. This recipe is the shared
6
+ write; Sign in when the user is signed out; the site's join flow for a non-member when
7
+ available; nothing for a permission failure, an empty result or a field error, where the same
8
+ request gives the same answer. This recipe is the shared
8
9
  idiom; the other recipes link here for it. Design guidance for the states:
9
10
  `himi docs designing-himi-screens` §5 ("Loading states", "Error states").
10
11
 
@@ -22,7 +23,7 @@ more than half model error, and the reference apps (`apps/himi-owner-lite`,
22
23
  ## 1. One status field, a classified error, derived flags
23
24
 
24
25
  ```ts
25
- type ErrorKind = "" | "offline" | "server" | "signIn" | "permission";
26
+ type ErrorKind = "" | "offline" | "server" | "signIn" | "membership" | "permission";
26
27
 
27
28
  interface State {
28
29
  status: "loading" | "ready" | "error";
@@ -34,6 +35,7 @@ interface State {
34
35
  hideOfflineError: boolean; // one EmptyState per error class …
35
36
  hideServerError: boolean;
36
37
  hideSignInError: boolean;
38
+ hideMembershipError: boolean;
37
39
  hidePermissionError: boolean;
38
40
  hideRefreshError: boolean; // … and a Banner when a refresh fails over rows we kept
39
41
  }
@@ -50,6 +52,7 @@ function shape(s: Partial<State>): State {
50
52
  hideOfflineError: !(blank && errorKind === "offline"),
51
53
  hideServerError: !(blank && errorKind === "server"),
52
54
  hideSignInError: !(blank && errorKind === "signIn"),
55
+ hideMembershipError: !(blank && errorKind === "membership"),
53
56
  hidePermissionError: !(blank && errorKind === "permission"),
54
57
  // Only a failure a retry can fix gets the banner over kept rows.
55
58
  hideRefreshError: !(!blank && (errorKind === "offline" || errorKind === "server")),
@@ -66,31 +69,49 @@ const TRANSPORT = /NSURLErrorDomain|(?:UnknownHost|Connect|SocketTimeout|NoRoute
66
69
  // A pinned site with no member session: iOS `siteNotPermitted(requested: "account", …)`,
67
70
  // Android `site account is not the bound site …`.
68
71
  const SIGNED_OUT = /wixApi not enabled|session unavailable|PRINCIPAL_CHANGED|not signed in|siteNotPermitted\(requested: "account"|site account is not the bound site/i;
72
+ // The account is not a member of this site: iOS `NotSiteMember(metaSiteId: …)`,
73
+ // Android `not a member of site …`. NOT `WDE0027` — that is Wix Data's generic
74
+ // permission refusal, which a signed-in member and a visitor both get.
75
+ const NOT_MEMBER = /\bNotSiteMember\b|\bnot a member of site\b/i;
69
76
 
70
- function classifyError(e: unknown): ErrorKind {
77
+ function classifyError(e: unknown, authState: "signedIn" | "signedOut" | "unknown" = "unknown"): ErrorKind {
71
78
  const msg = String((e as Error)?.message ?? e ?? "");
72
- const code = String((e as { code?: unknown })?.code ?? "");
79
+ // Some bridges put the Wix code in the response body rather than Error.code.
80
+ const code = String((e as { code?: unknown })?.code ?? /"(?:code|error)"\s*:\s*"([^"]+)"/.exec(msg)?.[1] ?? "");
73
81
  const status = Number(STATUS.exec(msg)?.[1] ?? 0);
74
- if (status === 401) return "signIn"; // signed out (or the session expired)
75
- if (status === 403) return "permission"; // signed in, without access
76
- if (SIGNED_OUT.test(msg)) return "signIn";
82
+ if (code === "AccountSessionExpired" || code === "webwix_account_signed_out" || SIGNED_OUT.test(msg)) return "signIn";
83
+ if (code === "NotSiteMember" || NOT_MEMBER.test(msg)) return "membership";
84
+ // A 401 is a missing or expired session — unless the host knows the user is signed
85
+ // in, when it is a policy refusal. Never "server": a retry sends the same 401 back.
86
+ if (status === 401) return authState === "signedIn" ? "permission" : "signIn";
87
+ // A 403 is a missing permission — but a visitor the host knows is signed out gets
88
+ // `403 WDE0027` on member-only data too, and signing in is what fixes that.
89
+ if (status === 403) return authState === "signedOut" ? "signIn" : "permission";
77
90
  if (code === "offline_write_blocked") return "offline"; // a write refused before it left
78
91
  // A web call that never reached Wix arrives as
79
92
  // `HTTP 502: {"error":"webwix_direct_failed","detail":"Failed to fetch"}` — judge its detail.
80
- if (status && !msg.includes("webwix_direct_failed")) return "server";
81
- if (TRANSPORT.test(msg)) return "offline";
93
+ // The top-level 502 is the bridge wrapper, not the underlying transport result.
94
+ const detail = msg.includes("webwix_direct_failed")
95
+ ? /"detail"\s*:\s*"([^"]+)"/.exec(msg)?.[1] ?? msg
96
+ : msg;
97
+ if (status && detail === msg) return "server";
98
+ if (TRANSPORT.test(detail)) return "offline";
82
99
  return "server"; // anything else — a bug, a malformed row — is not the user's connection
83
100
  }
84
101
 
102
+ // App-specific host integration. If the host does not expose auth state here,
103
+ // keep "unknown": a 401 then shows Sign in, which is what a session that ended needs.
104
+ function currentAuthState(): "signedIn" | "signedOut" | "unknown" { return "unknown"; }
105
+
85
106
  async function load(s: State): Promise<State> {
86
107
  try {
87
108
  const items = (await fetchRows()).map(normalize);
88
109
  return shape({ status: "ready", items });
89
110
  } catch (e) {
90
- const errorKind = classifyError(e);
111
+ const errorKind = classifyError(e, currentAuthState()); // use host/session state when available
91
112
  // Keep the rows: a refresh that fails must not blank a screen that had content —
92
113
  // unless access is gone, when the rows must not stay on screen either.
93
- const lostAccess = errorKind === "signIn" || errorKind === "permission";
114
+ const lostAccess = errorKind === "signIn" || errorKind === "membership" || errorKind === "permission";
94
115
  return shape({ ...s, items: lostAccess ? [] : s.items, status: "error", errorKind });
95
116
  }
96
117
  }
@@ -140,9 +161,16 @@ only when the screen shows the user something different for it.
140
161
  "body": { "package": "himalaya", "type": "EmptyState",
141
162
  "props": { "title": "Sign in to view products",
142
163
  "subtitle": "Your session has ended.",
143
- "action": { "actionId": "tier5.products.signIn" }, // a worker action that dispatches himi.wix.signInDirect
164
+ "action": { "actionId": "tier5.products.openSignIn" },
144
165
  "actionLabel": "Sign in" } },
145
166
  "modifiers": { "hiddenPath": "$service:products.hideSignInError" } },
167
+ { "id": "error.membership",
168
+ "body": { "package": "himalaya", "type": "EmptyState",
169
+ "props": { "title": "Join this site to view products",
170
+ "subtitle": "Use this site's join flow, or ask the owner for access.",
171
+ "action": { "actionId": "tier5.products.openJoin" },
172
+ "actionLabel": "Join this site" } },
173
+ "modifiers": { "hiddenPath": "$service:products.hideMembershipError" } },
146
174
  { "id": "error.permission", // no action: retrying gives the same answer
147
175
  "body": { "package": "himalaya", "type": "EmptyState",
148
176
  "props": { "title": "You need permission to view this screen",
@@ -164,10 +192,21 @@ only when the screen shows the user something different for it.
164
192
  the binding reads without negation.
165
193
  - Ready state is simply the data itself (the list rows, the detail fields) — it
166
194
  needs no dedicated marker.
167
- - **Sign in is a worker action.** A renderer cannot dispatch `himi.wix.*`, so the
168
- sign-in `EmptyState` calls `tier5.products.signIn`, which the worker defines:
169
- it dispatches `himi.wix.signInDirect` and then reloads
170
- (`himi docs wix-login-integration`).
195
+ - **Sign in uses a real auth flow.** A renderer cannot dispatch `himi.wix.*`, so
196
+ the sign-in `EmptyState` calls an app action that opens its configured auth gate
197
+ or sign-in screen, then reloads after authentication. Do not dispatch
198
+ `himi.wix.signInDirect` from this button: it requires the email and password
199
+ collected by a credential form (`himi docs wix-login-integration`).
200
+ - **Membership is not sign-in.** `NotSiteMember` (Android: `not a member of
201
+ site`) means the account must join this site. Route it to the membership state;
202
+ wire its action to the app's join/registration UI, or omit the action when no
203
+ such flow exists. A `403 WDE0027` is NOT membership: Wix Data returns it to a
204
+ signed-in member without access too, so it stays a permission error.
205
+ - **A 403 is a permission error** — the user lacks access and signing in again
206
+ changes nothing — unless the host's auth state says the user is signed out: a
207
+ visitor gets `403 WDE0027` on member-only data, and signing in fixes it. A 401 is
208
+ the session that ended — unless the host says the user is signed in, when it is a
209
+ permission refusal.
171
210
  - **Loading is a `Skeleton` shaped like the loaded content** when you know the
172
211
  layout — the same rows at about the same size. `StatefulSection` or
173
212
  `ActivityIndicator` are fine where they fit better. A plain `"Loading…"` `Text`
@@ -194,7 +233,7 @@ only when the screen shows the user something different for it.
194
233
  | loading | initial state (`status: "loading"`) | content-shaped skeleton; NO retry |
195
234
  | error, nothing on screen | `catch` in the first load | the class's `EmptyState`: user-facing copy + a recovery action where one exists |
196
235
  | error, rows on screen | `catch` in a refresh (offline or server) | rows stay; a `Banner` with **Try again** above them |
197
- | sign-in or permission | `catch` in any load | rows go; the class's `EmptyState` (Sign in, or no action) |
236
+ | sign-in, membership or permission | `catch` in any load | rows go; the class's `EmptyState` (auth flow, join flow, or no action) |
198
237
  | empty | derived: ready + 0 rows | say what *would* fill it, and how |
199
238
  | ready | successful fetch | the content |
200
239
 
@@ -227,9 +266,10 @@ only when the screen shows the user something different for it.
227
266
  (`dev/net-mocks.json`, CMS seed rows). `himi render <screenId> --no-mocks` shows
228
267
  the initial frame — the loading state — and
229
268
  `--state '{"products":{"status":"error","errorKind":"offline","hideOfflineError":false,…}}'`
230
- forces an error class without the network. A `dev/net-mocks.json` rule that
231
- answers 401, 403 or 500 previews the sign-in, permission and server classes
232
- through the real `classifyError`.
269
+ forces an error class without the network. A `dev/net-mocks.json` rule reaches
270
+ the worker only as `Wix <status> for …` — the hermetic crawl drops the response
271
+ body — so a 401, 403 or 500 rule previews the sign-in, permission and server
272
+ classes. Preview the membership class with `--state`.
233
273
 
234
274
  ## See also
235
275
 
@@ -66,7 +66,7 @@ not say why there are no rows.
66
66
  | Results | Matching rows, result count, and the available sort/filter controls. |
67
67
  | No matches | An `EmptyState` explaining that the current query or filters found nothing; offer a way to adjust them, and Clear only where the field is controlled (the native slot) — the inline `SearchField` is uncontrolled, so Clear there empties the query but not the field. |
68
68
  | Empty catalog | Explain that the catalog itself has no items yet, independent of the query; provide a useful browse/back action. |
69
- | Error | Classified as in `himi docs recipe-four-state`: an icon-less `EmptyState` per class with user-facing copy, Try again only for offline and server, Sign in for sign-in, no action for permission. Retain the query and controls, and keep it distinct from no matches. |
69
+ | Error | Classified as in `himi docs recipe-four-state`: an icon-less `EmptyState` per class with user-facing copy, Try again only for offline and server, the app's auth flow for sign-in, the site's join flow for `NotSiteMember`, and no action for permission. Retain the query and controls, and keep it distinct from no matches. |
70
70
 
71
71
  ### Native slot and old-client fallback
72
72
 
@@ -172,9 +172,9 @@ interface PostsState {
172
172
  query: string;
173
173
  hideNoPosts: boolean; // the EmptyState for "loaded, and there is nothing at all"
174
174
  hideNoMatches: boolean; // the EmptyState for "there are posts, none match the query"
175
- errorKind: "" | "offline" | "server" | "signIn" | "permission"; // classifyError(e)
175
+ errorKind: "" | "offline" | "server" | "signIn" | "membership" | "permission"; // classifyError(e, currentAuthState())
176
176
  hideLoading: boolean; hideOfflineError: boolean; hideServerError: boolean;
177
- hideSignInError: boolean; hidePermissionError: boolean;
177
+ hideSignInError: boolean; hideMembershipError: boolean; hidePermissionError: boolean;
178
178
  hideRefreshError: boolean; // hiddenPath flags, as in recipe-four-state
179
179
  }
180
180
 
@@ -219,7 +219,7 @@ clear: (s) => { currentQuery = ""; return shape({ ...s, query: "", items: s.all
219
219
  ```
220
220
 
221
221
  - `load()` fetches once, fills `all`, and sets `items = visible(all, currentQuery)`.
222
- On a `signIn` or `permission` failure it clears **`all` as well as `items`** —
222
+ On a `signIn`, `membership` or `permission` failure it clears **`all` as well as `items`** —
223
223
  `search`, `clear` and `retry` rebuild rows from `all`, so rows left there come
224
224
  back on the next keystroke.
225
225
  - The shipped Blog v3 `WixPost` type includes `categoryIds` and both Wix-image and
@@ -261,8 +261,11 @@ A searchable list loads data, so it carries the four states of
261
261
  while the first `load()` runs. The search field is usable at once.
262
262
  - **Error, nothing loaded** — one `EmptyState` per class `classifyError` can
263
263
  return, **no icon**, user-facing copy; offline and server get **Try again** →
264
- `tier5.posts.retry`, sign-in gets **Sign in**, permission gets no action. The
265
- `catch` sets `errorKind` from `classifyError(e)`; it never shows `e.message`.
264
+ `tier5.posts.retry`, sign-in opens the app's configured auth flow, `NotSiteMember`
265
+ opens the site's join flow when the app provides one, and permission gets no
266
+ action. The `catch` sets `errorKind` from `classifyError(e, currentAuthState())`
267
+ (both from `himi docs recipe-four-state`); it never
268
+ shows `e.message`.
266
269
  - **Retry keeps the query.** `retry` re-runs `load()` and then re-applies
267
270
  `currentQuery` — not `s.query`, which is the query from when the retry
268
271
  STARTED — so what the user typed during the fetch is not lost.
@@ -273,7 +276,8 @@ A searchable list loads data, so it carries the four states of
273
276
  retry: async (s) => {
274
277
  const next = await load(s); // classified on failure
275
278
  // Lost access clears the rows; any other failure keeps them for the Banner.
276
- const lostAccess = next.errorKind === "signIn" || next.errorKind === "permission";
279
+ const lostAccess = next.errorKind === "signIn" || next.errorKind === "membership"
280
+ || next.errorKind === "permission";
277
281
  const all = lostAccess ? [] : next.all;
278
282
  return shape({ ...next, all, query: currentQuery, items: visible(all, currentQuery) });
279
283
  },
@@ -112,10 +112,16 @@ The first arg after `state` is the action's payload object: the descriptor's `Ac
112
112
  | `Rating` | `onChange` | `{ value }` — the 1-based star index |
113
113
  | `ColorPicker` | `onChange` | `{ value }` — the hex string |
114
114
  | `DatePicker` | `onChange` | `{ value }` — unix **seconds**, string-encoded |
115
- | `OtpInput` | `onChange`, `onComplete` | `{ value }` — the assembled code (dispatched on React Native today; the other renderers show the boxes read-only) |
116
- | `ChipsInput` | `onRemove` | `{ value }` — the removed chip |
115
+ | `OtpInput` | `onChange`, `onComplete` | `{ value }` — the assembled code: `onChange` on every edit, `onComplete` once, on the edit that fills the last box |
116
+ | `ChipsInput` | `onAdd`, `onRemove` | `{ value }` — the new token (trimmed; committed by Return or a comma, and the field clears) / the removed chip. Append the token to `valuesPath` to show it |
117
117
  | `Button` / `ListItem` | `action` / `onTap` | exactly the descriptor's `params` (no injected value) |
118
118
 
119
+ For a controlled `OtpInput`, bind `valuePath` and have `onChange` store `payload.value` in that
120
+ bound field. An `onComplete` reducer can then clear it to empty the boxes. Service subscriptions
121
+ report **changes**: returning an already-empty bound field without first storing the user's edit
122
+ is a no-op and does not reset the local input. A completion-only input can omit `valuePath` and
123
+ submit the assembled code directly from `payload.value`.
124
+
119
125
  The payload rides in **`args`**. Some arms also mirror it into `params`; that is permitted only
120
126
  for string-encoded values, because `Action.params` is `map<string,string>` in the proto. Every
121
127
  host merges `{...params, ...args}` (args winning) before invoking the reducer, so a worker sees
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1475.0",
3
+ "version": "0.1476.0",
4
4
  "type": "module",
5
5
  "description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
6
6
  "keywords": [
@@ -71,5 +71,5 @@
71
71
  "artifactId": "himalaya-cli"
72
72
  }
73
73
  },
74
- "falconPackageHash": "224e56d2071ce0050cf65a861313895b2147862685f52d1f6b8abd95"
74
+ "falconPackageHash": "ca87854bfed096bc30428a79c429e7d947beb42046c8ac0d46dd613b"
75
75
  }