@wix/himalaya-cli 0.1634.0 → 0.1636.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 (54) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/app-config.d.ts +1 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/app-module.d.ts +2 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/wix-payments-contract.d.ts +22 -10
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/wix-payments-wire.d.ts +16 -6
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/wix-payments.d.ts +11 -4
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/schema.d.ts +6 -0
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/apple-iap-contract.d.ts +10 -4
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap-checkout.mjs +84 -16
  9. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap.mjs +67 -10
  10. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +83 -15
  11. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdui.mjs +1 -0
  12. package/dist/browser-plane/assets.json +5 -5
  13. package/dist/browser-plane/preview-deferred/{c3457ce58c96b65f303fec0ddf5f06733004eaf9e21e62302b155ec63a7765f7.bin → 67feb362281a3040ee05592769227c58a98ec3c8073dac95ecb5629141c67061.bin} +1 -1
  14. package/dist/browser-plane/sw.js +7 -7
  15. package/dist/cli.mjs +241 -57
  16. package/dist/preview-assets.generated.bin +0 -0
  17. package/fixtures/wix-canned.json +3 -0
  18. package/guides/designing-himi-screens.md +5 -1
  19. package/guides/himi-json-output.md +14 -1
  20. package/guides/himi-mcp.md +5 -3
  21. package/guides/iap-products.md +8 -0
  22. package/guides/sdk-reference.json +22 -8
  23. package/guides/skill.md +1 -0
  24. package/package.json +2 -2
  25. package/templates/beauty-salon/tier5-src/_shared/collection-contract.ts +0 -1
  26. package/templates/beauty-salon-owner/tier5-src/_shared/collection-contract.ts +0 -1
  27. package/templates/branded-lite/dev/index.ts +226 -2060
  28. package/templates/branded-lite/l10n/en.json +3 -5
  29. package/templates/branded-lite/tier5-src/_shared/collection-contract.ts +0 -1
  30. package/templates/branded-lite/tier5-src/_shared/himi-cms.ts +2 -1
  31. package/templates/branded-lite/tier5-src/himi-branded-lite-cms/index.ts +160 -39
  32. package/templates/community-club/dev/index.ts +210 -2044
  33. package/templates/community-club/tier5-src/_shared/collection-contract.ts +0 -1
  34. package/templates/community-club/tier5-src/_shared/himi-cms.ts +2 -1
  35. package/templates/community-club/tier5-src/himi-branded-lite-cms/index.ts +160 -39
  36. package/templates/community-club-owner/tier5-src/_shared/collection-contract.ts +0 -1
  37. package/templates/events-meetups/tier5-src/_shared/collection-contract.ts +0 -1
  38. package/templates/events-meetups-owner/tier5-src/_shared/collection-contract.ts +0 -1
  39. package/templates/gym-fitness/tier5-src/_shared/collection-contract.ts +0 -1
  40. package/templates/gym-fitness-owner/tier5-src/_shared/collection-contract.ts +0 -1
  41. package/templates/nonprofit-cause/tier5-src/_shared/collection-contract.ts +0 -1
  42. package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-contract.ts +0 -1
  43. package/templates/owner-lite/tier5-src/_shared/collection-contract.ts +0 -1
  44. package/templates/podcast-creator/tier5-src/_shared/collection-contract.ts +0 -1
  45. package/templates/podcast-creator-owner/tier5-src/_shared/collection-contract.ts +0 -1
  46. package/templates/portfolio-creative/tier5-src/_shared/collection-contract.ts +0 -1
  47. package/templates/portfolio-creative-owner/tier5-src/_shared/collection-contract.ts +0 -1
  48. package/templates/restaurant/tier5-src/_shared/collection-contract.ts +0 -1
  49. package/templates/restaurant-owner/tier5-src/_shared/collection-contract.ts +0 -1
  50. package/templates/retail-store/tier5-src/_shared/collection-contract.ts +0 -1
  51. package/templates/retail-store-owner/tier5-src/_shared/collection-contract.ts +0 -1
  52. package/templates/salon-spa/tier5-src/_shared/collection-contract.ts +0 -1
  53. package/templates/yoga-studio/tier5-src/_shared/collection-contract.ts +0 -1
  54. package/templates/yoga-studio-owner/tier5-src/_shared/collection-contract.ts +0 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$source": "en",
3
- "$generated": "2026-11-10T08:42:54.071Z",
3
+ "$generated": "2026-11-10T10:43:25.380Z",
4
4
  "messages": {
5
5
  "h_042c862e4467": {
6
6
  "source": "Try again",
@@ -47,7 +47,6 @@
47
47
  "himi_my_groups#payload.body.props.children[2].body.props.children[2].body.props.title",
48
48
  "himi_faq#payload.body.props.children[6].body.props.children[2].body.props.title",
49
49
  "himi_steps#payload.body.props.children[4].body.props.children[2].body.props.title",
50
- "himi_cms#payload.body.props.children[3].body.props.children[7].body.props.children[2].body.props.title",
51
50
  "himi_donations#payload.body.props.children[4].body.props.children[8].body.props.children[2].body.props.title",
52
51
  "himi_gift_cards#payload.body.props.children[4].body.props.children[8].body.props.children[2].body.props.title",
53
52
  "himi_videos#payload.body.props.children[7].body.props.children[8].body.props.children[2].body.props.title",
@@ -164,7 +163,7 @@
164
163
  "kind": "modifier.accessibilityLabel",
165
164
  "occurrences": [
166
165
  "himi_chat_thread#stickyFooter.body.props.children[0].body.props.children[0].modifiers.accessibilityLabel",
167
- "himi_business_chat#stickyFooter.body.props.children[1].body.props.children[0].modifiers.accessibilityLabel"
166
+ "himi_business_chat#stickyFooter.body.props.children[1].body.props.children[0].body.props.children[1].modifiers.accessibilityLabel"
168
167
  ],
169
168
  "translation": "",
170
169
  "key": "chatThread.composerLabel"
@@ -298,7 +297,7 @@
298
297
  "kind": "TextField.placeholder",
299
298
  "occurrences": [
300
299
  "himi_chat_thread#stickyFooter.body.props.children[0].body.props.children[0].body.props.placeholder",
301
- "himi_business_chat#stickyFooter.body.props.children[1].body.props.children[0].body.props.placeholder"
300
+ "himi_business_chat#stickyFooter.body.props.children[1].body.props.children[0].body.props.children[1].body.props.placeholder"
302
301
  ],
303
302
  "translation": "",
304
303
  "key": "chatThread.composerPlaceholder"
@@ -425,7 +424,6 @@
425
424
  "himi_forum_post#payload.body.props.children[3].body.props.title",
426
425
  "himi_my_events#payload.body.props.children[1].body.props.children[3].body.props.title",
427
426
  "himi_my_groups#payload.body.props.children[6].body.props.children[3].body.props.title",
428
- "himi_cms#payload.body.props.children[3].body.props.children[7].body.props.children[3].body.props.title",
429
427
  "himi_donations#payload.body.props.children[4].body.props.children[8].body.props.children[3].body.props.title",
430
428
  "himi_gift_cards#payload.body.props.children[4].body.props.children[8].body.props.children[3].body.props.title",
431
429
  "himi_videos#payload.body.props.children[7].body.props.children[8].body.props.children[3].body.props.title",
@@ -92,7 +92,6 @@ export const COLLECTIONS: Record<string, CollectionConfig> = {
92
92
  searchPlaceholder: 'Customer name, email or order #', sortLabels: { default: 'Newest', az: 'Customer A–Z', za: 'Customer Z–A' },
93
93
  filterValues: ['Recent 7 days', 'Paid', 'Unpaid', 'Partially paid', 'Pending', 'Refunded', 'Partly refunded'] },
94
94
  himiBrandedGroups: { rows: 'rows', title: 'name', subtitle: 'description', image: 'image', view: 'grid' },
95
- himiBrandedCms: { rows: 'rows', title: 'name', subtitle: 'subtitle', view: 'grid' },
96
95
  himiBrandedCmsCollection: { instanceInput: 'collection', rows: 'rows', title: 'title', subtitle: 'subtitle', view: 'list' },
97
96
  himiBrandedOrders: { rows: 'rows', title: 'title', subtitle: 'subtitle', image: 'image', metadata: ['status', 'meta'], rowStyle: 'flat', view: 'list', backendQuery: true,
98
97
  searchPlaceholder: 'Search by order number', countNoun: ['order', 'orders'], filterValues: ['Not fulfilled', 'Partially fulfilled', 'Fulfilled'],
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  queryDataItems as queryWixDataItems,
3
+ countDataItems,
3
4
  insertDataItem,
4
5
  updateDataItem,
5
6
  removeDataItem,
@@ -8,7 +9,7 @@ import {
8
9
  } from "@himalaya/wix-data";
9
10
 
10
11
  export type { WixDataItem, WixQueryDataItemsResponse };
11
- export { insertDataItem, updateDataItem, removeDataItem };
12
+ export { countDataItems, insertDataItem, updateDataItem, removeDataItem };
12
13
 
13
14
  export interface CmsFieldDefinition {
14
15
  key: string;
@@ -1,58 +1,135 @@
1
1
  import { defineService } from "../_shared/collection-service.js";
2
- import { collectionDefinitions, type CmsCollectionDefinition } from "../_shared/himi-cms";
3
- import { copyParam, stashCopy } from "../_shared/himi-copy.js";
2
+ import { collectionDefinitions, countDataItems, type CmsCollectionDefinition } from "../_shared/himi-cms";
3
+ import { copyParam, copyPlural, stashCopy } from "../_shared/himi-copy.js";
4
4
  import { getAll, upsert, type DbEntity } from "@himalaya/local-db";
5
5
 
6
6
  // Offline-first (data-offline): this catalog is derived from the app's BAKED CMS
7
7
  // contract (himalaya.input.cmsCollections) — there is no live wixApi fetch to
8
- // revalidate, so it is structurally read-only. We still add the read-through seam
9
- // around that baked load path: the derived rows are mirrored into the on-device store,
8
+ // revalidate the NAMES, so they are structurally read-only. We still add the read-through seam
9
+ // around that baked load path: the derived entries are mirrored into the on-device store,
10
10
  // and if the contract ever arrives empty we fall back to the last-synced cache.
11
11
  // DEGRADES GRACEFULLY: every local-db call is wrapped so a build that hasn't granted
12
12
  // `local_database` (+ declared the `cms_collections` entity) behaves exactly as before.
13
+ //
14
+ // The item COUNT under each name is the one live read. It is never cached: a stale number would
15
+ // read as the business's own, and a count the API did not return is not shown at all.
13
16
  const ENTITY = "cms_collections";
14
- async function readCached(): Promise<CollectionRow[]> {
15
- try { return (await getAll(ENTITY)) as unknown as CollectionRow[]; } catch { return []; }
17
+
18
+ /** What the screen needs to know about a collection, with no per-visit state. Mirrored to the cache. */
19
+ interface Entry {
20
+ id: string;
21
+ title: string;
22
+ detailScreenId: string;
23
+ isMembersOnly: boolean;
24
+ }
25
+
26
+ async function readCached(): Promise<Entry[]> {
27
+ try {
28
+ return (await getAll(ENTITY)).map((r) => {
29
+ const stored = r as unknown as Partial<Entry> & { meta?: string };
30
+ return {
31
+ id: String(stored.id ?? ""),
32
+ title: typeof stored.title === "string" ? stored.title : String(stored.id ?? ""),
33
+ detailScreenId: typeof stored.detailScreenId === "string" ? stored.detailScreenId : `himi_cms_collection_${encodeURIComponent(String(stored.id ?? ""))}`,
34
+ // A row cached before counts carried its "Members only" label in `meta`.
35
+ isMembersOnly: stored.isMembersOnly ?? Boolean(stored.meta),
36
+ };
37
+ }).filter((entry) => entry.id !== "");
38
+ } catch { return []; }
16
39
  }
17
- async function writeCached(rows: CollectionRow[]): Promise<void> {
18
- try { for (const r of rows) await upsert(ENTITY, r as unknown as DbEntity); } catch { /* capability not granted → online-only */ }
40
+ async function writeCached(entries: Entry[]): Promise<void> {
41
+ try { for (const e of entries) await upsert(ENTITY, e as unknown as DbEntity); } catch { /* capability not granted → online-only */ }
19
42
  }
20
43
 
44
+ /** "pending" until the count call settles; "none" for a members-only collection, which shows a lock instead. */
45
+ type CountStatus = "pending" | "ready" | "error" | "none";
46
+
21
47
  interface CollectionRow {
22
48
  id: string;
23
49
  title: string;
24
- subtitle: string;
25
- meta: string;
26
- initial: string;
27
50
  detailScreenId: string;
51
+ isMembersOnly: boolean;
52
+ membersOnlyText: string;
53
+ countStatus: CountStatus;
54
+ countText: string;
55
+ /** The row's accessible name: the collection, then whichever caption it shows. */
56
+ accessibilityLabel: string;
57
+ // The descriptor hides a node when its bound flag is TRUE, so each caption has its inverse.
58
+ noCountLoading: boolean;
59
+ noCountText: boolean;
60
+ noMembersOnly: boolean;
28
61
  }
29
62
 
30
63
  interface State {
31
64
  status: "ready";
32
- all: CollectionRow[];
33
65
  rows: CollectionRow[];
34
- emptyText: string;
35
- noRows: boolean;
66
+ /** More than 5 collections: the name search shows. */
67
+ showSearch: boolean;
68
+ hideSearch: boolean;
69
+ /** The contract holds no collection at all. */
36
70
  noEmpty: boolean;
71
+ /** A search matched none of them. */
72
+ noNoMatch: boolean;
73
+ noRows: boolean;
37
74
  }
38
75
 
39
- function row(collection: CmsCollectionDefinition): CollectionRow {
40
- const title = collection.displayName.trim() || collection.id;
41
- // No field count and no "Available to visitors": both are back-office schema/permission
42
- // metadata, meaningless to the visitor reading the card ([branded-lite-qa] #3077). "Members
43
- // only" stays — it tells the visitor they will need to sign in, which is theirs to know.
76
+ /** A name search earns its place only past this many collections. */
77
+ const SEARCH_THRESHOLD = 5;
78
+
79
+ let entries: Entry[] = [];
80
+ let query = "";
81
+ /** A settled count by collection id. A failed refresh never replaces a count already known. */
82
+ const counts = new Map<string, { status: "ready" | "error"; count: number }>();
83
+ /** Ids with a count call on the wire, so a second `load` (onAppear fires on every visit) does not double it. */
84
+ const inFlight = new Set<string>();
85
+
86
+ function entryFor(collection: CmsCollectionDefinition): Entry {
44
87
  return {
45
88
  id: collection.id,
46
- title,
47
- subtitle: "",
48
- meta: collection.readRole === "ANYONE" ? "" : copyParam("cms.membersOnlyAccessLabel", "Members only"),
49
- initial: (title[0] || "C").toUpperCase(),
89
+ title: collection.displayName.trim() || collection.id,
50
90
  detailScreenId: `himi_cms_collection_${encodeURIComponent(collection.id)}`,
91
+ isMembersOnly: collection.readRole !== "ANYONE",
51
92
  };
52
93
  }
53
94
 
54
- function ready(all: CollectionRow[], rows = all, emptyText = copyParam("cms.emptyText", "No CMS collections are published to this app yet.")): State {
55
- return { status: "ready", all, rows, emptyText, noRows: rows.length === 0, noEmpty: rows.length > 0 };
95
+ function rowFor(entry: Entry): CollectionRow {
96
+ const membersOnlyText = copyParam("cms.membersOnlyAccessLabel", "Members only");
97
+ const known = counts.get(entry.id);
98
+ const countStatus: CountStatus = entry.isMembersOnly ? "none" : known?.status ?? "pending";
99
+ const countText = countStatus === "ready" && known
100
+ ? copyPlural("cms.itemCount", "{n, plural, one {# item} other {# items}}", known.count)
101
+ : "";
102
+ const caption = entry.isMembersOnly ? membersOnlyText : countText;
103
+ return {
104
+ id: entry.id,
105
+ title: entry.title,
106
+ detailScreenId: entry.detailScreenId,
107
+ isMembersOnly: entry.isMembersOnly,
108
+ membersOnlyText,
109
+ countStatus,
110
+ countText,
111
+ accessibilityLabel: caption ? `${entry.title}, ${caption}` : entry.title,
112
+ noCountLoading: countStatus !== "pending",
113
+ noCountText: countStatus !== "ready",
114
+ noMembersOnly: !entry.isMembersOnly,
115
+ };
116
+ }
117
+
118
+ /** Every visible field, derived from the module's truth at the moment of the call, so an action that
119
+ * settles later (a count arriving, a search typed) can never publish an older picture of it. */
120
+ function view(): State {
121
+ const needle = query.trim().toLowerCase();
122
+ const shown = needle ? entries.filter((entry) => entry.title.toLowerCase().includes(needle)) : entries;
123
+ const showSearch = entries.length > SEARCH_THRESHOLD;
124
+ return {
125
+ status: "ready",
126
+ rows: shown.map(rowFor),
127
+ showSearch,
128
+ hideSearch: !showSearch,
129
+ noEmpty: entries.length > 0,
130
+ noNoMatch: entries.length === 0 || shown.length > 0,
131
+ noRows: shown.length === 0,
132
+ };
56
133
  }
57
134
 
58
135
  function value(arg: unknown, key: string): string {
@@ -61,30 +138,74 @@ function value(arg: unknown, key: string): string {
61
138
  return typeof result === "string" ? result : "";
62
139
  }
63
140
 
141
+ /** The count the API returned, or undefined: a missing or malformed total is never read as zero. */
142
+ function totalOf(response: { totalCount?: number } | undefined): number | undefined {
143
+ const total = response?.totalCount;
144
+ return typeof total === "number" && Number.isFinite(total) && total >= 0 ? total : undefined;
145
+ }
146
+
147
+ /** One count call per readable collection, all at once; each row fills as ITS call returns. */
148
+ function startCounts(): void {
149
+ for (const entry of entries) {
150
+ if (entry.isMembersOnly || inFlight.has(entry.id)) continue;
151
+ inFlight.add(entry.id);
152
+ const settle = (total: number | undefined) => { inFlight.delete(entry.id); void svc.countSettled({ id: entry.id, total }); };
153
+ countDataItems(entry.id).then((response) => settle(totalOf(response)), () => settle(undefined));
154
+ }
155
+ }
156
+
64
157
  const inputCollections = (globalThis as { himalaya?: { input?: { cmsCollections?: unknown } } }).himalaya?.input?.cmsCollections;
158
+ entries = collectionDefinitions(typeof inputCollections === "string" ? inputCollections : "").map(entryFor);
159
+ const initialState = view();
160
+ const derivedStateKeys = Object.keys(initialState) as Array<keyof State>;
65
161
 
66
162
  const svc = defineService<State, {
67
- load: (state: State, arg?: unknown) => Promise<State>;
163
+ load: (state: State, arg?: unknown) => Promise<State | undefined>;
68
164
  search: (state: State, arg?: unknown) => State;
165
+ countSettled: (state: State, arg: { id: string; total: number | undefined }) => State;
69
166
  }>("himiBrandedCms", {
70
- state: ready(collectionDefinitions(typeof inputCollections === "string" ? inputCollections : "").map(row)),
167
+ state: initialState,
168
+ // Rows and visibility are derived from this run's contract/query/counts.
169
+ // A prior search or count must not override them on a cold boot. Collection
170
+ // definitions already have their own explicit local-database cache.
171
+ notHydrated: derivedStateKeys,
172
+ notPersisted: derivedStateKeys,
71
173
  actions: {
72
- load: async (_state, arg) => {
174
+ load: async (state, arg) => {
73
175
  stashCopy(arg);
74
- const all = collectionDefinitions(value(arg, "collections")).map(row);
75
- if (all.length) { await writeCached(all); return ready(all); }
76
- // No baked contract rows: fall back to the last-synced cache (offline), else empty.
77
- const cached = await readCached();
78
- return cached.length ? ready(cached) : ready(all);
176
+ // The remounted SearchField starts empty. Reset before awaiting the cache so
177
+ // a search typed during this load is not cleared by its eventual completion.
178
+ query = "";
179
+ const given = value(arg, "collections");
180
+ const baked = collectionDefinitions(given).map(entryFor);
181
+ if (baked.length) {
182
+ entries = baked;
183
+ await writeCached(baked);
184
+ } else if (given !== "" || !entries.length) {
185
+ // The contract named no collection (or nothing at all seeded this screen): what was seeded from the
186
+ // input no longer stands. Fall back to the last-synced cache (offline), else the empty state.
187
+ entries = await readCached();
188
+ }
189
+ startCounts();
190
+ // Actions are not serialized: a count that settles while this one is still committing would be
191
+ // overwritten by the older picture returned here. The names normally match what the screen already
192
+ // shows (the same contract seeded it), so then there is nothing to commit and nothing to overwrite.
193
+ const next = view();
194
+ return JSON.stringify(next) === JSON.stringify(state) ? undefined : next;
195
+ },
196
+ search: (_state, arg) => {
197
+ query = value(arg, "value");
198
+ return view();
79
199
  },
80
- search: (state, arg) => {
81
- const query = value(arg, "value").trim().toLowerCase();
82
- const rows = query
83
- ? state.all.filter((item) => `${item.title} ${item.subtitle} ${item.meta}`.toLowerCase().includes(query))
84
- : state.all;
85
- return ready(state.all, rows, query ? copyParam("cms.searchEmptyText", "No collections match your search.") : undefined);
200
+ countSettled: (_state, arg) => {
201
+ if (arg.total !== undefined) counts.set(arg.id, { status: "ready", count: arg.total });
202
+ else if (counts.get(arg.id)?.status !== "ready") counts.set(arg.id, { status: "error", count: 0 });
203
+ return view();
86
204
  },
87
205
  },
88
206
  });
89
207
 
208
+ // NO module-scope `startCounts()`: a bridge call made during the bundle's own evaluation stalls on
209
+ // the Android WebView (see himi-owner-lite-aria-history). The names paint from the baked input, and
210
+ // onAppear's `load` starts the counts right after that first frame.
90
211
  export { svc };