@wix/himalaya-cli 0.1490.0 → 0.1491.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.
Files changed (24) hide show
  1. package/dist/cli.mjs +0 -1
  2. package/package.json +2 -2
  3. package/templates/beauty-salon-owner/tier5-src/_shared/owner-hosted-sites.ts +60 -0
  4. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-overview/index.ts +21 -7
  5. package/templates/community-club-owner/tier5-src/_shared/owner-hosted-sites.ts +60 -0
  6. package/templates/community-club-owner/tier5-src/himi-owner-lite-overview/index.ts +21 -7
  7. package/templates/events-meetups-owner/tier5-src/_shared/owner-hosted-sites.ts +60 -0
  8. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-overview/index.ts +21 -7
  9. package/templates/gym-fitness-owner/tier5-src/_shared/owner-hosted-sites.ts +60 -0
  10. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-overview/index.ts +21 -7
  11. package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-hosted-sites.ts +60 -0
  12. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-overview/index.ts +21 -7
  13. package/templates/owner-lite/tier5-src/_shared/owner-hosted-sites.ts +60 -0
  14. package/templates/owner-lite/tier5-src/himi-owner-lite-overview/index.ts +21 -7
  15. package/templates/podcast-creator-owner/tier5-src/_shared/owner-hosted-sites.ts +60 -0
  16. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-overview/index.ts +21 -7
  17. package/templates/portfolio-creative-owner/tier5-src/_shared/owner-hosted-sites.ts +60 -0
  18. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-overview/index.ts +21 -7
  19. package/templates/restaurant-owner/tier5-src/_shared/owner-hosted-sites.ts +60 -0
  20. package/templates/restaurant-owner/tier5-src/himi-owner-lite-overview/index.ts +21 -7
  21. package/templates/retail-store-owner/tier5-src/_shared/owner-hosted-sites.ts +60 -0
  22. package/templates/retail-store-owner/tier5-src/himi-owner-lite-overview/index.ts +21 -7
  23. package/templates/yoga-studio-owner/tier5-src/_shared/owner-hosted-sites.ts +60 -0
  24. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-overview/index.ts +21 -7
package/dist/cli.mjs CHANGED
@@ -22149,7 +22149,6 @@ function resolveIdiom(tokens2, ctx = {}) {
22149
22149
  var IDIOM_CONTEXTS, UNSAFE_KEYS;
22150
22150
  var init_idiom = __esm({
22151
22151
  "../../core/ts/src/idiom.ts"() {
22152
- "use strict";
22153
22152
  IDIOM_CONTEXTS = {
22154
22153
  ios: { native: ["ios"] },
22155
22154
  android: { native: ["material"] },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1490.0",
3
+ "version": "0.1491.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": "cb54373a9c2a773492e66c5579403c0720caa00367681b4479fbc0e3"
74
+ "falconPackageHash": "69ee53fb46bd8285fb455df2eb9a446d954346c0b2f990cfb7e54d09"
75
75
  }
@@ -0,0 +1,60 @@
1
+ // The businesses an EMBEDDING HOST says the signed-in owner owns — or null when there is no
2
+ // such host.
3
+ //
4
+ // Standalone, an owner screen lists businesses itself (`querySites`, account level) and reads
5
+ // each one with `site.forSite(msid)`, which stamps `wix-site-id` on that call. Embedded, the
6
+ // host owns site selection: its data source signs every call for the business the user picked
7
+ // and drops a worker-sent `wix-site-id`, so a fan-out read ONE business N times. A host that
8
+ // opts in supplies its own list instead (`HimalayaOwnedSitesDataSource` on iOS / Android, the
9
+ // `ownedSites` prop on web) and answers `himi.wix.ownedSites` with it; the embed then routes a
10
+ // `site.forSite(msid)` call to the host's per-site call for any id on that list and refuses
11
+ // every other id. So a worker reads the list here and keeps using `site.forSite` unchanged.
12
+ //
13
+ // A rejection is "not hosted": a standalone shell has no handler for the verb, and an older embed
14
+ // predates it. Callers then do exactly what they did before. A host that opted in but cannot list
15
+ // answers `{ error }` instead, which THROWS here: falling back to `querySites` would read the
16
+ // account through the host's single-site signer and fan out to ids the host then refuses.
17
+
18
+ import { dispatch } from "@himalaya/dispatch";
19
+
20
+ export const OWNED_SITES_ACTION = "himi.wix.ownedSites";
21
+
22
+ export interface HostedSite {
23
+ /** The business's metaSiteId — what `site.forSite` takes. */
24
+ id: string;
25
+ name: string;
26
+ logoUrl?: string;
27
+ color?: string;
28
+ }
29
+
30
+ function text(v: unknown): string {
31
+ return typeof v === "string" ? v.trim() : "";
32
+ }
33
+
34
+ /** The host's list, in the host's order; [] is a real answer (an owner with no businesses).
35
+ * null when the verb is unanswered or the answer is not a list. Throws the host's `{ error }`. */
36
+ export async function hostOwnedSites(): Promise<HostedSite[] | null> {
37
+ let value: unknown;
38
+ try {
39
+ value = await dispatch(OWNED_SITES_ACTION);
40
+ } catch {
41
+ return null;
42
+ }
43
+ // Native hosts resolve the payload itself; some web shells wrap it as `{ payload }`.
44
+ const body = value as { sites?: unknown; error?: unknown; payload?: { sites?: unknown; error?: unknown } } | null;
45
+ const error = body?.error ?? body?.payload?.error;
46
+ if (typeof error === "string") throw new Error(error || "The host could not list your businesses");
47
+ const raw = body?.sites ?? body?.payload?.sites;
48
+ if (!Array.isArray(raw)) return null;
49
+ const sites: HostedSite[] = [];
50
+ for (const entry of raw) {
51
+ const e = (entry ?? {}) as Record<string, unknown>;
52
+ const id = text(e.id);
53
+ if (!id) continue;
54
+ const site: HostedSite = { id, name: text(e.name) };
55
+ if (text(e.logoUrl)) site.logoUrl = text(e.logoUrl);
56
+ if (text(e.color)) site.color = text(e.color);
57
+ sites.push(site);
58
+ }
59
+ return sites;
60
+ }
@@ -22,6 +22,11 @@
22
22
  // or one whose mint fails, resolves to `null` and renders as "—" beside the others rather
23
23
  // than throwing the whole screen into its error state. A rollup that disappears because one
24
24
  // of five businesses is unreachable is worse than one that says so in a row.
25
+ //
26
+ // EMBEDDED, THE HOST NAMES THE BUSINESSES. A host that owns site selection (Symphony's Manage
27
+ // Wix) signs each call itself and would drop the per-call `wix-site-id`, so it supplies the
28
+ // owner's sites instead (`hostOwnedSites`) and resolves each `site.forSite(msid)` call against
29
+ // that list. The list replaces rung 1 only; the fan-out below is identical either way.
25
30
 
26
31
  import { defineService } from "@himalaya/state";
27
32
  import { querySites, type WixSite } from "@himalaya/wix-site-list";
@@ -31,6 +36,7 @@ import { ownerLoadError, isOwnerPreviewDemo, ownerDemoFallback } from "../_share
31
36
  import { DEMO_SITES, DEMO_NOTE } from "../_shared/owner-preview-demo.js";
32
37
  import { stashCopy, copyParam } from "../_shared/himi-copy.js";
33
38
  import { pickSite, pickedSite } from "../_shared/owner-site-pick.js";
39
+ import { hostOwnedSites } from "../_shared/owner-hosted-sites.js";
34
40
 
35
41
  /** Mirrors the Home dashboard's "needs attention" definition, so a number here means the
36
42
  * same thing it means after the owner drills into that business. */
@@ -176,16 +182,24 @@ async function attentionCount(msid: string): Promise<number | null> {
176
182
  }
177
183
  }
178
184
 
185
+ /** Rung 1: the host's list when embedded, else the account's own (`querySites`). */
186
+ async function listBusinesses(): Promise<Array<{ id: string; name: string }>> {
187
+ const unnamed = () => copyParam("ownerOverview.unnamedBusiness", "Business");
188
+ const hosted = await hostOwnedSites();
189
+ if (hosted) return hosted.map((h) => ({ id: h.id, name: h.name || unnamed() }));
190
+ const res = await querySites({ query: { cursorPaging: { limit: 50 } } });
191
+ return (res.sites ?? [])
192
+ .filter((r: WixSite) => r?.id)
193
+ .map((r: WixSite) => ({
194
+ id: r.id || "",
195
+ name: r.displayName || (r as { name?: string }).name || unnamed(),
196
+ }));
197
+ }
198
+
179
199
  async function loadRollup(): Promise<OverviewState> {
180
200
  if (isOwnerPreviewDemo()) return demoRollup();
181
201
  try {
182
- const res = await querySites({ query: { cursorPaging: { limit: 50 } } });
183
- const businesses = (res.sites ?? [])
184
- .filter((r: WixSite) => r?.id)
185
- .map((r: WixSite) => ({
186
- id: r.id || "",
187
- name: r.displayName || (r as { name?: string }).name || copyParam("ownerOverview.unnamedBusiness", "Business"),
188
- }));
202
+ const businesses = await listBusinesses();
189
203
  if (businesses.length === 0) return shape({ status: "ready", rows: [] });
190
204
 
191
205
  // THE FAN-OUT. Concurrent, one business each, active scope untouched.
@@ -0,0 +1,60 @@
1
+ // The businesses an EMBEDDING HOST says the signed-in owner owns — or null when there is no
2
+ // such host.
3
+ //
4
+ // Standalone, an owner screen lists businesses itself (`querySites`, account level) and reads
5
+ // each one with `site.forSite(msid)`, which stamps `wix-site-id` on that call. Embedded, the
6
+ // host owns site selection: its data source signs every call for the business the user picked
7
+ // and drops a worker-sent `wix-site-id`, so a fan-out read ONE business N times. A host that
8
+ // opts in supplies its own list instead (`HimalayaOwnedSitesDataSource` on iOS / Android, the
9
+ // `ownedSites` prop on web) and answers `himi.wix.ownedSites` with it; the embed then routes a
10
+ // `site.forSite(msid)` call to the host's per-site call for any id on that list and refuses
11
+ // every other id. So a worker reads the list here and keeps using `site.forSite` unchanged.
12
+ //
13
+ // A rejection is "not hosted": a standalone shell has no handler for the verb, and an older embed
14
+ // predates it. Callers then do exactly what they did before. A host that opted in but cannot list
15
+ // answers `{ error }` instead, which THROWS here: falling back to `querySites` would read the
16
+ // account through the host's single-site signer and fan out to ids the host then refuses.
17
+
18
+ import { dispatch } from "@himalaya/dispatch";
19
+
20
+ export const OWNED_SITES_ACTION = "himi.wix.ownedSites";
21
+
22
+ export interface HostedSite {
23
+ /** The business's metaSiteId — what `site.forSite` takes. */
24
+ id: string;
25
+ name: string;
26
+ logoUrl?: string;
27
+ color?: string;
28
+ }
29
+
30
+ function text(v: unknown): string {
31
+ return typeof v === "string" ? v.trim() : "";
32
+ }
33
+
34
+ /** The host's list, in the host's order; [] is a real answer (an owner with no businesses).
35
+ * null when the verb is unanswered or the answer is not a list. Throws the host's `{ error }`. */
36
+ export async function hostOwnedSites(): Promise<HostedSite[] | null> {
37
+ let value: unknown;
38
+ try {
39
+ value = await dispatch(OWNED_SITES_ACTION);
40
+ } catch {
41
+ return null;
42
+ }
43
+ // Native hosts resolve the payload itself; some web shells wrap it as `{ payload }`.
44
+ const body = value as { sites?: unknown; error?: unknown; payload?: { sites?: unknown; error?: unknown } } | null;
45
+ const error = body?.error ?? body?.payload?.error;
46
+ if (typeof error === "string") throw new Error(error || "The host could not list your businesses");
47
+ const raw = body?.sites ?? body?.payload?.sites;
48
+ if (!Array.isArray(raw)) return null;
49
+ const sites: HostedSite[] = [];
50
+ for (const entry of raw) {
51
+ const e = (entry ?? {}) as Record<string, unknown>;
52
+ const id = text(e.id);
53
+ if (!id) continue;
54
+ const site: HostedSite = { id, name: text(e.name) };
55
+ if (text(e.logoUrl)) site.logoUrl = text(e.logoUrl);
56
+ if (text(e.color)) site.color = text(e.color);
57
+ sites.push(site);
58
+ }
59
+ return sites;
60
+ }
@@ -22,6 +22,11 @@
22
22
  // or one whose mint fails, resolves to `null` and renders as "—" beside the others rather
23
23
  // than throwing the whole screen into its error state. A rollup that disappears because one
24
24
  // of five businesses is unreachable is worse than one that says so in a row.
25
+ //
26
+ // EMBEDDED, THE HOST NAMES THE BUSINESSES. A host that owns site selection (Symphony's Manage
27
+ // Wix) signs each call itself and would drop the per-call `wix-site-id`, so it supplies the
28
+ // owner's sites instead (`hostOwnedSites`) and resolves each `site.forSite(msid)` call against
29
+ // that list. The list replaces rung 1 only; the fan-out below is identical either way.
25
30
 
26
31
  import { defineService } from "@himalaya/state";
27
32
  import { querySites, type WixSite } from "@himalaya/wix-site-list";
@@ -31,6 +36,7 @@ import { ownerLoadError, isOwnerPreviewDemo, ownerDemoFallback } from "../_share
31
36
  import { DEMO_SITES, DEMO_NOTE } from "../_shared/owner-preview-demo.js";
32
37
  import { stashCopy, copyParam } from "../_shared/himi-copy.js";
33
38
  import { pickSite, pickedSite } from "../_shared/owner-site-pick.js";
39
+ import { hostOwnedSites } from "../_shared/owner-hosted-sites.js";
34
40
 
35
41
  /** Mirrors the Home dashboard's "needs attention" definition, so a number here means the
36
42
  * same thing it means after the owner drills into that business. */
@@ -176,16 +182,24 @@ async function attentionCount(msid: string): Promise<number | null> {
176
182
  }
177
183
  }
178
184
 
185
+ /** Rung 1: the host's list when embedded, else the account's own (`querySites`). */
186
+ async function listBusinesses(): Promise<Array<{ id: string; name: string }>> {
187
+ const unnamed = () => copyParam("ownerOverview.unnamedBusiness", "Business");
188
+ const hosted = await hostOwnedSites();
189
+ if (hosted) return hosted.map((h) => ({ id: h.id, name: h.name || unnamed() }));
190
+ const res = await querySites({ query: { cursorPaging: { limit: 50 } } });
191
+ return (res.sites ?? [])
192
+ .filter((r: WixSite) => r?.id)
193
+ .map((r: WixSite) => ({
194
+ id: r.id || "",
195
+ name: r.displayName || (r as { name?: string }).name || unnamed(),
196
+ }));
197
+ }
198
+
179
199
  async function loadRollup(): Promise<OverviewState> {
180
200
  if (isOwnerPreviewDemo()) return demoRollup();
181
201
  try {
182
- const res = await querySites({ query: { cursorPaging: { limit: 50 } } });
183
- const businesses = (res.sites ?? [])
184
- .filter((r: WixSite) => r?.id)
185
- .map((r: WixSite) => ({
186
- id: r.id || "",
187
- name: r.displayName || (r as { name?: string }).name || copyParam("ownerOverview.unnamedBusiness", "Business"),
188
- }));
202
+ const businesses = await listBusinesses();
189
203
  if (businesses.length === 0) return shape({ status: "ready", rows: [] });
190
204
 
191
205
  // THE FAN-OUT. Concurrent, one business each, active scope untouched.
@@ -0,0 +1,60 @@
1
+ // The businesses an EMBEDDING HOST says the signed-in owner owns — or null when there is no
2
+ // such host.
3
+ //
4
+ // Standalone, an owner screen lists businesses itself (`querySites`, account level) and reads
5
+ // each one with `site.forSite(msid)`, which stamps `wix-site-id` on that call. Embedded, the
6
+ // host owns site selection: its data source signs every call for the business the user picked
7
+ // and drops a worker-sent `wix-site-id`, so a fan-out read ONE business N times. A host that
8
+ // opts in supplies its own list instead (`HimalayaOwnedSitesDataSource` on iOS / Android, the
9
+ // `ownedSites` prop on web) and answers `himi.wix.ownedSites` with it; the embed then routes a
10
+ // `site.forSite(msid)` call to the host's per-site call for any id on that list and refuses
11
+ // every other id. So a worker reads the list here and keeps using `site.forSite` unchanged.
12
+ //
13
+ // A rejection is "not hosted": a standalone shell has no handler for the verb, and an older embed
14
+ // predates it. Callers then do exactly what they did before. A host that opted in but cannot list
15
+ // answers `{ error }` instead, which THROWS here: falling back to `querySites` would read the
16
+ // account through the host's single-site signer and fan out to ids the host then refuses.
17
+
18
+ import { dispatch } from "@himalaya/dispatch";
19
+
20
+ export const OWNED_SITES_ACTION = "himi.wix.ownedSites";
21
+
22
+ export interface HostedSite {
23
+ /** The business's metaSiteId — what `site.forSite` takes. */
24
+ id: string;
25
+ name: string;
26
+ logoUrl?: string;
27
+ color?: string;
28
+ }
29
+
30
+ function text(v: unknown): string {
31
+ return typeof v === "string" ? v.trim() : "";
32
+ }
33
+
34
+ /** The host's list, in the host's order; [] is a real answer (an owner with no businesses).
35
+ * null when the verb is unanswered or the answer is not a list. Throws the host's `{ error }`. */
36
+ export async function hostOwnedSites(): Promise<HostedSite[] | null> {
37
+ let value: unknown;
38
+ try {
39
+ value = await dispatch(OWNED_SITES_ACTION);
40
+ } catch {
41
+ return null;
42
+ }
43
+ // Native hosts resolve the payload itself; some web shells wrap it as `{ payload }`.
44
+ const body = value as { sites?: unknown; error?: unknown; payload?: { sites?: unknown; error?: unknown } } | null;
45
+ const error = body?.error ?? body?.payload?.error;
46
+ if (typeof error === "string") throw new Error(error || "The host could not list your businesses");
47
+ const raw = body?.sites ?? body?.payload?.sites;
48
+ if (!Array.isArray(raw)) return null;
49
+ const sites: HostedSite[] = [];
50
+ for (const entry of raw) {
51
+ const e = (entry ?? {}) as Record<string, unknown>;
52
+ const id = text(e.id);
53
+ if (!id) continue;
54
+ const site: HostedSite = { id, name: text(e.name) };
55
+ if (text(e.logoUrl)) site.logoUrl = text(e.logoUrl);
56
+ if (text(e.color)) site.color = text(e.color);
57
+ sites.push(site);
58
+ }
59
+ return sites;
60
+ }
@@ -22,6 +22,11 @@
22
22
  // or one whose mint fails, resolves to `null` and renders as "—" beside the others rather
23
23
  // than throwing the whole screen into its error state. A rollup that disappears because one
24
24
  // of five businesses is unreachable is worse than one that says so in a row.
25
+ //
26
+ // EMBEDDED, THE HOST NAMES THE BUSINESSES. A host that owns site selection (Symphony's Manage
27
+ // Wix) signs each call itself and would drop the per-call `wix-site-id`, so it supplies the
28
+ // owner's sites instead (`hostOwnedSites`) and resolves each `site.forSite(msid)` call against
29
+ // that list. The list replaces rung 1 only; the fan-out below is identical either way.
25
30
 
26
31
  import { defineService } from "@himalaya/state";
27
32
  import { querySites, type WixSite } from "@himalaya/wix-site-list";
@@ -31,6 +36,7 @@ import { ownerLoadError, isOwnerPreviewDemo, ownerDemoFallback } from "../_share
31
36
  import { DEMO_SITES, DEMO_NOTE } from "../_shared/owner-preview-demo.js";
32
37
  import { stashCopy, copyParam } from "../_shared/himi-copy.js";
33
38
  import { pickSite, pickedSite } from "../_shared/owner-site-pick.js";
39
+ import { hostOwnedSites } from "../_shared/owner-hosted-sites.js";
34
40
 
35
41
  /** Mirrors the Home dashboard's "needs attention" definition, so a number here means the
36
42
  * same thing it means after the owner drills into that business. */
@@ -176,16 +182,24 @@ async function attentionCount(msid: string): Promise<number | null> {
176
182
  }
177
183
  }
178
184
 
185
+ /** Rung 1: the host's list when embedded, else the account's own (`querySites`). */
186
+ async function listBusinesses(): Promise<Array<{ id: string; name: string }>> {
187
+ const unnamed = () => copyParam("ownerOverview.unnamedBusiness", "Business");
188
+ const hosted = await hostOwnedSites();
189
+ if (hosted) return hosted.map((h) => ({ id: h.id, name: h.name || unnamed() }));
190
+ const res = await querySites({ query: { cursorPaging: { limit: 50 } } });
191
+ return (res.sites ?? [])
192
+ .filter((r: WixSite) => r?.id)
193
+ .map((r: WixSite) => ({
194
+ id: r.id || "",
195
+ name: r.displayName || (r as { name?: string }).name || unnamed(),
196
+ }));
197
+ }
198
+
179
199
  async function loadRollup(): Promise<OverviewState> {
180
200
  if (isOwnerPreviewDemo()) return demoRollup();
181
201
  try {
182
- const res = await querySites({ query: { cursorPaging: { limit: 50 } } });
183
- const businesses = (res.sites ?? [])
184
- .filter((r: WixSite) => r?.id)
185
- .map((r: WixSite) => ({
186
- id: r.id || "",
187
- name: r.displayName || (r as { name?: string }).name || copyParam("ownerOverview.unnamedBusiness", "Business"),
188
- }));
202
+ const businesses = await listBusinesses();
189
203
  if (businesses.length === 0) return shape({ status: "ready", rows: [] });
190
204
 
191
205
  // THE FAN-OUT. Concurrent, one business each, active scope untouched.
@@ -0,0 +1,60 @@
1
+ // The businesses an EMBEDDING HOST says the signed-in owner owns — or null when there is no
2
+ // such host.
3
+ //
4
+ // Standalone, an owner screen lists businesses itself (`querySites`, account level) and reads
5
+ // each one with `site.forSite(msid)`, which stamps `wix-site-id` on that call. Embedded, the
6
+ // host owns site selection: its data source signs every call for the business the user picked
7
+ // and drops a worker-sent `wix-site-id`, so a fan-out read ONE business N times. A host that
8
+ // opts in supplies its own list instead (`HimalayaOwnedSitesDataSource` on iOS / Android, the
9
+ // `ownedSites` prop on web) and answers `himi.wix.ownedSites` with it; the embed then routes a
10
+ // `site.forSite(msid)` call to the host's per-site call for any id on that list and refuses
11
+ // every other id. So a worker reads the list here and keeps using `site.forSite` unchanged.
12
+ //
13
+ // A rejection is "not hosted": a standalone shell has no handler for the verb, and an older embed
14
+ // predates it. Callers then do exactly what they did before. A host that opted in but cannot list
15
+ // answers `{ error }` instead, which THROWS here: falling back to `querySites` would read the
16
+ // account through the host's single-site signer and fan out to ids the host then refuses.
17
+
18
+ import { dispatch } from "@himalaya/dispatch";
19
+
20
+ export const OWNED_SITES_ACTION = "himi.wix.ownedSites";
21
+
22
+ export interface HostedSite {
23
+ /** The business's metaSiteId — what `site.forSite` takes. */
24
+ id: string;
25
+ name: string;
26
+ logoUrl?: string;
27
+ color?: string;
28
+ }
29
+
30
+ function text(v: unknown): string {
31
+ return typeof v === "string" ? v.trim() : "";
32
+ }
33
+
34
+ /** The host's list, in the host's order; [] is a real answer (an owner with no businesses).
35
+ * null when the verb is unanswered or the answer is not a list. Throws the host's `{ error }`. */
36
+ export async function hostOwnedSites(): Promise<HostedSite[] | null> {
37
+ let value: unknown;
38
+ try {
39
+ value = await dispatch(OWNED_SITES_ACTION);
40
+ } catch {
41
+ return null;
42
+ }
43
+ // Native hosts resolve the payload itself; some web shells wrap it as `{ payload }`.
44
+ const body = value as { sites?: unknown; error?: unknown; payload?: { sites?: unknown; error?: unknown } } | null;
45
+ const error = body?.error ?? body?.payload?.error;
46
+ if (typeof error === "string") throw new Error(error || "The host could not list your businesses");
47
+ const raw = body?.sites ?? body?.payload?.sites;
48
+ if (!Array.isArray(raw)) return null;
49
+ const sites: HostedSite[] = [];
50
+ for (const entry of raw) {
51
+ const e = (entry ?? {}) as Record<string, unknown>;
52
+ const id = text(e.id);
53
+ if (!id) continue;
54
+ const site: HostedSite = { id, name: text(e.name) };
55
+ if (text(e.logoUrl)) site.logoUrl = text(e.logoUrl);
56
+ if (text(e.color)) site.color = text(e.color);
57
+ sites.push(site);
58
+ }
59
+ return sites;
60
+ }
@@ -22,6 +22,11 @@
22
22
  // or one whose mint fails, resolves to `null` and renders as "—" beside the others rather
23
23
  // than throwing the whole screen into its error state. A rollup that disappears because one
24
24
  // of five businesses is unreachable is worse than one that says so in a row.
25
+ //
26
+ // EMBEDDED, THE HOST NAMES THE BUSINESSES. A host that owns site selection (Symphony's Manage
27
+ // Wix) signs each call itself and would drop the per-call `wix-site-id`, so it supplies the
28
+ // owner's sites instead (`hostOwnedSites`) and resolves each `site.forSite(msid)` call against
29
+ // that list. The list replaces rung 1 only; the fan-out below is identical either way.
25
30
 
26
31
  import { defineService } from "@himalaya/state";
27
32
  import { querySites, type WixSite } from "@himalaya/wix-site-list";
@@ -31,6 +36,7 @@ import { ownerLoadError, isOwnerPreviewDemo, ownerDemoFallback } from "../_share
31
36
  import { DEMO_SITES, DEMO_NOTE } from "../_shared/owner-preview-demo.js";
32
37
  import { stashCopy, copyParam } from "../_shared/himi-copy.js";
33
38
  import { pickSite, pickedSite } from "../_shared/owner-site-pick.js";
39
+ import { hostOwnedSites } from "../_shared/owner-hosted-sites.js";
34
40
 
35
41
  /** Mirrors the Home dashboard's "needs attention" definition, so a number here means the
36
42
  * same thing it means after the owner drills into that business. */
@@ -176,16 +182,24 @@ async function attentionCount(msid: string): Promise<number | null> {
176
182
  }
177
183
  }
178
184
 
185
+ /** Rung 1: the host's list when embedded, else the account's own (`querySites`). */
186
+ async function listBusinesses(): Promise<Array<{ id: string; name: string }>> {
187
+ const unnamed = () => copyParam("ownerOverview.unnamedBusiness", "Business");
188
+ const hosted = await hostOwnedSites();
189
+ if (hosted) return hosted.map((h) => ({ id: h.id, name: h.name || unnamed() }));
190
+ const res = await querySites({ query: { cursorPaging: { limit: 50 } } });
191
+ return (res.sites ?? [])
192
+ .filter((r: WixSite) => r?.id)
193
+ .map((r: WixSite) => ({
194
+ id: r.id || "",
195
+ name: r.displayName || (r as { name?: string }).name || unnamed(),
196
+ }));
197
+ }
198
+
179
199
  async function loadRollup(): Promise<OverviewState> {
180
200
  if (isOwnerPreviewDemo()) return demoRollup();
181
201
  try {
182
- const res = await querySites({ query: { cursorPaging: { limit: 50 } } });
183
- const businesses = (res.sites ?? [])
184
- .filter((r: WixSite) => r?.id)
185
- .map((r: WixSite) => ({
186
- id: r.id || "",
187
- name: r.displayName || (r as { name?: string }).name || copyParam("ownerOverview.unnamedBusiness", "Business"),
188
- }));
202
+ const businesses = await listBusinesses();
189
203
  if (businesses.length === 0) return shape({ status: "ready", rows: [] });
190
204
 
191
205
  // THE FAN-OUT. Concurrent, one business each, active scope untouched.
@@ -0,0 +1,60 @@
1
+ // The businesses an EMBEDDING HOST says the signed-in owner owns — or null when there is no
2
+ // such host.
3
+ //
4
+ // Standalone, an owner screen lists businesses itself (`querySites`, account level) and reads
5
+ // each one with `site.forSite(msid)`, which stamps `wix-site-id` on that call. Embedded, the
6
+ // host owns site selection: its data source signs every call for the business the user picked
7
+ // and drops a worker-sent `wix-site-id`, so a fan-out read ONE business N times. A host that
8
+ // opts in supplies its own list instead (`HimalayaOwnedSitesDataSource` on iOS / Android, the
9
+ // `ownedSites` prop on web) and answers `himi.wix.ownedSites` with it; the embed then routes a
10
+ // `site.forSite(msid)` call to the host's per-site call for any id on that list and refuses
11
+ // every other id. So a worker reads the list here and keeps using `site.forSite` unchanged.
12
+ //
13
+ // A rejection is "not hosted": a standalone shell has no handler for the verb, and an older embed
14
+ // predates it. Callers then do exactly what they did before. A host that opted in but cannot list
15
+ // answers `{ error }` instead, which THROWS here: falling back to `querySites` would read the
16
+ // account through the host's single-site signer and fan out to ids the host then refuses.
17
+
18
+ import { dispatch } from "@himalaya/dispatch";
19
+
20
+ export const OWNED_SITES_ACTION = "himi.wix.ownedSites";
21
+
22
+ export interface HostedSite {
23
+ /** The business's metaSiteId — what `site.forSite` takes. */
24
+ id: string;
25
+ name: string;
26
+ logoUrl?: string;
27
+ color?: string;
28
+ }
29
+
30
+ function text(v: unknown): string {
31
+ return typeof v === "string" ? v.trim() : "";
32
+ }
33
+
34
+ /** The host's list, in the host's order; [] is a real answer (an owner with no businesses).
35
+ * null when the verb is unanswered or the answer is not a list. Throws the host's `{ error }`. */
36
+ export async function hostOwnedSites(): Promise<HostedSite[] | null> {
37
+ let value: unknown;
38
+ try {
39
+ value = await dispatch(OWNED_SITES_ACTION);
40
+ } catch {
41
+ return null;
42
+ }
43
+ // Native hosts resolve the payload itself; some web shells wrap it as `{ payload }`.
44
+ const body = value as { sites?: unknown; error?: unknown; payload?: { sites?: unknown; error?: unknown } } | null;
45
+ const error = body?.error ?? body?.payload?.error;
46
+ if (typeof error === "string") throw new Error(error || "The host could not list your businesses");
47
+ const raw = body?.sites ?? body?.payload?.sites;
48
+ if (!Array.isArray(raw)) return null;
49
+ const sites: HostedSite[] = [];
50
+ for (const entry of raw) {
51
+ const e = (entry ?? {}) as Record<string, unknown>;
52
+ const id = text(e.id);
53
+ if (!id) continue;
54
+ const site: HostedSite = { id, name: text(e.name) };
55
+ if (text(e.logoUrl)) site.logoUrl = text(e.logoUrl);
56
+ if (text(e.color)) site.color = text(e.color);
57
+ sites.push(site);
58
+ }
59
+ return sites;
60
+ }
@@ -22,6 +22,11 @@
22
22
  // or one whose mint fails, resolves to `null` and renders as "—" beside the others rather
23
23
  // than throwing the whole screen into its error state. A rollup that disappears because one
24
24
  // of five businesses is unreachable is worse than one that says so in a row.
25
+ //
26
+ // EMBEDDED, THE HOST NAMES THE BUSINESSES. A host that owns site selection (Symphony's Manage
27
+ // Wix) signs each call itself and would drop the per-call `wix-site-id`, so it supplies the
28
+ // owner's sites instead (`hostOwnedSites`) and resolves each `site.forSite(msid)` call against
29
+ // that list. The list replaces rung 1 only; the fan-out below is identical either way.
25
30
 
26
31
  import { defineService } from "@himalaya/state";
27
32
  import { querySites, type WixSite } from "@himalaya/wix-site-list";
@@ -31,6 +36,7 @@ import { ownerLoadError, isOwnerPreviewDemo, ownerDemoFallback } from "../_share
31
36
  import { DEMO_SITES, DEMO_NOTE } from "../_shared/owner-preview-demo.js";
32
37
  import { stashCopy, copyParam } from "../_shared/himi-copy.js";
33
38
  import { pickSite, pickedSite } from "../_shared/owner-site-pick.js";
39
+ import { hostOwnedSites } from "../_shared/owner-hosted-sites.js";
34
40
 
35
41
  /** Mirrors the Home dashboard's "needs attention" definition, so a number here means the
36
42
  * same thing it means after the owner drills into that business. */
@@ -176,16 +182,24 @@ async function attentionCount(msid: string): Promise<number | null> {
176
182
  }
177
183
  }
178
184
 
185
+ /** Rung 1: the host's list when embedded, else the account's own (`querySites`). */
186
+ async function listBusinesses(): Promise<Array<{ id: string; name: string }>> {
187
+ const unnamed = () => copyParam("ownerOverview.unnamedBusiness", "Business");
188
+ const hosted = await hostOwnedSites();
189
+ if (hosted) return hosted.map((h) => ({ id: h.id, name: h.name || unnamed() }));
190
+ const res = await querySites({ query: { cursorPaging: { limit: 50 } } });
191
+ return (res.sites ?? [])
192
+ .filter((r: WixSite) => r?.id)
193
+ .map((r: WixSite) => ({
194
+ id: r.id || "",
195
+ name: r.displayName || (r as { name?: string }).name || unnamed(),
196
+ }));
197
+ }
198
+
179
199
  async function loadRollup(): Promise<OverviewState> {
180
200
  if (isOwnerPreviewDemo()) return demoRollup();
181
201
  try {
182
- const res = await querySites({ query: { cursorPaging: { limit: 50 } } });
183
- const businesses = (res.sites ?? [])
184
- .filter((r: WixSite) => r?.id)
185
- .map((r: WixSite) => ({
186
- id: r.id || "",
187
- name: r.displayName || (r as { name?: string }).name || copyParam("ownerOverview.unnamedBusiness", "Business"),
188
- }));
202
+ const businesses = await listBusinesses();
189
203
  if (businesses.length === 0) return shape({ status: "ready", rows: [] });
190
204
 
191
205
  // THE FAN-OUT. Concurrent, one business each, active scope untouched.
@@ -0,0 +1,60 @@
1
+ // The businesses an EMBEDDING HOST says the signed-in owner owns — or null when there is no
2
+ // such host.
3
+ //
4
+ // Standalone, an owner screen lists businesses itself (`querySites`, account level) and reads
5
+ // each one with `site.forSite(msid)`, which stamps `wix-site-id` on that call. Embedded, the
6
+ // host owns site selection: its data source signs every call for the business the user picked
7
+ // and drops a worker-sent `wix-site-id`, so a fan-out read ONE business N times. A host that
8
+ // opts in supplies its own list instead (`HimalayaOwnedSitesDataSource` on iOS / Android, the
9
+ // `ownedSites` prop on web) and answers `himi.wix.ownedSites` with it; the embed then routes a
10
+ // `site.forSite(msid)` call to the host's per-site call for any id on that list and refuses
11
+ // every other id. So a worker reads the list here and keeps using `site.forSite` unchanged.
12
+ //
13
+ // A rejection is "not hosted": a standalone shell has no handler for the verb, and an older embed
14
+ // predates it. Callers then do exactly what they did before. A host that opted in but cannot list
15
+ // answers `{ error }` instead, which THROWS here: falling back to `querySites` would read the
16
+ // account through the host's single-site signer and fan out to ids the host then refuses.
17
+
18
+ import { dispatch } from "@himalaya/dispatch";
19
+
20
+ export const OWNED_SITES_ACTION = "himi.wix.ownedSites";
21
+
22
+ export interface HostedSite {
23
+ /** The business's metaSiteId — what `site.forSite` takes. */
24
+ id: string;
25
+ name: string;
26
+ logoUrl?: string;
27
+ color?: string;
28
+ }
29
+
30
+ function text(v: unknown): string {
31
+ return typeof v === "string" ? v.trim() : "";
32
+ }
33
+
34
+ /** The host's list, in the host's order; [] is a real answer (an owner with no businesses).
35
+ * null when the verb is unanswered or the answer is not a list. Throws the host's `{ error }`. */
36
+ export async function hostOwnedSites(): Promise<HostedSite[] | null> {
37
+ let value: unknown;
38
+ try {
39
+ value = await dispatch(OWNED_SITES_ACTION);
40
+ } catch {
41
+ return null;
42
+ }
43
+ // Native hosts resolve the payload itself; some web shells wrap it as `{ payload }`.
44
+ const body = value as { sites?: unknown; error?: unknown; payload?: { sites?: unknown; error?: unknown } } | null;
45
+ const error = body?.error ?? body?.payload?.error;
46
+ if (typeof error === "string") throw new Error(error || "The host could not list your businesses");
47
+ const raw = body?.sites ?? body?.payload?.sites;
48
+ if (!Array.isArray(raw)) return null;
49
+ const sites: HostedSite[] = [];
50
+ for (const entry of raw) {
51
+ const e = (entry ?? {}) as Record<string, unknown>;
52
+ const id = text(e.id);
53
+ if (!id) continue;
54
+ const site: HostedSite = { id, name: text(e.name) };
55
+ if (text(e.logoUrl)) site.logoUrl = text(e.logoUrl);
56
+ if (text(e.color)) site.color = text(e.color);
57
+ sites.push(site);
58
+ }
59
+ return sites;
60
+ }
@@ -22,6 +22,11 @@
22
22
  // or one whose mint fails, resolves to `null` and renders as "—" beside the others rather
23
23
  // than throwing the whole screen into its error state. A rollup that disappears because one
24
24
  // of five businesses is unreachable is worse than one that says so in a row.
25
+ //
26
+ // EMBEDDED, THE HOST NAMES THE BUSINESSES. A host that owns site selection (Symphony's Manage
27
+ // Wix) signs each call itself and would drop the per-call `wix-site-id`, so it supplies the
28
+ // owner's sites instead (`hostOwnedSites`) and resolves each `site.forSite(msid)` call against
29
+ // that list. The list replaces rung 1 only; the fan-out below is identical either way.
25
30
 
26
31
  import { defineService } from "@himalaya/state";
27
32
  import { querySites, type WixSite } from "@himalaya/wix-site-list";
@@ -31,6 +36,7 @@ import { ownerLoadError, isOwnerPreviewDemo, ownerDemoFallback } from "../_share
31
36
  import { DEMO_SITES, DEMO_NOTE } from "../_shared/owner-preview-demo.js";
32
37
  import { stashCopy, copyParam } from "../_shared/himi-copy.js";
33
38
  import { pickSite, pickedSite } from "../_shared/owner-site-pick.js";
39
+ import { hostOwnedSites } from "../_shared/owner-hosted-sites.js";
34
40
 
35
41
  /** Mirrors the Home dashboard's "needs attention" definition, so a number here means the
36
42
  * same thing it means after the owner drills into that business. */
@@ -176,16 +182,24 @@ async function attentionCount(msid: string): Promise<number | null> {
176
182
  }
177
183
  }
178
184
 
185
+ /** Rung 1: the host's list when embedded, else the account's own (`querySites`). */
186
+ async function listBusinesses(): Promise<Array<{ id: string; name: string }>> {
187
+ const unnamed = () => copyParam("ownerOverview.unnamedBusiness", "Business");
188
+ const hosted = await hostOwnedSites();
189
+ if (hosted) return hosted.map((h) => ({ id: h.id, name: h.name || unnamed() }));
190
+ const res = await querySites({ query: { cursorPaging: { limit: 50 } } });
191
+ return (res.sites ?? [])
192
+ .filter((r: WixSite) => r?.id)
193
+ .map((r: WixSite) => ({
194
+ id: r.id || "",
195
+ name: r.displayName || (r as { name?: string }).name || unnamed(),
196
+ }));
197
+ }
198
+
179
199
  async function loadRollup(): Promise<OverviewState> {
180
200
  if (isOwnerPreviewDemo()) return demoRollup();
181
201
  try {
182
- const res = await querySites({ query: { cursorPaging: { limit: 50 } } });
183
- const businesses = (res.sites ?? [])
184
- .filter((r: WixSite) => r?.id)
185
- .map((r: WixSite) => ({
186
- id: r.id || "",
187
- name: r.displayName || (r as { name?: string }).name || copyParam("ownerOverview.unnamedBusiness", "Business"),
188
- }));
202
+ const businesses = await listBusinesses();
189
203
  if (businesses.length === 0) return shape({ status: "ready", rows: [] });
190
204
 
191
205
  // THE FAN-OUT. Concurrent, one business each, active scope untouched.
@@ -0,0 +1,60 @@
1
+ // The businesses an EMBEDDING HOST says the signed-in owner owns — or null when there is no
2
+ // such host.
3
+ //
4
+ // Standalone, an owner screen lists businesses itself (`querySites`, account level) and reads
5
+ // each one with `site.forSite(msid)`, which stamps `wix-site-id` on that call. Embedded, the
6
+ // host owns site selection: its data source signs every call for the business the user picked
7
+ // and drops a worker-sent `wix-site-id`, so a fan-out read ONE business N times. A host that
8
+ // opts in supplies its own list instead (`HimalayaOwnedSitesDataSource` on iOS / Android, the
9
+ // `ownedSites` prop on web) and answers `himi.wix.ownedSites` with it; the embed then routes a
10
+ // `site.forSite(msid)` call to the host's per-site call for any id on that list and refuses
11
+ // every other id. So a worker reads the list here and keeps using `site.forSite` unchanged.
12
+ //
13
+ // A rejection is "not hosted": a standalone shell has no handler for the verb, and an older embed
14
+ // predates it. Callers then do exactly what they did before. A host that opted in but cannot list
15
+ // answers `{ error }` instead, which THROWS here: falling back to `querySites` would read the
16
+ // account through the host's single-site signer and fan out to ids the host then refuses.
17
+
18
+ import { dispatch } from "@himalaya/dispatch";
19
+
20
+ export const OWNED_SITES_ACTION = "himi.wix.ownedSites";
21
+
22
+ export interface HostedSite {
23
+ /** The business's metaSiteId — what `site.forSite` takes. */
24
+ id: string;
25
+ name: string;
26
+ logoUrl?: string;
27
+ color?: string;
28
+ }
29
+
30
+ function text(v: unknown): string {
31
+ return typeof v === "string" ? v.trim() : "";
32
+ }
33
+
34
+ /** The host's list, in the host's order; [] is a real answer (an owner with no businesses).
35
+ * null when the verb is unanswered or the answer is not a list. Throws the host's `{ error }`. */
36
+ export async function hostOwnedSites(): Promise<HostedSite[] | null> {
37
+ let value: unknown;
38
+ try {
39
+ value = await dispatch(OWNED_SITES_ACTION);
40
+ } catch {
41
+ return null;
42
+ }
43
+ // Native hosts resolve the payload itself; some web shells wrap it as `{ payload }`.
44
+ const body = value as { sites?: unknown; error?: unknown; payload?: { sites?: unknown; error?: unknown } } | null;
45
+ const error = body?.error ?? body?.payload?.error;
46
+ if (typeof error === "string") throw new Error(error || "The host could not list your businesses");
47
+ const raw = body?.sites ?? body?.payload?.sites;
48
+ if (!Array.isArray(raw)) return null;
49
+ const sites: HostedSite[] = [];
50
+ for (const entry of raw) {
51
+ const e = (entry ?? {}) as Record<string, unknown>;
52
+ const id = text(e.id);
53
+ if (!id) continue;
54
+ const site: HostedSite = { id, name: text(e.name) };
55
+ if (text(e.logoUrl)) site.logoUrl = text(e.logoUrl);
56
+ if (text(e.color)) site.color = text(e.color);
57
+ sites.push(site);
58
+ }
59
+ return sites;
60
+ }
@@ -22,6 +22,11 @@
22
22
  // or one whose mint fails, resolves to `null` and renders as "—" beside the others rather
23
23
  // than throwing the whole screen into its error state. A rollup that disappears because one
24
24
  // of five businesses is unreachable is worse than one that says so in a row.
25
+ //
26
+ // EMBEDDED, THE HOST NAMES THE BUSINESSES. A host that owns site selection (Symphony's Manage
27
+ // Wix) signs each call itself and would drop the per-call `wix-site-id`, so it supplies the
28
+ // owner's sites instead (`hostOwnedSites`) and resolves each `site.forSite(msid)` call against
29
+ // that list. The list replaces rung 1 only; the fan-out below is identical either way.
25
30
 
26
31
  import { defineService } from "@himalaya/state";
27
32
  import { querySites, type WixSite } from "@himalaya/wix-site-list";
@@ -31,6 +36,7 @@ import { ownerLoadError, isOwnerPreviewDemo, ownerDemoFallback } from "../_share
31
36
  import { DEMO_SITES, DEMO_NOTE } from "../_shared/owner-preview-demo.js";
32
37
  import { stashCopy, copyParam } from "../_shared/himi-copy.js";
33
38
  import { pickSite, pickedSite } from "../_shared/owner-site-pick.js";
39
+ import { hostOwnedSites } from "../_shared/owner-hosted-sites.js";
34
40
 
35
41
  /** Mirrors the Home dashboard's "needs attention" definition, so a number here means the
36
42
  * same thing it means after the owner drills into that business. */
@@ -176,16 +182,24 @@ async function attentionCount(msid: string): Promise<number | null> {
176
182
  }
177
183
  }
178
184
 
185
+ /** Rung 1: the host's list when embedded, else the account's own (`querySites`). */
186
+ async function listBusinesses(): Promise<Array<{ id: string; name: string }>> {
187
+ const unnamed = () => copyParam("ownerOverview.unnamedBusiness", "Business");
188
+ const hosted = await hostOwnedSites();
189
+ if (hosted) return hosted.map((h) => ({ id: h.id, name: h.name || unnamed() }));
190
+ const res = await querySites({ query: { cursorPaging: { limit: 50 } } });
191
+ return (res.sites ?? [])
192
+ .filter((r: WixSite) => r?.id)
193
+ .map((r: WixSite) => ({
194
+ id: r.id || "",
195
+ name: r.displayName || (r as { name?: string }).name || unnamed(),
196
+ }));
197
+ }
198
+
179
199
  async function loadRollup(): Promise<OverviewState> {
180
200
  if (isOwnerPreviewDemo()) return demoRollup();
181
201
  try {
182
- const res = await querySites({ query: { cursorPaging: { limit: 50 } } });
183
- const businesses = (res.sites ?? [])
184
- .filter((r: WixSite) => r?.id)
185
- .map((r: WixSite) => ({
186
- id: r.id || "",
187
- name: r.displayName || (r as { name?: string }).name || copyParam("ownerOverview.unnamedBusiness", "Business"),
188
- }));
202
+ const businesses = await listBusinesses();
189
203
  if (businesses.length === 0) return shape({ status: "ready", rows: [] });
190
204
 
191
205
  // THE FAN-OUT. Concurrent, one business each, active scope untouched.
@@ -0,0 +1,60 @@
1
+ // The businesses an EMBEDDING HOST says the signed-in owner owns — or null when there is no
2
+ // such host.
3
+ //
4
+ // Standalone, an owner screen lists businesses itself (`querySites`, account level) and reads
5
+ // each one with `site.forSite(msid)`, which stamps `wix-site-id` on that call. Embedded, the
6
+ // host owns site selection: its data source signs every call for the business the user picked
7
+ // and drops a worker-sent `wix-site-id`, so a fan-out read ONE business N times. A host that
8
+ // opts in supplies its own list instead (`HimalayaOwnedSitesDataSource` on iOS / Android, the
9
+ // `ownedSites` prop on web) and answers `himi.wix.ownedSites` with it; the embed then routes a
10
+ // `site.forSite(msid)` call to the host's per-site call for any id on that list and refuses
11
+ // every other id. So a worker reads the list here and keeps using `site.forSite` unchanged.
12
+ //
13
+ // A rejection is "not hosted": a standalone shell has no handler for the verb, and an older embed
14
+ // predates it. Callers then do exactly what they did before. A host that opted in but cannot list
15
+ // answers `{ error }` instead, which THROWS here: falling back to `querySites` would read the
16
+ // account through the host's single-site signer and fan out to ids the host then refuses.
17
+
18
+ import { dispatch } from "@himalaya/dispatch";
19
+
20
+ export const OWNED_SITES_ACTION = "himi.wix.ownedSites";
21
+
22
+ export interface HostedSite {
23
+ /** The business's metaSiteId — what `site.forSite` takes. */
24
+ id: string;
25
+ name: string;
26
+ logoUrl?: string;
27
+ color?: string;
28
+ }
29
+
30
+ function text(v: unknown): string {
31
+ return typeof v === "string" ? v.trim() : "";
32
+ }
33
+
34
+ /** The host's list, in the host's order; [] is a real answer (an owner with no businesses).
35
+ * null when the verb is unanswered or the answer is not a list. Throws the host's `{ error }`. */
36
+ export async function hostOwnedSites(): Promise<HostedSite[] | null> {
37
+ let value: unknown;
38
+ try {
39
+ value = await dispatch(OWNED_SITES_ACTION);
40
+ } catch {
41
+ return null;
42
+ }
43
+ // Native hosts resolve the payload itself; some web shells wrap it as `{ payload }`.
44
+ const body = value as { sites?: unknown; error?: unknown; payload?: { sites?: unknown; error?: unknown } } | null;
45
+ const error = body?.error ?? body?.payload?.error;
46
+ if (typeof error === "string") throw new Error(error || "The host could not list your businesses");
47
+ const raw = body?.sites ?? body?.payload?.sites;
48
+ if (!Array.isArray(raw)) return null;
49
+ const sites: HostedSite[] = [];
50
+ for (const entry of raw) {
51
+ const e = (entry ?? {}) as Record<string, unknown>;
52
+ const id = text(e.id);
53
+ if (!id) continue;
54
+ const site: HostedSite = { id, name: text(e.name) };
55
+ if (text(e.logoUrl)) site.logoUrl = text(e.logoUrl);
56
+ if (text(e.color)) site.color = text(e.color);
57
+ sites.push(site);
58
+ }
59
+ return sites;
60
+ }
@@ -22,6 +22,11 @@
22
22
  // or one whose mint fails, resolves to `null` and renders as "—" beside the others rather
23
23
  // than throwing the whole screen into its error state. A rollup that disappears because one
24
24
  // of five businesses is unreachable is worse than one that says so in a row.
25
+ //
26
+ // EMBEDDED, THE HOST NAMES THE BUSINESSES. A host that owns site selection (Symphony's Manage
27
+ // Wix) signs each call itself and would drop the per-call `wix-site-id`, so it supplies the
28
+ // owner's sites instead (`hostOwnedSites`) and resolves each `site.forSite(msid)` call against
29
+ // that list. The list replaces rung 1 only; the fan-out below is identical either way.
25
30
 
26
31
  import { defineService } from "@himalaya/state";
27
32
  import { querySites, type WixSite } from "@himalaya/wix-site-list";
@@ -31,6 +36,7 @@ import { ownerLoadError, isOwnerPreviewDemo, ownerDemoFallback } from "../_share
31
36
  import { DEMO_SITES, DEMO_NOTE } from "../_shared/owner-preview-demo.js";
32
37
  import { stashCopy, copyParam } from "../_shared/himi-copy.js";
33
38
  import { pickSite, pickedSite } from "../_shared/owner-site-pick.js";
39
+ import { hostOwnedSites } from "../_shared/owner-hosted-sites.js";
34
40
 
35
41
  /** Mirrors the Home dashboard's "needs attention" definition, so a number here means the
36
42
  * same thing it means after the owner drills into that business. */
@@ -176,16 +182,24 @@ async function attentionCount(msid: string): Promise<number | null> {
176
182
  }
177
183
  }
178
184
 
185
+ /** Rung 1: the host's list when embedded, else the account's own (`querySites`). */
186
+ async function listBusinesses(): Promise<Array<{ id: string; name: string }>> {
187
+ const unnamed = () => copyParam("ownerOverview.unnamedBusiness", "Business");
188
+ const hosted = await hostOwnedSites();
189
+ if (hosted) return hosted.map((h) => ({ id: h.id, name: h.name || unnamed() }));
190
+ const res = await querySites({ query: { cursorPaging: { limit: 50 } } });
191
+ return (res.sites ?? [])
192
+ .filter((r: WixSite) => r?.id)
193
+ .map((r: WixSite) => ({
194
+ id: r.id || "",
195
+ name: r.displayName || (r as { name?: string }).name || unnamed(),
196
+ }));
197
+ }
198
+
179
199
  async function loadRollup(): Promise<OverviewState> {
180
200
  if (isOwnerPreviewDemo()) return demoRollup();
181
201
  try {
182
- const res = await querySites({ query: { cursorPaging: { limit: 50 } } });
183
- const businesses = (res.sites ?? [])
184
- .filter((r: WixSite) => r?.id)
185
- .map((r: WixSite) => ({
186
- id: r.id || "",
187
- name: r.displayName || (r as { name?: string }).name || copyParam("ownerOverview.unnamedBusiness", "Business"),
188
- }));
202
+ const businesses = await listBusinesses();
189
203
  if (businesses.length === 0) return shape({ status: "ready", rows: [] });
190
204
 
191
205
  // THE FAN-OUT. Concurrent, one business each, active scope untouched.
@@ -0,0 +1,60 @@
1
+ // The businesses an EMBEDDING HOST says the signed-in owner owns — or null when there is no
2
+ // such host.
3
+ //
4
+ // Standalone, an owner screen lists businesses itself (`querySites`, account level) and reads
5
+ // each one with `site.forSite(msid)`, which stamps `wix-site-id` on that call. Embedded, the
6
+ // host owns site selection: its data source signs every call for the business the user picked
7
+ // and drops a worker-sent `wix-site-id`, so a fan-out read ONE business N times. A host that
8
+ // opts in supplies its own list instead (`HimalayaOwnedSitesDataSource` on iOS / Android, the
9
+ // `ownedSites` prop on web) and answers `himi.wix.ownedSites` with it; the embed then routes a
10
+ // `site.forSite(msid)` call to the host's per-site call for any id on that list and refuses
11
+ // every other id. So a worker reads the list here and keeps using `site.forSite` unchanged.
12
+ //
13
+ // A rejection is "not hosted": a standalone shell has no handler for the verb, and an older embed
14
+ // predates it. Callers then do exactly what they did before. A host that opted in but cannot list
15
+ // answers `{ error }` instead, which THROWS here: falling back to `querySites` would read the
16
+ // account through the host's single-site signer and fan out to ids the host then refuses.
17
+
18
+ import { dispatch } from "@himalaya/dispatch";
19
+
20
+ export const OWNED_SITES_ACTION = "himi.wix.ownedSites";
21
+
22
+ export interface HostedSite {
23
+ /** The business's metaSiteId — what `site.forSite` takes. */
24
+ id: string;
25
+ name: string;
26
+ logoUrl?: string;
27
+ color?: string;
28
+ }
29
+
30
+ function text(v: unknown): string {
31
+ return typeof v === "string" ? v.trim() : "";
32
+ }
33
+
34
+ /** The host's list, in the host's order; [] is a real answer (an owner with no businesses).
35
+ * null when the verb is unanswered or the answer is not a list. Throws the host's `{ error }`. */
36
+ export async function hostOwnedSites(): Promise<HostedSite[] | null> {
37
+ let value: unknown;
38
+ try {
39
+ value = await dispatch(OWNED_SITES_ACTION);
40
+ } catch {
41
+ return null;
42
+ }
43
+ // Native hosts resolve the payload itself; some web shells wrap it as `{ payload }`.
44
+ const body = value as { sites?: unknown; error?: unknown; payload?: { sites?: unknown; error?: unknown } } | null;
45
+ const error = body?.error ?? body?.payload?.error;
46
+ if (typeof error === "string") throw new Error(error || "The host could not list your businesses");
47
+ const raw = body?.sites ?? body?.payload?.sites;
48
+ if (!Array.isArray(raw)) return null;
49
+ const sites: HostedSite[] = [];
50
+ for (const entry of raw) {
51
+ const e = (entry ?? {}) as Record<string, unknown>;
52
+ const id = text(e.id);
53
+ if (!id) continue;
54
+ const site: HostedSite = { id, name: text(e.name) };
55
+ if (text(e.logoUrl)) site.logoUrl = text(e.logoUrl);
56
+ if (text(e.color)) site.color = text(e.color);
57
+ sites.push(site);
58
+ }
59
+ return sites;
60
+ }
@@ -22,6 +22,11 @@
22
22
  // or one whose mint fails, resolves to `null` and renders as "—" beside the others rather
23
23
  // than throwing the whole screen into its error state. A rollup that disappears because one
24
24
  // of five businesses is unreachable is worse than one that says so in a row.
25
+ //
26
+ // EMBEDDED, THE HOST NAMES THE BUSINESSES. A host that owns site selection (Symphony's Manage
27
+ // Wix) signs each call itself and would drop the per-call `wix-site-id`, so it supplies the
28
+ // owner's sites instead (`hostOwnedSites`) and resolves each `site.forSite(msid)` call against
29
+ // that list. The list replaces rung 1 only; the fan-out below is identical either way.
25
30
 
26
31
  import { defineService } from "@himalaya/state";
27
32
  import { querySites, type WixSite } from "@himalaya/wix-site-list";
@@ -31,6 +36,7 @@ import { ownerLoadError, isOwnerPreviewDemo, ownerDemoFallback } from "../_share
31
36
  import { DEMO_SITES, DEMO_NOTE } from "../_shared/owner-preview-demo.js";
32
37
  import { stashCopy, copyParam } from "../_shared/himi-copy.js";
33
38
  import { pickSite, pickedSite } from "../_shared/owner-site-pick.js";
39
+ import { hostOwnedSites } from "../_shared/owner-hosted-sites.js";
34
40
 
35
41
  /** Mirrors the Home dashboard's "needs attention" definition, so a number here means the
36
42
  * same thing it means after the owner drills into that business. */
@@ -176,16 +182,24 @@ async function attentionCount(msid: string): Promise<number | null> {
176
182
  }
177
183
  }
178
184
 
185
+ /** Rung 1: the host's list when embedded, else the account's own (`querySites`). */
186
+ async function listBusinesses(): Promise<Array<{ id: string; name: string }>> {
187
+ const unnamed = () => copyParam("ownerOverview.unnamedBusiness", "Business");
188
+ const hosted = await hostOwnedSites();
189
+ if (hosted) return hosted.map((h) => ({ id: h.id, name: h.name || unnamed() }));
190
+ const res = await querySites({ query: { cursorPaging: { limit: 50 } } });
191
+ return (res.sites ?? [])
192
+ .filter((r: WixSite) => r?.id)
193
+ .map((r: WixSite) => ({
194
+ id: r.id || "",
195
+ name: r.displayName || (r as { name?: string }).name || unnamed(),
196
+ }));
197
+ }
198
+
179
199
  async function loadRollup(): Promise<OverviewState> {
180
200
  if (isOwnerPreviewDemo()) return demoRollup();
181
201
  try {
182
- const res = await querySites({ query: { cursorPaging: { limit: 50 } } });
183
- const businesses = (res.sites ?? [])
184
- .filter((r: WixSite) => r?.id)
185
- .map((r: WixSite) => ({
186
- id: r.id || "",
187
- name: r.displayName || (r as { name?: string }).name || copyParam("ownerOverview.unnamedBusiness", "Business"),
188
- }));
202
+ const businesses = await listBusinesses();
189
203
  if (businesses.length === 0) return shape({ status: "ready", rows: [] });
190
204
 
191
205
  // THE FAN-OUT. Concurrent, one business each, active scope untouched.
@@ -0,0 +1,60 @@
1
+ // The businesses an EMBEDDING HOST says the signed-in owner owns — or null when there is no
2
+ // such host.
3
+ //
4
+ // Standalone, an owner screen lists businesses itself (`querySites`, account level) and reads
5
+ // each one with `site.forSite(msid)`, which stamps `wix-site-id` on that call. Embedded, the
6
+ // host owns site selection: its data source signs every call for the business the user picked
7
+ // and drops a worker-sent `wix-site-id`, so a fan-out read ONE business N times. A host that
8
+ // opts in supplies its own list instead (`HimalayaOwnedSitesDataSource` on iOS / Android, the
9
+ // `ownedSites` prop on web) and answers `himi.wix.ownedSites` with it; the embed then routes a
10
+ // `site.forSite(msid)` call to the host's per-site call for any id on that list and refuses
11
+ // every other id. So a worker reads the list here and keeps using `site.forSite` unchanged.
12
+ //
13
+ // A rejection is "not hosted": a standalone shell has no handler for the verb, and an older embed
14
+ // predates it. Callers then do exactly what they did before. A host that opted in but cannot list
15
+ // answers `{ error }` instead, which THROWS here: falling back to `querySites` would read the
16
+ // account through the host's single-site signer and fan out to ids the host then refuses.
17
+
18
+ import { dispatch } from "@himalaya/dispatch";
19
+
20
+ export const OWNED_SITES_ACTION = "himi.wix.ownedSites";
21
+
22
+ export interface HostedSite {
23
+ /** The business's metaSiteId — what `site.forSite` takes. */
24
+ id: string;
25
+ name: string;
26
+ logoUrl?: string;
27
+ color?: string;
28
+ }
29
+
30
+ function text(v: unknown): string {
31
+ return typeof v === "string" ? v.trim() : "";
32
+ }
33
+
34
+ /** The host's list, in the host's order; [] is a real answer (an owner with no businesses).
35
+ * null when the verb is unanswered or the answer is not a list. Throws the host's `{ error }`. */
36
+ export async function hostOwnedSites(): Promise<HostedSite[] | null> {
37
+ let value: unknown;
38
+ try {
39
+ value = await dispatch(OWNED_SITES_ACTION);
40
+ } catch {
41
+ return null;
42
+ }
43
+ // Native hosts resolve the payload itself; some web shells wrap it as `{ payload }`.
44
+ const body = value as { sites?: unknown; error?: unknown; payload?: { sites?: unknown; error?: unknown } } | null;
45
+ const error = body?.error ?? body?.payload?.error;
46
+ if (typeof error === "string") throw new Error(error || "The host could not list your businesses");
47
+ const raw = body?.sites ?? body?.payload?.sites;
48
+ if (!Array.isArray(raw)) return null;
49
+ const sites: HostedSite[] = [];
50
+ for (const entry of raw) {
51
+ const e = (entry ?? {}) as Record<string, unknown>;
52
+ const id = text(e.id);
53
+ if (!id) continue;
54
+ const site: HostedSite = { id, name: text(e.name) };
55
+ if (text(e.logoUrl)) site.logoUrl = text(e.logoUrl);
56
+ if (text(e.color)) site.color = text(e.color);
57
+ sites.push(site);
58
+ }
59
+ return sites;
60
+ }
@@ -22,6 +22,11 @@
22
22
  // or one whose mint fails, resolves to `null` and renders as "—" beside the others rather
23
23
  // than throwing the whole screen into its error state. A rollup that disappears because one
24
24
  // of five businesses is unreachable is worse than one that says so in a row.
25
+ //
26
+ // EMBEDDED, THE HOST NAMES THE BUSINESSES. A host that owns site selection (Symphony's Manage
27
+ // Wix) signs each call itself and would drop the per-call `wix-site-id`, so it supplies the
28
+ // owner's sites instead (`hostOwnedSites`) and resolves each `site.forSite(msid)` call against
29
+ // that list. The list replaces rung 1 only; the fan-out below is identical either way.
25
30
 
26
31
  import { defineService } from "@himalaya/state";
27
32
  import { querySites, type WixSite } from "@himalaya/wix-site-list";
@@ -31,6 +36,7 @@ import { ownerLoadError, isOwnerPreviewDemo, ownerDemoFallback } from "../_share
31
36
  import { DEMO_SITES, DEMO_NOTE } from "../_shared/owner-preview-demo.js";
32
37
  import { stashCopy, copyParam } from "../_shared/himi-copy.js";
33
38
  import { pickSite, pickedSite } from "../_shared/owner-site-pick.js";
39
+ import { hostOwnedSites } from "../_shared/owner-hosted-sites.js";
34
40
 
35
41
  /** Mirrors the Home dashboard's "needs attention" definition, so a number here means the
36
42
  * same thing it means after the owner drills into that business. */
@@ -176,16 +182,24 @@ async function attentionCount(msid: string): Promise<number | null> {
176
182
  }
177
183
  }
178
184
 
185
+ /** Rung 1: the host's list when embedded, else the account's own (`querySites`). */
186
+ async function listBusinesses(): Promise<Array<{ id: string; name: string }>> {
187
+ const unnamed = () => copyParam("ownerOverview.unnamedBusiness", "Business");
188
+ const hosted = await hostOwnedSites();
189
+ if (hosted) return hosted.map((h) => ({ id: h.id, name: h.name || unnamed() }));
190
+ const res = await querySites({ query: { cursorPaging: { limit: 50 } } });
191
+ return (res.sites ?? [])
192
+ .filter((r: WixSite) => r?.id)
193
+ .map((r: WixSite) => ({
194
+ id: r.id || "",
195
+ name: r.displayName || (r as { name?: string }).name || unnamed(),
196
+ }));
197
+ }
198
+
179
199
  async function loadRollup(): Promise<OverviewState> {
180
200
  if (isOwnerPreviewDemo()) return demoRollup();
181
201
  try {
182
- const res = await querySites({ query: { cursorPaging: { limit: 50 } } });
183
- const businesses = (res.sites ?? [])
184
- .filter((r: WixSite) => r?.id)
185
- .map((r: WixSite) => ({
186
- id: r.id || "",
187
- name: r.displayName || (r as { name?: string }).name || copyParam("ownerOverview.unnamedBusiness", "Business"),
188
- }));
202
+ const businesses = await listBusinesses();
189
203
  if (businesses.length === 0) return shape({ status: "ready", rows: [] });
190
204
 
191
205
  // THE FAN-OUT. Concurrent, one business each, active scope untouched.
@@ -0,0 +1,60 @@
1
+ // The businesses an EMBEDDING HOST says the signed-in owner owns — or null when there is no
2
+ // such host.
3
+ //
4
+ // Standalone, an owner screen lists businesses itself (`querySites`, account level) and reads
5
+ // each one with `site.forSite(msid)`, which stamps `wix-site-id` on that call. Embedded, the
6
+ // host owns site selection: its data source signs every call for the business the user picked
7
+ // and drops a worker-sent `wix-site-id`, so a fan-out read ONE business N times. A host that
8
+ // opts in supplies its own list instead (`HimalayaOwnedSitesDataSource` on iOS / Android, the
9
+ // `ownedSites` prop on web) and answers `himi.wix.ownedSites` with it; the embed then routes a
10
+ // `site.forSite(msid)` call to the host's per-site call for any id on that list and refuses
11
+ // every other id. So a worker reads the list here and keeps using `site.forSite` unchanged.
12
+ //
13
+ // A rejection is "not hosted": a standalone shell has no handler for the verb, and an older embed
14
+ // predates it. Callers then do exactly what they did before. A host that opted in but cannot list
15
+ // answers `{ error }` instead, which THROWS here: falling back to `querySites` would read the
16
+ // account through the host's single-site signer and fan out to ids the host then refuses.
17
+
18
+ import { dispatch } from "@himalaya/dispatch";
19
+
20
+ export const OWNED_SITES_ACTION = "himi.wix.ownedSites";
21
+
22
+ export interface HostedSite {
23
+ /** The business's metaSiteId — what `site.forSite` takes. */
24
+ id: string;
25
+ name: string;
26
+ logoUrl?: string;
27
+ color?: string;
28
+ }
29
+
30
+ function text(v: unknown): string {
31
+ return typeof v === "string" ? v.trim() : "";
32
+ }
33
+
34
+ /** The host's list, in the host's order; [] is a real answer (an owner with no businesses).
35
+ * null when the verb is unanswered or the answer is not a list. Throws the host's `{ error }`. */
36
+ export async function hostOwnedSites(): Promise<HostedSite[] | null> {
37
+ let value: unknown;
38
+ try {
39
+ value = await dispatch(OWNED_SITES_ACTION);
40
+ } catch {
41
+ return null;
42
+ }
43
+ // Native hosts resolve the payload itself; some web shells wrap it as `{ payload }`.
44
+ const body = value as { sites?: unknown; error?: unknown; payload?: { sites?: unknown; error?: unknown } } | null;
45
+ const error = body?.error ?? body?.payload?.error;
46
+ if (typeof error === "string") throw new Error(error || "The host could not list your businesses");
47
+ const raw = body?.sites ?? body?.payload?.sites;
48
+ if (!Array.isArray(raw)) return null;
49
+ const sites: HostedSite[] = [];
50
+ for (const entry of raw) {
51
+ const e = (entry ?? {}) as Record<string, unknown>;
52
+ const id = text(e.id);
53
+ if (!id) continue;
54
+ const site: HostedSite = { id, name: text(e.name) };
55
+ if (text(e.logoUrl)) site.logoUrl = text(e.logoUrl);
56
+ if (text(e.color)) site.color = text(e.color);
57
+ sites.push(site);
58
+ }
59
+ return sites;
60
+ }
@@ -22,6 +22,11 @@
22
22
  // or one whose mint fails, resolves to `null` and renders as "—" beside the others rather
23
23
  // than throwing the whole screen into its error state. A rollup that disappears because one
24
24
  // of five businesses is unreachable is worse than one that says so in a row.
25
+ //
26
+ // EMBEDDED, THE HOST NAMES THE BUSINESSES. A host that owns site selection (Symphony's Manage
27
+ // Wix) signs each call itself and would drop the per-call `wix-site-id`, so it supplies the
28
+ // owner's sites instead (`hostOwnedSites`) and resolves each `site.forSite(msid)` call against
29
+ // that list. The list replaces rung 1 only; the fan-out below is identical either way.
25
30
 
26
31
  import { defineService } from "@himalaya/state";
27
32
  import { querySites, type WixSite } from "@himalaya/wix-site-list";
@@ -31,6 +36,7 @@ import { ownerLoadError, isOwnerPreviewDemo, ownerDemoFallback } from "../_share
31
36
  import { DEMO_SITES, DEMO_NOTE } from "../_shared/owner-preview-demo.js";
32
37
  import { stashCopy, copyParam } from "../_shared/himi-copy.js";
33
38
  import { pickSite, pickedSite } from "../_shared/owner-site-pick.js";
39
+ import { hostOwnedSites } from "../_shared/owner-hosted-sites.js";
34
40
 
35
41
  /** Mirrors the Home dashboard's "needs attention" definition, so a number here means the
36
42
  * same thing it means after the owner drills into that business. */
@@ -176,16 +182,24 @@ async function attentionCount(msid: string): Promise<number | null> {
176
182
  }
177
183
  }
178
184
 
185
+ /** Rung 1: the host's list when embedded, else the account's own (`querySites`). */
186
+ async function listBusinesses(): Promise<Array<{ id: string; name: string }>> {
187
+ const unnamed = () => copyParam("ownerOverview.unnamedBusiness", "Business");
188
+ const hosted = await hostOwnedSites();
189
+ if (hosted) return hosted.map((h) => ({ id: h.id, name: h.name || unnamed() }));
190
+ const res = await querySites({ query: { cursorPaging: { limit: 50 } } });
191
+ return (res.sites ?? [])
192
+ .filter((r: WixSite) => r?.id)
193
+ .map((r: WixSite) => ({
194
+ id: r.id || "",
195
+ name: r.displayName || (r as { name?: string }).name || unnamed(),
196
+ }));
197
+ }
198
+
179
199
  async function loadRollup(): Promise<OverviewState> {
180
200
  if (isOwnerPreviewDemo()) return demoRollup();
181
201
  try {
182
- const res = await querySites({ query: { cursorPaging: { limit: 50 } } });
183
- const businesses = (res.sites ?? [])
184
- .filter((r: WixSite) => r?.id)
185
- .map((r: WixSite) => ({
186
- id: r.id || "",
187
- name: r.displayName || (r as { name?: string }).name || copyParam("ownerOverview.unnamedBusiness", "Business"),
188
- }));
202
+ const businesses = await listBusinesses();
189
203
  if (businesses.length === 0) return shape({ status: "ready", rows: [] });
190
204
 
191
205
  // THE FAN-OUT. Concurrent, one business each, active scope untouched.