@wix/himalaya-cli 0.942.0 → 0.944.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.
@@ -11,7 +11,12 @@ import {
11
11
  } from "@himalaya/wix-stores-collections";
12
12
 
13
13
  interface Row { id: string; name: string; subtitle: string }
14
- interface S { status: "loading" | "ready"; collections: Row[]; emptyText: string }
14
+ interface S {
15
+ status: "loading" | "ready" | "error";
16
+ collections: Row[];
17
+ emptyText: string;
18
+ error: string;
19
+ }
15
20
 
16
21
  function fromCategory(c: StoreCategory): Row {
17
22
  return { id: c.id || "", name: c.name || "Category", subtitle: categoryCount(c) };
@@ -23,26 +28,69 @@ function fromCollection(c: WixCollection): Row {
23
28
  return { id: cc.id || cc.name || "collection", name: cc.name || "Collection", subtitle: n != null ? `${n} ${n === 1 ? "product" : "products"}` : "" };
24
29
  }
25
30
 
26
- async function loadCollections(): Promise<Row[]> {
31
+ type Loaded = { ok: true; rows: Row[] } | { ok: false; error: string };
32
+
33
+ async function loadCollections(): Promise<Loaded> {
27
34
  try {
28
35
  const r = await queryStoreCategories(50);
29
- const rows = (r.categories ?? []).map(fromCategory).filter((row) => row.id);
30
- if (rows.length) return rows;
31
- } catch { /* fall through to V1 */ }
36
+ // A V3 SUCCESS with zero rows is an empty catalogue, not a reason to try V1, and this is the
37
+ // line that matters. Written as `if (rows.length) return rows`, a modern (stores-v3) site
38
+ // that simply has no categories fell through to the V1 call — which `himi sdk
39
+ // wix-stores-collections` documents as answering HTTP 428 on exactly those sites — and the
40
+ // catch below turned that 428 into an empty list. The screen then told the visitor "No
41
+ // categories yet" over a dead endpoint.
42
+ //
43
+ // Only a V3 THROW earns the fallback, because a legacy V1-catalogue site is the single case
44
+ // the fallback exists for.
45
+ //
46
+ // WHAT IS ESTABLISHED vs WHAT IS ASSUMED, because the two are not the same here and a
47
+ // reviewer was right to ask. Established: V1-on-V3 answers HTTP 428 — `himi sdk
48
+ // wix-stores-collections` documents it and names the message. ASSUMED, and not yet
49
+ // verified against a live legacy site: that the reverse — the V3 Categories API, a separate
50
+ // app-namespaced service, called on a Catalog-V1 site — THROWS rather than answering
51
+ // `200 {}`. If it answers 200-empty, a legacy site stops reaching the fallback and shows
52
+ // "No categories yet" instead of its collections: the same wrong-empty-state this change
53
+ // fixes, moved to the other site type.
54
+ //
55
+ // It is left as a throw-only fallback deliberately, because the alternative is worse:
56
+ // falling back on a V3 empty result sends every modern site with a genuinely empty
57
+ // catalogue into the 428 path, and the catch below now surfaces that as "Couldn't load
58
+ // categories" over a catalogue that is simply empty. Confirm against a Catalog-V1 site
59
+ // before changing this; five templates carry this file.
60
+ return { ok: true, rows: (r.categories ?? []).map(fromCategory).filter((row) => row.id) };
61
+ } catch { /* V3 unavailable — a legacy V1 site is what the fallback is for */ }
32
62
  try {
33
- const r = await queryCollections({ query: { paging: { limit: 50 } } } as never);
34
- return (((r as unknown as { collections?: WixCollection[] }).collections) ?? []).map(fromCollection);
35
- } catch {
36
- return [];
63
+ // No `as never` here: `WixQueryCollectionsRequest.query` is `WixCollectionsQuery`, which
64
+ // declares `paging`, so this argument type-checks as written. Reach for a cast only when the
65
+ // types really cannot express the call — silencing them by habit hides the next real
66
+ // mismatch.
67
+ const r = await queryCollections({ query: { paging: { limit: 50 } } });
68
+ return { ok: true, rows: (((r as unknown as { collections?: WixCollection[] }).collections) ?? []).map(fromCollection) };
69
+ } catch (e) {
70
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
37
71
  }
38
72
  }
39
73
 
40
74
  const svc = defineService<S, { load: (s: S) => Promise<S> }>("spacesCollections", {
41
- state: { status: "loading", collections: [], emptyText: "" },
75
+ state: { status: "loading", collections: [], emptyText: "", error: "" },
42
76
  actions: {
43
77
  load: async () => {
44
- const collections = await loadCollections();
45
- return { status: "ready", collections, emptyText: collections.length ? "" : "No categories yet" };
78
+ const res = await loadCollections();
79
+ if (!res.ok) {
80
+ // `emptyText` carries the honest line as well as `error`, deliberately: this screen's
81
+ // descriptor binds only `emptyText`, so writing the failure there is what stops the
82
+ // visitor being told the catalogue is empty when the call actually failed. `status` and
83
+ // `error` are what an error-state binding and `himi test` read — bind them when you add
84
+ // a dedicated error view.
85
+ const line = "Couldn't load categories. Pull to refresh.";
86
+ return { status: "error", collections: [], emptyText: line, error: res.error };
87
+ }
88
+ return {
89
+ status: "ready",
90
+ collections: res.rows,
91
+ emptyText: res.rows.length ? "" : "No categories yet",
92
+ error: "",
93
+ };
46
94
  },
47
95
  },
48
96
  });
@@ -11,7 +11,12 @@ import {
11
11
  } from "@himalaya/wix-stores-collections";
12
12
 
13
13
  interface Row { id: string; name: string; subtitle: string }
14
- interface S { status: "loading" | "ready"; collections: Row[]; emptyText: string }
14
+ interface S {
15
+ status: "loading" | "ready" | "error";
16
+ collections: Row[];
17
+ emptyText: string;
18
+ error: string;
19
+ }
15
20
 
16
21
  function fromCategory(c: StoreCategory): Row {
17
22
  return { id: c.id || "", name: c.name || "Category", subtitle: categoryCount(c) };
@@ -23,26 +28,69 @@ function fromCollection(c: WixCollection): Row {
23
28
  return { id: cc.id || cc.name || "collection", name: cc.name || "Collection", subtitle: n != null ? `${n} ${n === 1 ? "product" : "products"}` : "" };
24
29
  }
25
30
 
26
- async function loadCollections(): Promise<Row[]> {
31
+ type Loaded = { ok: true; rows: Row[] } | { ok: false; error: string };
32
+
33
+ async function loadCollections(): Promise<Loaded> {
27
34
  try {
28
35
  const r = await queryStoreCategories(50);
29
- const rows = (r.categories ?? []).map(fromCategory).filter((row) => row.id);
30
- if (rows.length) return rows;
31
- } catch { /* fall through to V1 */ }
36
+ // A V3 SUCCESS with zero rows is an empty catalogue, not a reason to try V1, and this is the
37
+ // line that matters. Written as `if (rows.length) return rows`, a modern (stores-v3) site
38
+ // that simply has no categories fell through to the V1 call — which `himi sdk
39
+ // wix-stores-collections` documents as answering HTTP 428 on exactly those sites — and the
40
+ // catch below turned that 428 into an empty list. The screen then told the visitor "No
41
+ // categories yet" over a dead endpoint.
42
+ //
43
+ // Only a V3 THROW earns the fallback, because a legacy V1-catalogue site is the single case
44
+ // the fallback exists for.
45
+ //
46
+ // WHAT IS ESTABLISHED vs WHAT IS ASSUMED, because the two are not the same here and a
47
+ // reviewer was right to ask. Established: V1-on-V3 answers HTTP 428 — `himi sdk
48
+ // wix-stores-collections` documents it and names the message. ASSUMED, and not yet
49
+ // verified against a live legacy site: that the reverse — the V3 Categories API, a separate
50
+ // app-namespaced service, called on a Catalog-V1 site — THROWS rather than answering
51
+ // `200 {}`. If it answers 200-empty, a legacy site stops reaching the fallback and shows
52
+ // "No categories yet" instead of its collections: the same wrong-empty-state this change
53
+ // fixes, moved to the other site type.
54
+ //
55
+ // It is left as a throw-only fallback deliberately, because the alternative is worse:
56
+ // falling back on a V3 empty result sends every modern site with a genuinely empty
57
+ // catalogue into the 428 path, and the catch below now surfaces that as "Couldn't load
58
+ // categories" over a catalogue that is simply empty. Confirm against a Catalog-V1 site
59
+ // before changing this; five templates carry this file.
60
+ return { ok: true, rows: (r.categories ?? []).map(fromCategory).filter((row) => row.id) };
61
+ } catch { /* V3 unavailable — a legacy V1 site is what the fallback is for */ }
32
62
  try {
33
- const r = await queryCollections({ query: { paging: { limit: 50 } } } as never);
34
- return (((r as unknown as { collections?: WixCollection[] }).collections) ?? []).map(fromCollection);
35
- } catch {
36
- return [];
63
+ // No `as never` here: `WixQueryCollectionsRequest.query` is `WixCollectionsQuery`, which
64
+ // declares `paging`, so this argument type-checks as written. Reach for a cast only when the
65
+ // types really cannot express the call — silencing them by habit hides the next real
66
+ // mismatch.
67
+ const r = await queryCollections({ query: { paging: { limit: 50 } } });
68
+ return { ok: true, rows: (((r as unknown as { collections?: WixCollection[] }).collections) ?? []).map(fromCollection) };
69
+ } catch (e) {
70
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
37
71
  }
38
72
  }
39
73
 
40
74
  const svc = defineService<S, { load: (s: S) => Promise<S> }>("spacesCollections", {
41
- state: { status: "loading", collections: [], emptyText: "" },
75
+ state: { status: "loading", collections: [], emptyText: "", error: "" },
42
76
  actions: {
43
77
  load: async () => {
44
- const collections = await loadCollections();
45
- return { status: "ready", collections, emptyText: collections.length ? "" : "No categories yet" };
78
+ const res = await loadCollections();
79
+ if (!res.ok) {
80
+ // `emptyText` carries the honest line as well as `error`, deliberately: this screen's
81
+ // descriptor binds only `emptyText`, so writing the failure there is what stops the
82
+ // visitor being told the catalogue is empty when the call actually failed. `status` and
83
+ // `error` are what an error-state binding and `himi test` read — bind them when you add
84
+ // a dedicated error view.
85
+ const line = "Couldn't load categories. Pull to refresh.";
86
+ return { status: "error", collections: [], emptyText: line, error: res.error };
87
+ }
88
+ return {
89
+ status: "ready",
90
+ collections: res.rows,
91
+ emptyText: res.rows.length ? "" : "No categories yet",
92
+ error: "",
93
+ };
46
94
  },
47
95
  },
48
96
  });
@@ -11,7 +11,12 @@ import {
11
11
  } from "@himalaya/wix-stores-collections";
12
12
 
13
13
  interface Row { id: string; name: string; subtitle: string }
14
- interface S { status: "loading" | "ready"; collections: Row[]; emptyText: string }
14
+ interface S {
15
+ status: "loading" | "ready" | "error";
16
+ collections: Row[];
17
+ emptyText: string;
18
+ error: string;
19
+ }
15
20
 
16
21
  function fromCategory(c: StoreCategory): Row {
17
22
  return { id: c.id || "", name: c.name || "Category", subtitle: categoryCount(c) };
@@ -23,26 +28,69 @@ function fromCollection(c: WixCollection): Row {
23
28
  return { id: cc.id || cc.name || "collection", name: cc.name || "Collection", subtitle: n != null ? `${n} ${n === 1 ? "product" : "products"}` : "" };
24
29
  }
25
30
 
26
- async function loadCollections(): Promise<Row[]> {
31
+ type Loaded = { ok: true; rows: Row[] } | { ok: false; error: string };
32
+
33
+ async function loadCollections(): Promise<Loaded> {
27
34
  try {
28
35
  const r = await queryStoreCategories(50);
29
- const rows = (r.categories ?? []).map(fromCategory).filter((row) => row.id);
30
- if (rows.length) return rows;
31
- } catch { /* fall through to V1 */ }
36
+ // A V3 SUCCESS with zero rows is an empty catalogue, not a reason to try V1, and this is the
37
+ // line that matters. Written as `if (rows.length) return rows`, a modern (stores-v3) site
38
+ // that simply has no categories fell through to the V1 call — which `himi sdk
39
+ // wix-stores-collections` documents as answering HTTP 428 on exactly those sites — and the
40
+ // catch below turned that 428 into an empty list. The screen then told the visitor "No
41
+ // categories yet" over a dead endpoint.
42
+ //
43
+ // Only a V3 THROW earns the fallback, because a legacy V1-catalogue site is the single case
44
+ // the fallback exists for.
45
+ //
46
+ // WHAT IS ESTABLISHED vs WHAT IS ASSUMED, because the two are not the same here and a
47
+ // reviewer was right to ask. Established: V1-on-V3 answers HTTP 428 — `himi sdk
48
+ // wix-stores-collections` documents it and names the message. ASSUMED, and not yet
49
+ // verified against a live legacy site: that the reverse — the V3 Categories API, a separate
50
+ // app-namespaced service, called on a Catalog-V1 site — THROWS rather than answering
51
+ // `200 {}`. If it answers 200-empty, a legacy site stops reaching the fallback and shows
52
+ // "No categories yet" instead of its collections: the same wrong-empty-state this change
53
+ // fixes, moved to the other site type.
54
+ //
55
+ // It is left as a throw-only fallback deliberately, because the alternative is worse:
56
+ // falling back on a V3 empty result sends every modern site with a genuinely empty
57
+ // catalogue into the 428 path, and the catch below now surfaces that as "Couldn't load
58
+ // categories" over a catalogue that is simply empty. Confirm against a Catalog-V1 site
59
+ // before changing this; five templates carry this file.
60
+ return { ok: true, rows: (r.categories ?? []).map(fromCategory).filter((row) => row.id) };
61
+ } catch { /* V3 unavailable — a legacy V1 site is what the fallback is for */ }
32
62
  try {
33
- const r = await queryCollections({ query: { paging: { limit: 50 } } } as never);
34
- return (((r as unknown as { collections?: WixCollection[] }).collections) ?? []).map(fromCollection);
35
- } catch {
36
- return [];
63
+ // No `as never` here: `WixQueryCollectionsRequest.query` is `WixCollectionsQuery`, which
64
+ // declares `paging`, so this argument type-checks as written. Reach for a cast only when the
65
+ // types really cannot express the call — silencing them by habit hides the next real
66
+ // mismatch.
67
+ const r = await queryCollections({ query: { paging: { limit: 50 } } });
68
+ return { ok: true, rows: (((r as unknown as { collections?: WixCollection[] }).collections) ?? []).map(fromCollection) };
69
+ } catch (e) {
70
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
37
71
  }
38
72
  }
39
73
 
40
74
  const svc = defineService<S, { load: (s: S) => Promise<S> }>("spacesCollections", {
41
- state: { status: "loading", collections: [], emptyText: "" },
75
+ state: { status: "loading", collections: [], emptyText: "", error: "" },
42
76
  actions: {
43
77
  load: async () => {
44
- const collections = await loadCollections();
45
- return { status: "ready", collections, emptyText: collections.length ? "" : "No categories yet" };
78
+ const res = await loadCollections();
79
+ if (!res.ok) {
80
+ // `emptyText` carries the honest line as well as `error`, deliberately: this screen's
81
+ // descriptor binds only `emptyText`, so writing the failure there is what stops the
82
+ // visitor being told the catalogue is empty when the call actually failed. `status` and
83
+ // `error` are what an error-state binding and `himi test` read — bind them when you add
84
+ // a dedicated error view.
85
+ const line = "Couldn't load categories. Pull to refresh.";
86
+ return { status: "error", collections: [], emptyText: line, error: res.error };
87
+ }
88
+ return {
89
+ status: "ready",
90
+ collections: res.rows,
91
+ emptyText: res.rows.length ? "" : "No categories yet",
92
+ error: "",
93
+ };
46
94
  },
47
95
  },
48
96
  });
@@ -11,7 +11,12 @@ import {
11
11
  } from "@himalaya/wix-stores-collections";
12
12
 
13
13
  interface Row { id: string; name: string; subtitle: string }
14
- interface S { status: "loading" | "ready"; collections: Row[]; emptyText: string }
14
+ interface S {
15
+ status: "loading" | "ready" | "error";
16
+ collections: Row[];
17
+ emptyText: string;
18
+ error: string;
19
+ }
15
20
 
16
21
  function fromCategory(c: StoreCategory): Row {
17
22
  return { id: c.id || "", name: c.name || "Category", subtitle: categoryCount(c) };
@@ -23,26 +28,69 @@ function fromCollection(c: WixCollection): Row {
23
28
  return { id: cc.id || cc.name || "collection", name: cc.name || "Collection", subtitle: n != null ? `${n} ${n === 1 ? "product" : "products"}` : "" };
24
29
  }
25
30
 
26
- async function loadCollections(): Promise<Row[]> {
31
+ type Loaded = { ok: true; rows: Row[] } | { ok: false; error: string };
32
+
33
+ async function loadCollections(): Promise<Loaded> {
27
34
  try {
28
35
  const r = await queryStoreCategories(50);
29
- const rows = (r.categories ?? []).map(fromCategory).filter((row) => row.id);
30
- if (rows.length) return rows;
31
- } catch { /* fall through to V1 */ }
36
+ // A V3 SUCCESS with zero rows is an empty catalogue, not a reason to try V1, and this is the
37
+ // line that matters. Written as `if (rows.length) return rows`, a modern (stores-v3) site
38
+ // that simply has no categories fell through to the V1 call — which `himi sdk
39
+ // wix-stores-collections` documents as answering HTTP 428 on exactly those sites — and the
40
+ // catch below turned that 428 into an empty list. The screen then told the visitor "No
41
+ // categories yet" over a dead endpoint.
42
+ //
43
+ // Only a V3 THROW earns the fallback, because a legacy V1-catalogue site is the single case
44
+ // the fallback exists for.
45
+ //
46
+ // WHAT IS ESTABLISHED vs WHAT IS ASSUMED, because the two are not the same here and a
47
+ // reviewer was right to ask. Established: V1-on-V3 answers HTTP 428 — `himi sdk
48
+ // wix-stores-collections` documents it and names the message. ASSUMED, and not yet
49
+ // verified against a live legacy site: that the reverse — the V3 Categories API, a separate
50
+ // app-namespaced service, called on a Catalog-V1 site — THROWS rather than answering
51
+ // `200 {}`. If it answers 200-empty, a legacy site stops reaching the fallback and shows
52
+ // "No categories yet" instead of its collections: the same wrong-empty-state this change
53
+ // fixes, moved to the other site type.
54
+ //
55
+ // It is left as a throw-only fallback deliberately, because the alternative is worse:
56
+ // falling back on a V3 empty result sends every modern site with a genuinely empty
57
+ // catalogue into the 428 path, and the catch below now surfaces that as "Couldn't load
58
+ // categories" over a catalogue that is simply empty. Confirm against a Catalog-V1 site
59
+ // before changing this; five templates carry this file.
60
+ return { ok: true, rows: (r.categories ?? []).map(fromCategory).filter((row) => row.id) };
61
+ } catch { /* V3 unavailable — a legacy V1 site is what the fallback is for */ }
32
62
  try {
33
- const r = await queryCollections({ query: { paging: { limit: 50 } } } as never);
34
- return (((r as unknown as { collections?: WixCollection[] }).collections) ?? []).map(fromCollection);
35
- } catch {
36
- return [];
63
+ // No `as never` here: `WixQueryCollectionsRequest.query` is `WixCollectionsQuery`, which
64
+ // declares `paging`, so this argument type-checks as written. Reach for a cast only when the
65
+ // types really cannot express the call — silencing them by habit hides the next real
66
+ // mismatch.
67
+ const r = await queryCollections({ query: { paging: { limit: 50 } } });
68
+ return { ok: true, rows: (((r as unknown as { collections?: WixCollection[] }).collections) ?? []).map(fromCollection) };
69
+ } catch (e) {
70
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
37
71
  }
38
72
  }
39
73
 
40
74
  const svc = defineService<S, { load: (s: S) => Promise<S> }>("spacesCollections", {
41
- state: { status: "loading", collections: [], emptyText: "" },
75
+ state: { status: "loading", collections: [], emptyText: "", error: "" },
42
76
  actions: {
43
77
  load: async () => {
44
- const collections = await loadCollections();
45
- return { status: "ready", collections, emptyText: collections.length ? "" : "No categories yet" };
78
+ const res = await loadCollections();
79
+ if (!res.ok) {
80
+ // `emptyText` carries the honest line as well as `error`, deliberately: this screen's
81
+ // descriptor binds only `emptyText`, so writing the failure there is what stops the
82
+ // visitor being told the catalogue is empty when the call actually failed. `status` and
83
+ // `error` are what an error-state binding and `himi test` read — bind them when you add
84
+ // a dedicated error view.
85
+ const line = "Couldn't load categories. Pull to refresh.";
86
+ return { status: "error", collections: [], emptyText: line, error: res.error };
87
+ }
88
+ return {
89
+ status: "ready",
90
+ collections: res.rows,
91
+ emptyText: res.rows.length ? "" : "No categories yet",
92
+ error: "",
93
+ };
46
94
  },
47
95
  },
48
96
  });
@@ -11,7 +11,12 @@ import {
11
11
  } from "@himalaya/wix-stores-collections";
12
12
 
13
13
  interface Row { id: string; name: string; subtitle: string }
14
- interface S { status: "loading" | "ready"; collections: Row[]; emptyText: string }
14
+ interface S {
15
+ status: "loading" | "ready" | "error";
16
+ collections: Row[];
17
+ emptyText: string;
18
+ error: string;
19
+ }
15
20
 
16
21
  function fromCategory(c: StoreCategory): Row {
17
22
  return { id: c.id || "", name: c.name || "Category", subtitle: categoryCount(c) };
@@ -23,26 +28,69 @@ function fromCollection(c: WixCollection): Row {
23
28
  return { id: cc.id || cc.name || "collection", name: cc.name || "Collection", subtitle: n != null ? `${n} ${n === 1 ? "product" : "products"}` : "" };
24
29
  }
25
30
 
26
- async function loadCollections(): Promise<Row[]> {
31
+ type Loaded = { ok: true; rows: Row[] } | { ok: false; error: string };
32
+
33
+ async function loadCollections(): Promise<Loaded> {
27
34
  try {
28
35
  const r = await queryStoreCategories(50);
29
- const rows = (r.categories ?? []).map(fromCategory).filter((row) => row.id);
30
- if (rows.length) return rows;
31
- } catch { /* fall through to V1 */ }
36
+ // A V3 SUCCESS with zero rows is an empty catalogue, not a reason to try V1, and this is the
37
+ // line that matters. Written as `if (rows.length) return rows`, a modern (stores-v3) site
38
+ // that simply has no categories fell through to the V1 call — which `himi sdk
39
+ // wix-stores-collections` documents as answering HTTP 428 on exactly those sites — and the
40
+ // catch below turned that 428 into an empty list. The screen then told the visitor "No
41
+ // categories yet" over a dead endpoint.
42
+ //
43
+ // Only a V3 THROW earns the fallback, because a legacy V1-catalogue site is the single case
44
+ // the fallback exists for.
45
+ //
46
+ // WHAT IS ESTABLISHED vs WHAT IS ASSUMED, because the two are not the same here and a
47
+ // reviewer was right to ask. Established: V1-on-V3 answers HTTP 428 — `himi sdk
48
+ // wix-stores-collections` documents it and names the message. ASSUMED, and not yet
49
+ // verified against a live legacy site: that the reverse — the V3 Categories API, a separate
50
+ // app-namespaced service, called on a Catalog-V1 site — THROWS rather than answering
51
+ // `200 {}`. If it answers 200-empty, a legacy site stops reaching the fallback and shows
52
+ // "No categories yet" instead of its collections: the same wrong-empty-state this change
53
+ // fixes, moved to the other site type.
54
+ //
55
+ // It is left as a throw-only fallback deliberately, because the alternative is worse:
56
+ // falling back on a V3 empty result sends every modern site with a genuinely empty
57
+ // catalogue into the 428 path, and the catch below now surfaces that as "Couldn't load
58
+ // categories" over a catalogue that is simply empty. Confirm against a Catalog-V1 site
59
+ // before changing this; five templates carry this file.
60
+ return { ok: true, rows: (r.categories ?? []).map(fromCategory).filter((row) => row.id) };
61
+ } catch { /* V3 unavailable — a legacy V1 site is what the fallback is for */ }
32
62
  try {
33
- const r = await queryCollections({ query: { paging: { limit: 50 } } } as never);
34
- return (((r as unknown as { collections?: WixCollection[] }).collections) ?? []).map(fromCollection);
35
- } catch {
36
- return [];
63
+ // No `as never` here: `WixQueryCollectionsRequest.query` is `WixCollectionsQuery`, which
64
+ // declares `paging`, so this argument type-checks as written. Reach for a cast only when the
65
+ // types really cannot express the call — silencing them by habit hides the next real
66
+ // mismatch.
67
+ const r = await queryCollections({ query: { paging: { limit: 50 } } });
68
+ return { ok: true, rows: (((r as unknown as { collections?: WixCollection[] }).collections) ?? []).map(fromCollection) };
69
+ } catch (e) {
70
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
37
71
  }
38
72
  }
39
73
 
40
74
  const svc = defineService<S, { load: (s: S) => Promise<S> }>("spacesCollections", {
41
- state: { status: "loading", collections: [], emptyText: "" },
75
+ state: { status: "loading", collections: [], emptyText: "", error: "" },
42
76
  actions: {
43
77
  load: async () => {
44
- const collections = await loadCollections();
45
- return { status: "ready", collections, emptyText: collections.length ? "" : "No categories yet" };
78
+ const res = await loadCollections();
79
+ if (!res.ok) {
80
+ // `emptyText` carries the honest line as well as `error`, deliberately: this screen's
81
+ // descriptor binds only `emptyText`, so writing the failure there is what stops the
82
+ // visitor being told the catalogue is empty when the call actually failed. `status` and
83
+ // `error` are what an error-state binding and `himi test` read — bind them when you add
84
+ // a dedicated error view.
85
+ const line = "Couldn't load categories. Pull to refresh.";
86
+ return { status: "error", collections: [], emptyText: line, error: res.error };
87
+ }
88
+ return {
89
+ status: "ready",
90
+ collections: res.rows,
91
+ emptyText: res.rows.length ? "" : "No categories yet",
92
+ error: "",
93
+ };
46
94
  },
47
95
  },
48
96
  });