@wix/himalaya-cli 0.1484.0 → 0.1486.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/dist/cli.mjs +2 -1
- package/guides/dynamic-list-t5.descriptor.json +14 -0
- package/guides/dynamic-list.service.ts +48 -18
- package/guides/form-submit-t5.descriptor.json +1 -2
- package/guides/form-submit.service.ts +42 -22
- package/guides/qa-builds.md +8 -6
- package/guides/recipe-four-state.md +2 -2
- package/guides/search-list-t5.descriptor.json +14 -0
- package/guides/search-list.service.ts +48 -17
- package/package.json +2 -2
package/dist/cli.mjs
CHANGED
|
@@ -22144,6 +22144,7 @@ function resolveIdiom(tokens2, ctx = {}) {
|
|
|
22144
22144
|
var IDIOM_CONTEXTS, UNSAFE_KEYS;
|
|
22145
22145
|
var init_idiom = __esm({
|
|
22146
22146
|
"../../core/ts/src/idiom.ts"() {
|
|
22147
|
+
"use strict";
|
|
22147
22148
|
IDIOM_CONTEXTS = {
|
|
22148
22149
|
ios: { native: ["ios"] },
|
|
22149
22150
|
android: { native: ["material"] },
|
|
@@ -187803,7 +187804,7 @@ var COMMANDS = [
|
|
|
187803
187804
|
{ name: "patch", summary: 'change ONE item of a published app \u2014 a screen descriptor, a Tier-5 bundle, tokens, or the app-config baseline \u2014 from a RAW FILE, with no content package and no build. The low-level door: it does not lint your package, because there is no package here; `himi push --only <selector>` is the same edit with the full build + lint gate and is what to reach for when the package is in hand. Derives a new release from the app\'s current LIVE release, or from `--from-release <id>`. An explicit `--draft` or `--live` is REQUIRED \u2014 a patch has no default blast radius \u2014 and `--notes` is required with `--live` on a screen or app-config patch, because off-repo `gitSha` is "dev" and the ledger notes are then the only durable record of bytes that correspond to no commit. A branch does not merge: activating one derived from an older release reverts every item the patch does not move, so that case is refused until `--accept-regressions`. A bundle patch must repoint a screen (`--for-screen`) or the bytes land under a manifest key nothing reads. It cannot ADD a screen (screens are git-only, RFC 0012), never claims a slug \u2014 the app must already be published \u2014 and carries no attestation, source archive, assets, icon or minBinaryVersion, each named in the output.', required: "screen <id> --file <descriptor.json> [--bundle <sha256|none>] | bundle --file <bundle.js> (--for-screen <id>[,<id>] | --no-rebind) | tokens --file <tokens.json> | app-config --file <app-config.json> ; (--draft|--live) --app <a> [--from-release <id>] [--accept-regressions] [--label <n>] [--notes <t>] [--force] [--allow-binary-gap] [--target <t>] (--control-token)" },
|
|
187804
187805
|
{ name: "flags", summary: "read and change the app's OPERATIONAL flags (RFC 0013) \u2014 `list` prints what the DEVICE door serves, `set <key> --definition` creates or replaces one, `ramp <key>` moves its rollout share or kills it, `rm <key>` deletes it and echoes the definition back so the removal is reversible. Every write is followed by a read of the same door the device uses, and the answer is reported as `servedNow`: flags are stored in the app's git-committed flags.json, so a control plane with no checkout accepts a write it will never serve.", required: "list|set|ramp|rm [<key>] [--definition <json|@file>] [--percent 0-100] [--enabled true|false] [--app <a>] (--control-token)" },
|
|
187805
187806
|
{ name: "visibility", summary: "read (no value) or set whether the public registry (/v1/apps + the Himi Player picker) LISTS the app. `public` lists it for everyone; `unlisted` hides it from listings while direct reads by app id \u2014 deep links, previews \u2014 keep working. App from --app or the content dir. Setting it needs no republish.", required: "[public|unlisted] [--app <a>] [--content <dir>] (--control-token)" },
|
|
187806
|
-
{ name: "dist", summary: "build an INSTALLABLE QA binary (signed IPA / APK) on the Wix Job Service Mac pool and return an install link \u2014 the serve plane brokers the job, the pool builds it. WAITS for the build by default (~4 min) and prints the install link; `--no-wait` returns a jobId immediately instead. Omitting `--release` builds whatever is LIVE for the app. A named draft is refused unless `--allow-draft` makes that exceptional build explicit. `status <jobId>` / `watch <jobId>` / `list --app <a>` / `cancel <jobId>` inspect and stop builds (`watch` reports up front whether the job mints its own signing identity or borrows one, so a doomed iOS build is cancellable in seconds rather than after the archive). Off-repo, name a target \u2014 `--target wix` for a self-serve Wix login or `--target bo` for the internal employee path (the in-repo default is a local dev plane that isn't reachable off-repo); self-serve callers may build their OWN apps under a tighter budget (2 in flight / 5 per 24h), while a manage-only owner-role credential is refused. `--shell` picks the native host to sign into (branded-lite | owner-lite | brandy | base44 | player | demo-app) and defaults to the content package's himalaya.content.json `shell` (templates carry the one they were written for), else branded-lite. The per-app shells ADD the release's declared capabilities to the binary, while Himi Player and demo-app refuse a release declaring one they lack; `--plan` previews it without building; `brandy` is the site shell \u2014 the customer's own website as native-framed tabs. `--signing` defaults to `wix-managed` \u2014 the Wix Apple account mints a per-app signing identity for the build, so the caller provisions no Apple credentials of their own; `--signing byo --site <id>` reuses an existing customer ad-hoc profile and certificate for iOS; requires an owned site and a completed personal signing binding. `--android-signing` is the Android counterpart and a SEPARATE axis (a keystore, not an Apple account): `qa-keystore` (default \u2014 the repo's committed QA key, identical on every pool machine, so a build installs over the previous one), `wix-keystore` (the Wix key the pool machine carries; the build FAILS naming the paths searched if it carries none, rather than quietly signing with something else), or `debug` (the old per-machine key \u2014 QA must uninstall any previous build first). `--aab` builds an Android App Bundle (.aab) for the owner to upload to Google Play AND an APK from the same build \u2014 a download link for the .aab, an install link and QR for the APK; Android only; the bundle is signed with the Wix production key (`wix-keystore`, the upload key Branded apps use) \u2014 any other `--android-signing` is refused \u2014 and the APK with the committed QA key;
|
|
187807
|
+
{ name: "dist", summary: "build an INSTALLABLE QA binary (signed IPA / APK) on the Wix Job Service Mac pool and return an install link \u2014 the serve plane brokers the job, the pool builds it. WAITS for the build by default (~4 min) and prints the install link; `--no-wait` returns a jobId immediately instead. Omitting `--release` builds whatever is LIVE for the app. A named draft is refused unless `--allow-draft` makes that exceptional build explicit. `status <jobId>` / `watch <jobId>` / `list --app <a>` / `cancel <jobId>` inspect and stop builds (`watch` reports up front whether the job mints its own signing identity or borrows one, so a doomed iOS build is cancellable in seconds rather than after the archive). Off-repo, name a target \u2014 `--target wix` for a self-serve Wix login or `--target bo` for the internal employee path (the in-repo default is a local dev plane that isn't reachable off-repo); self-serve callers may build their OWN apps under a tighter budget (2 in flight / 5 per 24h), while a manage-only owner-role credential is refused. `--shell` picks the native host to sign into (branded-lite | owner-lite | brandy | base44 | player | demo-app) and defaults to the content package's himalaya.content.json `shell` (templates carry the one they were written for), else branded-lite. The per-app shells ADD the release's declared capabilities to the binary, while Himi Player and demo-app refuse a release declaring one they lack; `--plan` previews it without building; `brandy` is the site shell \u2014 the customer's own website as native-framed tabs. `--signing` defaults to `wix-managed` \u2014 the Wix Apple account mints a per-app signing identity for the build, so the caller provisions no Apple credentials of their own; `--signing byo --site <id>` reuses an existing customer ad-hoc profile and certificate for iOS; requires an owned site and a completed personal signing binding. `--android-signing` is the Android counterpart and a SEPARATE axis (a keystore, not an Apple account): `qa-keystore` (default \u2014 the repo's committed QA key, identical on every pool machine, so a build installs over the previous one), `wix-keystore` (the Wix key the pool machine carries; the build FAILS naming the paths searched if it carries none, rather than quietly signing with something else), or `debug` (the old per-machine key \u2014 QA must uninstall any previous build first). `--aab` builds an Android App Bundle (.aab) for the owner to upload to Google Play AND an APK from the same build \u2014 a download link for the .aab, an install link and QR for the APK; Android only; the bundle is signed with the Wix production key (`wix-keystore`, the upload key Branded apps use) \u2014 any other `--android-signing` is refused \u2014 and the APK with the committed QA key; a self-serve caller gets one only as the store build of its own Containers app (it must own the app's site, and `--aab` sends your Wix login); any other self-serve `--aab` gets 403 android_format_not_allowed. NOT the normal way to ship: screens, Tier-5 bundles, tokens and app-config all go over the air with `himi push` in seconds \u2014 a binary trip is only needed for a NATIVE delta (a new SDUI component, a new declared capability/entitlement, a new bundle id, or a minBinaryVersion floor bump). Refuses to build an app whose launcher icon is missing, a placeholder, or one a store would reject \u2014 the one thing a binary carries that no later OTA push can fix; `--force` builds anyway", required: "[--app <a>] [--platform ios,android] [--release <id>] [--allow-draft] [--target <t>] [--shell branded-lite|owner-lite|brandy|base44|player|demo-app] [--signing wix-managed|byo] [--site <id>] [--android-signing qa-keystore|wix-keystore|debug] [--aab] [--borrow-ios-id <bundleId>] [--no-wait] [--force] [--plan] | status <jobId> [--target <t>] | watch <jobId> [--target <t>] [--interval <ms>] | list --app <a> [--target <t>] | cancel <jobId> [--target <t>]" },
|
|
187807
187808
|
{ name: "testflight", summary: 'build, sign and upload an app to TestFlight (internal testers \u2014 no Beta App Review), then WAIT until App Store Connect finishes processing it. Sugar for `store upload --track testflight`. Prompts first: a build number is consumed permanently on a real Apple account and cannot be reused \u2014 `--yes` for CI, `--dry-run` to see the plan without touching anything. The wait is the real check, not a nicety: fastlane prints "Successfully uploaded" for a binary App Store Connect then rejects in processing, and a rejected binary leaves NO build record at all (the reason is emailed). Public App Store review is deliberately NOT automatable here \u2014 `--track appstore` is refused; use /submit-ios-app when you mean to go public', required: "<app> [--track testflight|internal] [--profile <name>] [--yes] [--dry-run] [--skip-build] [--no-wait] [--json]" },
|
|
187808
187809
|
{ name: "store", summary: "the App Store Connect group behind `testflight`. `preflight <app>` checks readiness offline (icon, entitlements, encryption flag, version, store.config ids, listing + review record); `upload <app>` is what `testflight` runs; `status <app>` reads live build + processing state. `--profile` selects WHICH App Store Connect account: an app built for a Wix customer ships under the CUSTOMER's Apple team (Apple guideline 4.2.6 forbids an app-generation service submitting on a client's behalf), declared per-app as ios.credentialProfile in store.config.yaml and resolved from ~/.himalaya/store-credentials/<name>.json \u2014 the .p8 itself never enters the repo. `iap draft|plan|apply|status|connect|screenshots` registers an app owner's digital products as in-app purchases (non-consumables, or consumable price tiers): draft reads the Wix catalog and writes the manifest, plan and status only read, apply is the one App Store mutation boundary, connect readies the Wix site to sell them (purchases app, In-App Purchase key, appleIAP provider, product mappings; --preview-tag routes only its Serve calls to a deploy preview), screenshots renders each product's purchase screen from the app (--content) as a bound iPhone shows it to a signed-in member and names the 1290\xD72796 PNG as the item's App Review screenshot, and none of them builds, signs or needs a checkout (`himi docs iap-products`)", required: "preflight <app> [--profile <n>] | upload <app> [--track <t>] [--profile <n>] [--yes] [--dry-run] | status <app> [--profile <n>] | iap draft --site <msid> --profile <n> --apple-app <id> --bundle <id> --base-territory <t> --out <f> [--territories <a,b>] [--locale <l>] [--mode per-item|tiers] [--merge <f>] [--report <f>] [--json] | iap plan --manifest <f> --profile <n> --out <f> [--json] | iap apply --plan <f> --profile <n> --out <f> [--yes] [--limit <n>] [--json] | iap status --manifest <f> --profile <n> [--out <f>] [--json] | iap connect --manifest <f> --profile <n> [--iap-key <AuthKey.p8> --iap-key-id <id>] [--content <dir>] [--preview-tag dp-<tag>] [--dry-run] [--yes] [--out <f>] [--json] | iap screenshots --manifest <f> --content <dir> [--profile <n>] [--out-dir <d>] [--only <id,\u2026>] [--force] [--dry-run] [--json]" },
|
|
187809
187810
|
{ name: "guide", summary: "print the Himalaya overview + headless dev loop + available topics (offline)", required: "[--json]" },
|
|
@@ -83,6 +83,20 @@
|
|
|
83
83
|
"hiddenPath": "$service:products.hideServerError"
|
|
84
84
|
}
|
|
85
85
|
},
|
|
86
|
+
{
|
|
87
|
+
"id": "error.membership",
|
|
88
|
+
"body": {
|
|
89
|
+
"package": "himalaya",
|
|
90
|
+
"type": "EmptyState",
|
|
91
|
+
"props": {
|
|
92
|
+
"title": "Join this site to view products",
|
|
93
|
+
"subtitle": "Ask the owner of this store to add you as a member."
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"modifiers": {
|
|
97
|
+
"hiddenPath": "$service:products.hideMembershipError"
|
|
98
|
+
}
|
|
99
|
+
},
|
|
86
100
|
{
|
|
87
101
|
"id": "error.permission",
|
|
88
102
|
"body": {
|
|
@@ -24,19 +24,21 @@ interface ProductRow {
|
|
|
24
24
|
initial: string; // derived presentation field (avatar letter)
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
// Every class the recipe's classifier returns …
|
|
28
|
+
type ErrorKind = "" | "offline" | "server" | "signIn" | "membership" | "permission";
|
|
29
|
+
// … and the ones this screen shows: it has no sign-in flow (see shownKind()).
|
|
30
|
+
type ShownKind = Exclude<ErrorKind, "" | "signIn">;
|
|
30
31
|
|
|
31
32
|
interface ProductsState {
|
|
32
33
|
status: "loading" | "ready" | "error";
|
|
33
34
|
items: ProductRow[]; // ← itemsPath binds here; MUST initialize as an array
|
|
34
|
-
errorKind:
|
|
35
|
+
errorKind: "" | ShownKind; // why the last load failed; "" unless status is "error"
|
|
35
36
|
// hiddenPath hides a node when its flag is TRUE.
|
|
36
37
|
hideLoading: boolean; // skeleton: first load, nothing on screen
|
|
37
38
|
notEmpty: boolean; // EmptyState: ready with zero rows
|
|
38
39
|
hideOfflineError: boolean; // one EmptyState per error class …
|
|
39
40
|
hideServerError: boolean;
|
|
41
|
+
hideMembershipError: boolean;
|
|
40
42
|
hidePermissionError: boolean;
|
|
41
43
|
hideRefreshError: boolean; // … and a Banner when a refresh fails over kept rows
|
|
42
44
|
selectedId: string; // row identity from onTap params — drive a detail view from it
|
|
@@ -62,7 +64,7 @@ function normalize(raw: StoreProduct): ProductRow {
|
|
|
62
64
|
function shape(s: Partial<ProductsState>): ProductsState {
|
|
63
65
|
const status = s.status ?? "ready";
|
|
64
66
|
const items = s.items ?? [];
|
|
65
|
-
const errorKind:
|
|
67
|
+
const errorKind: "" | ShownKind = status === "error" ? (s.errorKind || "server") : "";
|
|
66
68
|
const blank = items.length === 0; // nothing on screen to keep
|
|
67
69
|
return {
|
|
68
70
|
status,
|
|
@@ -72,14 +74,17 @@ function shape(s: Partial<ProductsState>): ProductsState {
|
|
|
72
74
|
notEmpty: !(status === "ready" && blank),
|
|
73
75
|
hideOfflineError: !(blank && errorKind === "offline"),
|
|
74
76
|
hideServerError: !(blank && errorKind === "server"),
|
|
77
|
+
hideMembershipError: !(blank && errorKind === "membership"),
|
|
75
78
|
hidePermissionError: !(blank && errorKind === "permission"),
|
|
76
79
|
hideRefreshError: !(!blank && (errorKind === "offline" || errorKind === "server")),
|
|
77
80
|
selectedId: s.selectedId ?? "",
|
|
78
81
|
};
|
|
79
82
|
}
|
|
80
83
|
|
|
84
|
+
// The classifier below is `himi docs recipe-four-state`'s, VERBATIM — a test holds
|
|
85
|
+
// the two byte-identical, so the rules an agent copies from here are the documented ones.
|
|
81
86
|
// The HTTP status rides in the message: `HTTP 503: …` on a device, `Wix 503 for …`
|
|
82
|
-
// under `himi test` / `himi render`.
|
|
87
|
+
// under `himi test` / `himi render`.
|
|
83
88
|
const STATUS = /\b(?:HTTP|Wix) (\d{3})\b/;
|
|
84
89
|
// No answer at all — what a dropped connection says on every platform.
|
|
85
90
|
const TRANSPORT = /NSURLErrorDomain|(?:UnknownHost|Connect|SocketTimeout|NoRouteToHost|SSLHandshake)Exception|Failed to connect|Unable to resolve host|Connection (?:refused|reset)|Network is unreachable|Software caused connection abort|ECONNRESET|ETIMEDOUT|EPIPE|Broken pipe|stream was reset|unexpected end of stream|timed out|^timeout$|Failed to fetch|NetworkError|^(?:TypeError: )?Load failed$|net::ERR_|^[a-z0-9-]+(?:\.[a-z0-9-]+)+$/i;
|
|
@@ -88,21 +93,45 @@ const TRANSPORT = /NSURLErrorDomain|(?:UnknownHost|Connect|SocketTimeout|NoRoute
|
|
|
88
93
|
// A pinned site with no member session: iOS `siteNotPermitted(requested: "account", …)`,
|
|
89
94
|
// Android `site account is not the bound site …`; the web host `HTTP 501: {"error":"webwix_owner_no_session"}`.
|
|
90
95
|
const SIGNED_OUT = /wixApi not enabled|session unavailable|PRINCIPAL_CHANGED|not signed in|siteNotPermitted\(requested: "account"|site account is not the bound site|webwix_owner_no_session/i;
|
|
96
|
+
// The account is not a member of this site: iOS `NotSiteMember(metaSiteId: …)`,
|
|
97
|
+
// Android `not a member of site …`. NOT `WDE0027` — that is Wix Data's generic
|
|
98
|
+
// permission refusal, which a signed-in member and a visitor both get.
|
|
99
|
+
const NOT_MEMBER = /\bNotSiteMember\b|\bnot a member of site\b/i;
|
|
91
100
|
|
|
92
|
-
|
|
93
|
-
function classifyError(e: unknown): ErrorKind {
|
|
101
|
+
function classifyError(e: unknown, authState: "signedIn" | "signedOut" | "unknown" = "unknown"): ErrorKind {
|
|
94
102
|
const msg = String((e as Error)?.message ?? e ?? "");
|
|
95
|
-
|
|
103
|
+
// Some bridges put the Wix code in the response body rather than Error.code.
|
|
104
|
+
const code = String((e as { code?: unknown })?.code ?? /"(?:code|error)"\s*:\s*"([^"]+)"/.exec(msg)?.[1] ?? "");
|
|
96
105
|
const status = Number(STATUS.exec(msg)?.[1] ?? 0);
|
|
97
|
-
if (
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
106
|
+
if (code === "AccountSessionExpired" || code === "webwix_account_signed_out" || SIGNED_OUT.test(msg)) return "signIn";
|
|
107
|
+
if (code === "NotSiteMember" || NOT_MEMBER.test(msg)) return "membership";
|
|
108
|
+
// A 401 is a missing or expired session — unless the host knows the user is signed
|
|
109
|
+
// in, when it is a policy refusal. Never "server": a retry sends the same 401 back.
|
|
110
|
+
if (status === 401) return authState === "signedIn" ? "permission" : "signIn";
|
|
111
|
+
// A 403 is a missing permission — but a visitor the host knows is signed out gets
|
|
112
|
+
// `403 WDE0027` on member-only data too, and signing in is what fixes that.
|
|
113
|
+
if (status === 403) return authState === "signedOut" ? "signIn" : "permission";
|
|
114
|
+
if (code === "offline_write_blocked") return "offline"; // a write refused before it left
|
|
101
115
|
// A web call that never reached Wix arrives as
|
|
102
116
|
// `HTTP 502: {"error":"webwix_direct_failed","detail":"Failed to fetch"}` — judge its detail.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
117
|
+
// The top-level 502 is the bridge wrapper, not the underlying transport result.
|
|
118
|
+
const detail = msg.includes("webwix_direct_failed")
|
|
119
|
+
? /"detail"\s*:\s*"([^"]+)"/.exec(msg)?.[1] ?? msg
|
|
120
|
+
: msg;
|
|
121
|
+
if (status && detail === msg) return "server";
|
|
122
|
+
if (TRANSPORT.test(detail)) return "offline";
|
|
123
|
+
return "server"; // anything else — a bug, a malformed row — is not the user's connection
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// App-specific host integration. If the host does not expose auth state here, keep
|
|
127
|
+
// "unknown": a 401 then reads as a session that ended.
|
|
128
|
+
function currentAuthState(): "signedIn" | "signedOut" | "unknown" { return "unknown"; }
|
|
129
|
+
|
|
130
|
+
// This screen has no sign-in flow, so a sign-in failure shows the permission state —
|
|
131
|
+
// every class it can show has a node in the descriptor.
|
|
132
|
+
function shownKind(e: unknown): ShownKind {
|
|
133
|
+
const kind = classifyError(e, currentAuthState());
|
|
134
|
+
return kind === "signIn" ? "permission" : kind || "server";
|
|
106
135
|
}
|
|
107
136
|
|
|
108
137
|
async function loadList(s: Partial<ProductsState>): Promise<ProductsState> {
|
|
@@ -111,9 +140,10 @@ async function loadList(s: Partial<ProductsState>): Promise<ProductsState> {
|
|
|
111
140
|
const items = (res.products ?? []).filter((p) => p?.id).map(normalize);
|
|
112
141
|
return shape({ status: "ready", items, selectedId: s.selectedId });
|
|
113
142
|
} catch (e) {
|
|
114
|
-
const errorKind =
|
|
143
|
+
const errorKind = shownKind(e);
|
|
115
144
|
// Keep the rows on a refresh failure; clear them when access is gone.
|
|
116
|
-
const
|
|
145
|
+
const lostAccess = errorKind === "membership" || errorKind === "permission";
|
|
146
|
+
const items = lostAccess ? [] : s.items ?? [];
|
|
117
147
|
return shape({ ...s, status: "error", items, errorKind });
|
|
118
148
|
}
|
|
119
149
|
}
|
|
@@ -25,7 +25,6 @@ interface PostFormState {
|
|
|
25
25
|
savedMessage: string; // "" unless saved → collapseWhenEmpty
|
|
26
26
|
hideSavedLink: boolean; // gates the "Open post" button via hiddenPath
|
|
27
27
|
savedScreenId: string; // deep-link target for the created entity
|
|
28
|
-
cannotSave: boolean; // true after a permission failure → the submit's disabledPath
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
function shape(s: Partial<PostFormState>): PostFormState {
|
|
@@ -39,22 +38,26 @@ function shape(s: Partial<PostFormState>): PostFormState {
|
|
|
39
38
|
savedMessage: s.savedMessage ?? "",
|
|
40
39
|
savedScreenId,
|
|
41
40
|
hideSavedLink: status !== "saved" || !savedScreenId,
|
|
42
|
-
cannotSave: s.cannotSave ?? false,
|
|
43
41
|
};
|
|
44
42
|
}
|
|
45
43
|
|
|
46
44
|
// A failed save is an ACTION error: one user-facing sentence per class, shown next to
|
|
47
45
|
// the submit button, which is the retry (`himi docs recipe-four-state`). Never e.message.
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
// Submit stays available after every class: the user retries once they reconnect,
|
|
47
|
+
// sign in, join the site or are granted access — without restarting the form.
|
|
48
|
+
type ErrorKind = "" | "offline" | "server" | "signIn" | "membership" | "permission";
|
|
49
|
+
const SAVE_FAILED: Record<Exclude<ErrorKind, "">, string> = {
|
|
50
50
|
offline: "You're offline, so nothing was saved. Try again when you reconnect.",
|
|
51
51
|
server: "Couldn't save your changes. Try again.",
|
|
52
52
|
signIn: "Your session has ended. Sign in, then save again.",
|
|
53
|
+
membership: "Join this site before saving.",
|
|
53
54
|
permission: "You don't have permission to save this.",
|
|
54
55
|
};
|
|
55
56
|
|
|
57
|
+
// The classifier below is `himi docs recipe-four-state`'s, VERBATIM — a test holds
|
|
58
|
+
// the two byte-identical, so the rules an agent copies from here are the documented ones.
|
|
56
59
|
// The HTTP status rides in the message: `HTTP 503: …` on a device, `Wix 503 for …`
|
|
57
|
-
// under `himi test` / `himi render`.
|
|
60
|
+
// under `himi test` / `himi render`.
|
|
58
61
|
const STATUS = /\b(?:HTTP|Wix) (\d{3})\b/;
|
|
59
62
|
// No answer at all — what a dropped connection says on every platform.
|
|
60
63
|
const TRANSPORT = /NSURLErrorDomain|(?:UnknownHost|Connect|SocketTimeout|NoRouteToHost|SSLHandshake)Exception|Failed to connect|Unable to resolve host|Connection (?:refused|reset)|Network is unreachable|Software caused connection abort|ECONNRESET|ETIMEDOUT|EPIPE|Broken pipe|stream was reset|unexpected end of stream|timed out|^timeout$|Failed to fetch|NetworkError|^(?:TypeError: )?Load failed$|net::ERR_|^[a-z0-9-]+(?:\.[a-z0-9-]+)+$/i;
|
|
@@ -63,37 +66,54 @@ const TRANSPORT = /NSURLErrorDomain|(?:UnknownHost|Connect|SocketTimeout|NoRoute
|
|
|
63
66
|
// A pinned site with no member session: iOS `siteNotPermitted(requested: "account", …)`,
|
|
64
67
|
// Android `site account is not the bound site …`; the web host `HTTP 501: {"error":"webwix_owner_no_session"}`.
|
|
65
68
|
const SIGNED_OUT = /wixApi not enabled|session unavailable|PRINCIPAL_CHANGED|not signed in|siteNotPermitted\(requested: "account"|site account is not the bound site|webwix_owner_no_session/i;
|
|
69
|
+
// The account is not a member of this site: iOS `NotSiteMember(metaSiteId: …)`,
|
|
70
|
+
// Android `not a member of site …`. NOT `WDE0027` — that is Wix Data's generic
|
|
71
|
+
// permission refusal, which a signed-in member and a visitor both get.
|
|
72
|
+
const NOT_MEMBER = /\bNotSiteMember\b|\bnot a member of site\b/i;
|
|
66
73
|
|
|
67
|
-
|
|
68
|
-
function classifyError(e: unknown): ErrorKind {
|
|
74
|
+
function classifyError(e: unknown, authState: "signedIn" | "signedOut" | "unknown" = "unknown"): ErrorKind {
|
|
69
75
|
const msg = String((e as Error)?.message ?? e ?? "");
|
|
70
|
-
|
|
76
|
+
// Some bridges put the Wix code in the response body rather than Error.code.
|
|
77
|
+
const code = String((e as { code?: unknown })?.code ?? /"(?:code|error)"\s*:\s*"([^"]+)"/.exec(msg)?.[1] ?? "");
|
|
71
78
|
const status = Number(STATUS.exec(msg)?.[1] ?? 0);
|
|
72
|
-
if (
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
79
|
+
if (code === "AccountSessionExpired" || code === "webwix_account_signed_out" || SIGNED_OUT.test(msg)) return "signIn";
|
|
80
|
+
if (code === "NotSiteMember" || NOT_MEMBER.test(msg)) return "membership";
|
|
81
|
+
// A 401 is a missing or expired session — unless the host knows the user is signed
|
|
82
|
+
// in, when it is a policy refusal. Never "server": a retry sends the same 401 back.
|
|
83
|
+
if (status === 401) return authState === "signedIn" ? "permission" : "signIn";
|
|
84
|
+
// A 403 is a missing permission — but a visitor the host knows is signed out gets
|
|
85
|
+
// `403 WDE0027` on member-only data too, and signing in is what fixes that.
|
|
86
|
+
if (status === 403) return authState === "signedOut" ? "signIn" : "permission";
|
|
87
|
+
if (code === "offline_write_blocked") return "offline"; // a write refused before it left
|
|
76
88
|
// A web call that never reached Wix arrives as
|
|
77
89
|
// `HTTP 502: {"error":"webwix_direct_failed","detail":"Failed to fetch"}` — judge its detail.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
90
|
+
// The top-level 502 is the bridge wrapper, not the underlying transport result.
|
|
91
|
+
const detail = msg.includes("webwix_direct_failed")
|
|
92
|
+
? /"detail"\s*:\s*"([^"]+)"/.exec(msg)?.[1] ?? msg
|
|
93
|
+
: msg;
|
|
94
|
+
if (status && detail === msg) return "server";
|
|
95
|
+
if (TRANSPORT.test(detail)) return "offline";
|
|
96
|
+
return "server"; // anything else — a bug, a malformed row — is not the user's connection
|
|
81
97
|
}
|
|
82
98
|
|
|
99
|
+
// App-specific host integration. If the host does not expose auth state here, keep
|
|
100
|
+
// "unknown": a 401 then reads as a session that ended.
|
|
101
|
+
function currentAuthState(): "signedIn" | "signedOut" | "unknown" { return "unknown"; }
|
|
102
|
+
|
|
83
103
|
// Blog draft bodies are rich content — wrap the plain text minimally.
|
|
84
104
|
function toRichContent(text: string): WixRichContent {
|
|
85
105
|
return { nodes: [{ type: "PARAGRAPH", nodes: [{ type: "TEXT", textData: { text, decorations: [] } }] }] };
|
|
86
106
|
}
|
|
87
107
|
|
|
88
108
|
// Every key that describes the LAST attempt, not this run: a cold boot starts from the declared values.
|
|
89
|
-
const OUTCOME_KEYS = ["status", "error", "savedMessage", "savedScreenId", "hideSavedLink"
|
|
109
|
+
const OUTCOME_KEYS = ["status", "error", "savedMessage", "savedScreenId", "hideSavedLink"] as const;
|
|
90
110
|
|
|
91
111
|
const svc = defineService<PostFormState, {
|
|
92
112
|
submit: (s: PostFormState, values?: Record<string, unknown>) => Promise<PostFormState>;
|
|
93
113
|
}>("postForm", {
|
|
94
114
|
state: shape({}),
|
|
95
|
-
// Outcome keys are per-attempt: a cold boot must not restore a stale error
|
|
96
|
-
//
|
|
115
|
+
// Outcome keys are per-attempt: a cold boot must not restore a stale error or a
|
|
116
|
+
// "Draft created." line from an earlier run.
|
|
97
117
|
notHydrated: OUTCOME_KEYS,
|
|
98
118
|
actions: {
|
|
99
119
|
// `values` is the ONLY channel for what the user typed — keyed by FormField id.
|
|
@@ -115,18 +135,18 @@ const svc = defineService<PostFormState, {
|
|
|
115
135
|
savedScreenId: id ? `my_post_${id}` : "", // wire a templated detail route per the tier-5 guide
|
|
116
136
|
});
|
|
117
137
|
} catch (e) {
|
|
118
|
-
const kind = classifyError(e);
|
|
138
|
+
const kind = classifyError(e, currentAuthState()) || "server";
|
|
119
139
|
// Echo what the user typed back into the prefill, so a failed save loses nothing.
|
|
120
140
|
// Clear the last success line: a failed attempt must not also read "Draft created.".
|
|
121
|
-
return shape({ ...s, title, body, status: "error", error: SAVE_FAILED[kind], savedMessage: ""
|
|
141
|
+
return shape({ ...s, title, body, status: "error", error: SAVE_FAILED[kind], savedMessage: "" });
|
|
122
142
|
}
|
|
123
143
|
},
|
|
124
144
|
},
|
|
125
145
|
});
|
|
126
146
|
|
|
127
147
|
// An older runtime ignores `notHydrated` (it does not set `himalaya.hydrationOptOut`) and restores the
|
|
128
|
-
// whole snapshot: reset the outcome keys in place before any action runs, or
|
|
129
|
-
// would come back on every launch
|
|
148
|
+
// whole snapshot: reset the outcome keys in place before any action runs, or last run's error line
|
|
149
|
+
// would come back on every launch.
|
|
130
150
|
if ((globalThis as { himalaya?: { hydrationOptOut?: unknown } }).himalaya?.hydrationOptOut !== true) {
|
|
131
151
|
const booted = (svc as unknown as { getState?: () => Record<string, unknown> | undefined }).getState?.();
|
|
132
152
|
const fresh = shape({}) as unknown as Record<string, unknown>;
|
package/guides/qa-builds.md
CHANGED
|
@@ -1033,7 +1033,7 @@ and a bundle installs on nothing. `--aab` builds the bundle **and an APK from th
|
|
|
1033
1033
|
an owner who uploads the bundle to Play Console themselves and installs the APK on a phone:
|
|
1034
1034
|
|
|
1035
1035
|
```sh
|
|
1036
|
-
himi login --dpx --target bo # an INTERNAL caller
|
|
1036
|
+
himi login --dpx --target bo # an INTERNAL caller; a self-serve caller gets a bundle only as a Containers store build (see below)
|
|
1037
1037
|
himi dist --app <app> --platform android --aab --wait --target bo
|
|
1038
1038
|
```
|
|
1039
1039
|
|
|
@@ -1046,8 +1046,9 @@ himi dist --app <app> --platform android --aab --wait --target bo
|
|
|
1046
1046
|
(`apk.androidSigningUsed: "qa-keystore"`) and it sits in the bundle's own S3 bucket; otherwise the
|
|
1047
1047
|
bundle still succeeds and the record says `companion_apk_dropped: …`. The record carries `androidFormat: "aab"`.
|
|
1048
1048
|
- **Android only.** The default stays an APK, and iOS is untouched. It travels as
|
|
1049
|
-
`androidFormat: "aab"` on `POST /v1/admin/dist` (every `/v1/owner/dist`
|
|
1050
|
-
|
|
1049
|
+
`androidFormat: "aab"` on `POST /v1/admin/dist` (a self-serve caller, every `/v1/owner/dist`
|
|
1050
|
+
caller included, gets `403 android_format_not_allowed` unless the build is a Containers app's
|
|
1051
|
+
store build, below), and reaches the pool as
|
|
1051
1052
|
`DIST_ANDROID_FORMAT=aab` in the `command` blob (see
|
|
1052
1053
|
[why](#why-the-jobs-parameters-ride-in-command-not-envvars)). Refusals, all typed 400s before
|
|
1053
1054
|
any pool time is spent: `invalid_android_format` (anything but `apk` / `aab`),
|
|
@@ -1115,7 +1116,8 @@ himi dist --app <app> --platform android --aab --wait --target bo
|
|
|
1115
1116
|
The same checks are available WITHOUT a build, for the Containers editor to ask before it
|
|
1116
1117
|
activates a draft release: `GET /v1/admin/site/store-build-check?app=&site=[&shell=]` answers
|
|
1117
1118
|
`200 { ok: true, android: { packageName } }` or the same refusals, through `ValidateStoreBuild`'s
|
|
1118
|
-
check-only mode, so it locks nothing.
|
|
1119
|
+
check-only mode, so it locks nothing. It names no release, so it cannot give
|
|
1120
|
+
`409 release_predates_store_builds`; a draft the editor just built is on a current commit. Until that mode is wired it answers
|
|
1119
1121
|
`501 store_builds_unconfigured`; it never falls back to the locking call.
|
|
1120
1122
|
|
|
1121
1123
|
**Why the Wix production key, and only for the bundle.** There are two Android keys: the committed
|
|
@@ -1124,8 +1126,8 @@ app's bundle carries (`mobile-apps-engine` refuses any other certificate for a P
|
|
|
1124
1126
|
Play App Signing on, so Google re-signs what users install. An `.aab` therefore signs with it —
|
|
1125
1127
|
only its `WixOne` entry of `oneapp-production.keystore`: the alias is pinned for a bundle, whatever
|
|
1126
1128
|
alias the pool's signing files name. Because the first upload binds a Play listing to that key for good, a **self-serve** caller
|
|
1127
|
-
gets
|
|
1128
|
-
|
|
1129
|
+
gets a bundle only as a Containers app's store build, which checks the app, its owner and its
|
|
1130
|
+
package name first (above); any other self-serve `--aab` is `403 android_format_not_allowed`. A self-serve **APK** with
|
|
1129
1131
|
`wix-keystore` keeps `403 android_signing_not_allowed`, unchanged. The APK built beside a bundle is
|
|
1130
1132
|
QA-signed, so no installable APK carrying the WixOne signature is ever handed out; WixOne would not
|
|
1131
1133
|
help it anyway, because the APKs Google Play delivers carry Google's app-signing key, so a phone with
|
|
@@ -67,8 +67,8 @@ const TRANSPORT = /NSURLErrorDomain|(?:UnknownHost|Connect|SocketTimeout|NoRoute
|
|
|
67
67
|
// Android reports an unknown host as the bare hostname (`users.wix.com`) — hence the last form.
|
|
68
68
|
// No session on the device: the native bridge refuses before any request leaves.
|
|
69
69
|
// A pinned site with no member session: iOS `siteNotPermitted(requested: "account", …)`,
|
|
70
|
-
// Android `site account is not the bound site
|
|
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;
|
|
70
|
+
// Android `site account is not the bound site …`; the web host `HTTP 501: {"error":"webwix_owner_no_session"}`.
|
|
71
|
+
const SIGNED_OUT = /wixApi not enabled|session unavailable|PRINCIPAL_CHANGED|not signed in|siteNotPermitted\(requested: "account"|site account is not the bound site|webwix_owner_no_session/i;
|
|
72
72
|
// The account is not a member of this site: iOS `NotSiteMember(metaSiteId: …)`,
|
|
73
73
|
// Android `not a member of site …`. NOT `WDE0027` — that is Wix Data's generic
|
|
74
74
|
// permission refusal, which a signed-in member and a visitor both get.
|
|
@@ -110,6 +110,20 @@
|
|
|
110
110
|
"hiddenPath": "$service:posts.hideServerError"
|
|
111
111
|
}
|
|
112
112
|
},
|
|
113
|
+
{
|
|
114
|
+
"id": "error.membership",
|
|
115
|
+
"body": {
|
|
116
|
+
"package": "himalaya",
|
|
117
|
+
"type": "EmptyState",
|
|
118
|
+
"props": {
|
|
119
|
+
"title": "Join this site to view posts",
|
|
120
|
+
"subtitle": "Ask the owner of this blog to add you as a member."
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
"modifiers": {
|
|
124
|
+
"hiddenPath": "$service:posts.hideMembershipError"
|
|
125
|
+
}
|
|
126
|
+
},
|
|
113
127
|
{
|
|
114
128
|
"id": "error.permission",
|
|
115
129
|
"body": {
|
|
@@ -21,15 +21,17 @@ interface PostRow {
|
|
|
21
21
|
categoryIds: string[];
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
//
|
|
25
|
-
type ErrorKind = "" | "offline" | "server" | "permission";
|
|
24
|
+
// Every class the recipe's classifier returns …
|
|
25
|
+
type ErrorKind = "" | "offline" | "server" | "signIn" | "membership" | "permission";
|
|
26
|
+
// … and the ones this screen shows: it has no sign-in flow (see shownKind()).
|
|
27
|
+
type ShownKind = Exclude<ErrorKind, "" | "signIn">;
|
|
26
28
|
|
|
27
29
|
interface PostsState {
|
|
28
30
|
status: "loading" | "ready" | "error";
|
|
29
31
|
all: PostRow[]; // the full fetched set — NOT bound by the descriptor
|
|
30
32
|
items: PostRow[]; // ← itemsPath binds here; ALWAYS derived from all + query
|
|
31
33
|
query: string;
|
|
32
|
-
errorKind:
|
|
34
|
+
errorKind: "" | ShownKind;
|
|
33
35
|
// hiddenPath flags (hide when TRUE) — see `himi docs recipe-four-state`
|
|
34
36
|
hideLoading: boolean;
|
|
35
37
|
hideNoPosts: boolean; // EmptyState: the blog has no posts at all
|
|
@@ -37,6 +39,7 @@ interface PostsState {
|
|
|
37
39
|
notEmpty: boolean; // false while either EmptyState shows
|
|
38
40
|
hideOfflineError: boolean;
|
|
39
41
|
hideServerError: boolean;
|
|
42
|
+
hideMembershipError: boolean;
|
|
40
43
|
hidePermissionError: boolean;
|
|
41
44
|
hideRefreshError: boolean;
|
|
42
45
|
}
|
|
@@ -64,7 +67,7 @@ function shape(s: Partial<PostsState>): PostsState {
|
|
|
64
67
|
const all = s.all ?? [];
|
|
65
68
|
const query = s.query ?? "";
|
|
66
69
|
const items = s.items ?? visible(all, query);
|
|
67
|
-
const errorKind:
|
|
70
|
+
const errorKind: "" | ShownKind = status === "error" ? (s.errorKind || "server") : "";
|
|
68
71
|
const blank = all.length === 0; // nothing loaded to keep
|
|
69
72
|
return {
|
|
70
73
|
status,
|
|
@@ -80,13 +83,16 @@ function shape(s: Partial<PostsState>): PostsState {
|
|
|
80
83
|
hideLoading: !(status === "loading" && blank),
|
|
81
84
|
hideOfflineError: !(blank && errorKind === "offline"),
|
|
82
85
|
hideServerError: !(blank && errorKind === "server"),
|
|
86
|
+
hideMembershipError: !(blank && errorKind === "membership"),
|
|
83
87
|
hidePermissionError: !(blank && errorKind === "permission"),
|
|
84
88
|
hideRefreshError: !(!blank && (errorKind === "offline" || errorKind === "server")),
|
|
85
89
|
};
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
// The classifier below is `himi docs recipe-four-state`'s, VERBATIM — a test holds
|
|
93
|
+
// the two byte-identical, so the rules an agent copies from here are the documented ones.
|
|
88
94
|
// The HTTP status rides in the message: `HTTP 503: …` on a device, `Wix 503 for …`
|
|
89
|
-
// under `himi test` / `himi render`.
|
|
95
|
+
// under `himi test` / `himi render`.
|
|
90
96
|
const STATUS = /\b(?:HTTP|Wix) (\d{3})\b/;
|
|
91
97
|
// No answer at all — what a dropped connection says on every platform.
|
|
92
98
|
const TRANSPORT = /NSURLErrorDomain|(?:UnknownHost|Connect|SocketTimeout|NoRouteToHost|SSLHandshake)Exception|Failed to connect|Unable to resolve host|Connection (?:refused|reset)|Network is unreachable|Software caused connection abort|ECONNRESET|ETIMEDOUT|EPIPE|Broken pipe|stream was reset|unexpected end of stream|timed out|^timeout$|Failed to fetch|NetworkError|^(?:TypeError: )?Load failed$|net::ERR_|^[a-z0-9-]+(?:\.[a-z0-9-]+)+$/i;
|
|
@@ -95,21 +101,45 @@ const TRANSPORT = /NSURLErrorDomain|(?:UnknownHost|Connect|SocketTimeout|NoRoute
|
|
|
95
101
|
// A pinned site with no member session: iOS `siteNotPermitted(requested: "account", …)`,
|
|
96
102
|
// Android `site account is not the bound site …`; the web host `HTTP 501: {"error":"webwix_owner_no_session"}`.
|
|
97
103
|
const SIGNED_OUT = /wixApi not enabled|session unavailable|PRINCIPAL_CHANGED|not signed in|siteNotPermitted\(requested: "account"|site account is not the bound site|webwix_owner_no_session/i;
|
|
104
|
+
// The account is not a member of this site: iOS `NotSiteMember(metaSiteId: …)`,
|
|
105
|
+
// Android `not a member of site …`. NOT `WDE0027` — that is Wix Data's generic
|
|
106
|
+
// permission refusal, which a signed-in member and a visitor both get.
|
|
107
|
+
const NOT_MEMBER = /\bNotSiteMember\b|\bnot a member of site\b/i;
|
|
98
108
|
|
|
99
|
-
|
|
100
|
-
function classifyError(e: unknown): ErrorKind {
|
|
109
|
+
function classifyError(e: unknown, authState: "signedIn" | "signedOut" | "unknown" = "unknown"): ErrorKind {
|
|
101
110
|
const msg = String((e as Error)?.message ?? e ?? "");
|
|
102
|
-
|
|
111
|
+
// Some bridges put the Wix code in the response body rather than Error.code.
|
|
112
|
+
const code = String((e as { code?: unknown })?.code ?? /"(?:code|error)"\s*:\s*"([^"]+)"/.exec(msg)?.[1] ?? "");
|
|
103
113
|
const status = Number(STATUS.exec(msg)?.[1] ?? 0);
|
|
104
|
-
if (
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
114
|
+
if (code === "AccountSessionExpired" || code === "webwix_account_signed_out" || SIGNED_OUT.test(msg)) return "signIn";
|
|
115
|
+
if (code === "NotSiteMember" || NOT_MEMBER.test(msg)) return "membership";
|
|
116
|
+
// A 401 is a missing or expired session — unless the host knows the user is signed
|
|
117
|
+
// in, when it is a policy refusal. Never "server": a retry sends the same 401 back.
|
|
118
|
+
if (status === 401) return authState === "signedIn" ? "permission" : "signIn";
|
|
119
|
+
// A 403 is a missing permission — but a visitor the host knows is signed out gets
|
|
120
|
+
// `403 WDE0027` on member-only data too, and signing in is what fixes that.
|
|
121
|
+
if (status === 403) return authState === "signedOut" ? "signIn" : "permission";
|
|
122
|
+
if (code === "offline_write_blocked") return "offline"; // a write refused before it left
|
|
108
123
|
// A web call that never reached Wix arrives as
|
|
109
124
|
// `HTTP 502: {"error":"webwix_direct_failed","detail":"Failed to fetch"}` — judge its detail.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
125
|
+
// The top-level 502 is the bridge wrapper, not the underlying transport result.
|
|
126
|
+
const detail = msg.includes("webwix_direct_failed")
|
|
127
|
+
? /"detail"\s*:\s*"([^"]+)"/.exec(msg)?.[1] ?? msg
|
|
128
|
+
: msg;
|
|
129
|
+
if (status && detail === msg) return "server";
|
|
130
|
+
if (TRANSPORT.test(detail)) return "offline";
|
|
131
|
+
return "server"; // anything else — a bug, a malformed row — is not the user's connection
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// App-specific host integration. If the host does not expose auth state here, keep
|
|
135
|
+
// "unknown": a 401 then reads as a session that ended.
|
|
136
|
+
function currentAuthState(): "signedIn" | "signedOut" | "unknown" { return "unknown"; }
|
|
137
|
+
|
|
138
|
+
// This screen has no sign-in flow, so a sign-in failure shows the permission state —
|
|
139
|
+
// every class it can show has a node in the descriptor.
|
|
140
|
+
function shownKind(e: unknown): ShownKind {
|
|
141
|
+
const kind = classifyError(e, currentAuthState());
|
|
142
|
+
return kind === "signIn" ? "permission" : kind || "server";
|
|
113
143
|
}
|
|
114
144
|
|
|
115
145
|
// The SearchField onChange arg shape varies by surface — read it defensively.
|
|
@@ -132,9 +162,10 @@ async function loadList(s: Partial<PostsState>): Promise<PostsState> {
|
|
|
132
162
|
.filter((r) => r.id);
|
|
133
163
|
return shape({ status: "ready", all, query: currentQuery });
|
|
134
164
|
} catch (e) {
|
|
135
|
-
const errorKind =
|
|
165
|
+
const errorKind = shownKind(e);
|
|
136
166
|
// Keep what was loaded on a refresh failure; clear it when access is gone.
|
|
137
|
-
const
|
|
167
|
+
const lostAccess = errorKind === "membership" || errorKind === "permission";
|
|
168
|
+
const all = lostAccess ? [] : s.all ?? [];
|
|
138
169
|
return shape({ status: "error", all, query: currentQuery, errorKind });
|
|
139
170
|
}
|
|
140
171
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/himalaya-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1486.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": "
|
|
74
|
+
"falconPackageHash": "90739efa8b897c00c4666dfd784b02c22d05fbd8ae637fe851d2da5f"
|
|
75
75
|
}
|