@wix/himalaya-cli 0.1528.0 → 0.1530.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 (126) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/bookings-v2/src/index.d.ts +24 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/ecom/src/extensions.d.ts +144 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/restaurants/src/extensions.d.ts +93 -0
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap-checkout.mjs +27 -5
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap.mjs +19 -3
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +27 -5
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-ecom.mjs +58 -0
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-restaurants.mjs +23 -0
  9. package/dist/cli.mjs +3110 -1993
  10. package/dist/preview-assets.generated.bin +0 -0
  11. package/fixtures/wix-canned.json +83 -0
  12. package/guides/hhp.md +39 -0
  13. package/guides/himi-json-output.md +22 -1
  14. package/guides/release-notes.md +17 -0
  15. package/guides/templates.md +11 -1
  16. package/guides/tier-5-add-a-screen.md +4 -0
  17. package/package.json +2 -2
  18. package/templates/beauty-salon/config.ts +12 -1
  19. package/templates/beauty-salon/dev/index.ts +1767 -2698
  20. package/templates/beauty-salon/tier5-src/_shared/collection-service.ts +23 -5
  21. package/templates/beauty-salon/tier5-src/_shared/engine-intl.ts +18 -0
  22. package/templates/beauty-salon/tier5-src/_shared/himi-restaurants.ts +12 -5
  23. package/templates/beauty-salon/tier5-src/_shared/member-bookings.ts +50 -7
  24. package/templates/beauty-salon/tier5-src/_shared/restaurant-ordering.ts +201 -0
  25. package/templates/beauty-salon/tier5-src/himi-booking-bookings/index.ts +385 -74
  26. package/templates/beauty-salon/tier5-src/himi-branded-lite-cart/index.ts +6 -0
  27. package/templates/beauty-salon-owner/tier5-src/_shared/collection-service.ts +23 -5
  28. package/templates/beauty-salon-owner/tier5-src/_shared/engine-intl.ts +18 -0
  29. package/templates/branded-lite/config.ts +12 -1
  30. package/templates/branded-lite/dev/index.ts +4955 -4801
  31. package/templates/branded-lite/tier5-src/_shared/collection-service.ts +23 -5
  32. package/templates/branded-lite/tier5-src/_shared/engine-intl.ts +18 -0
  33. package/templates/branded-lite/tier5-src/_shared/himi-restaurants.ts +12 -5
  34. package/templates/branded-lite/tier5-src/_shared/member-bookings.ts +50 -7
  35. package/templates/branded-lite/tier5-src/_shared/restaurant-ordering.ts +201 -0
  36. package/templates/branded-lite/tier5-src/himi-booking-bookings/index.ts +385 -74
  37. package/templates/branded-lite/tier5-src/himi-branded-lite-cart/index.ts +6 -0
  38. package/templates/branded-lite/tier5-src/himi-branded-lite-menu-item/index.ts +144 -77
  39. package/templates/branded-lite/tier5-src/himi-branded-lite-menus/index.ts +16 -5
  40. package/templates/branded-lite/tier5-src/himi-branded-lite-reservations/index.ts +1 -1
  41. package/templates/branded-lite/tier5-src/wix-spaces-food-order-detail/index.ts +76 -14
  42. package/templates/branded-lite/tier5-src/wix-spaces-food-orders/index.ts +807 -55
  43. package/templates/community-club/config.ts +12 -1
  44. package/templates/community-club/tier5-src/_shared/collection-service.ts +23 -5
  45. package/templates/community-club/tier5-src/_shared/engine-intl.ts +18 -0
  46. package/templates/community-club/tier5-src/_shared/himi-restaurants.ts +12 -5
  47. package/templates/community-club/tier5-src/_shared/member-bookings.ts +50 -7
  48. package/templates/community-club-owner/tier5-src/_shared/collection-service.ts +23 -5
  49. package/templates/community-club-owner/tier5-src/_shared/engine-intl.ts +18 -0
  50. package/templates/events-meetups/config.ts +12 -1
  51. package/templates/events-meetups/tier5-src/_shared/collection-service.ts +23 -5
  52. package/templates/events-meetups/tier5-src/_shared/engine-intl.ts +18 -0
  53. package/templates/events-meetups/tier5-src/_shared/himi-restaurants.ts +12 -5
  54. package/templates/events-meetups/tier5-src/_shared/member-bookings.ts +50 -7
  55. package/templates/events-meetups-owner/tier5-src/_shared/collection-service.ts +23 -5
  56. package/templates/events-meetups-owner/tier5-src/_shared/engine-intl.ts +18 -0
  57. package/templates/gym-fitness/config.ts +12 -1
  58. package/templates/gym-fitness/dev/index.ts +1753 -2697
  59. package/templates/gym-fitness/tier5-src/_shared/collection-service.ts +23 -5
  60. package/templates/gym-fitness/tier5-src/_shared/engine-intl.ts +18 -0
  61. package/templates/gym-fitness/tier5-src/_shared/himi-restaurants.ts +12 -5
  62. package/templates/gym-fitness/tier5-src/_shared/member-bookings.ts +50 -7
  63. package/templates/gym-fitness/tier5-src/_shared/restaurant-ordering.ts +201 -0
  64. package/templates/gym-fitness/tier5-src/himi-booking-bookings/index.ts +385 -74
  65. package/templates/gym-fitness/tier5-src/himi-branded-lite-cart/index.ts +6 -0
  66. package/templates/gym-fitness-owner/tier5-src/_shared/collection-service.ts +23 -5
  67. package/templates/gym-fitness-owner/tier5-src/_shared/engine-intl.ts +18 -0
  68. package/templates/nonprofit-cause/config.ts +12 -1
  69. package/templates/nonprofit-cause/tier5-src/_shared/collection-service.ts +23 -5
  70. package/templates/nonprofit-cause/tier5-src/_shared/engine-intl.ts +18 -0
  71. package/templates/nonprofit-cause/tier5-src/_shared/himi-restaurants.ts +12 -5
  72. package/templates/nonprofit-cause/tier5-src/_shared/member-bookings.ts +50 -7
  73. package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-service.ts +23 -5
  74. package/templates/nonprofit-cause-owner/tier5-src/_shared/engine-intl.ts +18 -0
  75. package/templates/owner-lite/tier5-src/_shared/collection-service.ts +23 -5
  76. package/templates/owner-lite/tier5-src/_shared/engine-intl.ts +18 -0
  77. package/templates/podcast-creator/config.ts +12 -1
  78. package/templates/podcast-creator/tier5-src/_shared/collection-service.ts +23 -5
  79. package/templates/podcast-creator/tier5-src/_shared/engine-intl.ts +18 -0
  80. package/templates/podcast-creator/tier5-src/_shared/himi-restaurants.ts +12 -5
  81. package/templates/podcast-creator/tier5-src/_shared/member-bookings.ts +50 -7
  82. package/templates/podcast-creator-owner/tier5-src/_shared/collection-service.ts +23 -5
  83. package/templates/podcast-creator-owner/tier5-src/_shared/engine-intl.ts +18 -0
  84. package/templates/portfolio-creative/config.ts +12 -1
  85. package/templates/portfolio-creative/tier5-src/_shared/collection-service.ts +23 -5
  86. package/templates/portfolio-creative/tier5-src/_shared/engine-intl.ts +18 -0
  87. package/templates/portfolio-creative/tier5-src/_shared/himi-restaurants.ts +12 -5
  88. package/templates/portfolio-creative/tier5-src/_shared/member-bookings.ts +50 -7
  89. package/templates/portfolio-creative/tier5-src/_shared/restaurant-ordering.ts +201 -0
  90. package/templates/portfolio-creative/tier5-src/himi-branded-lite-cart/index.ts +6 -0
  91. package/templates/portfolio-creative-owner/tier5-src/_shared/collection-service.ts +23 -5
  92. package/templates/portfolio-creative-owner/tier5-src/_shared/engine-intl.ts +18 -0
  93. package/templates/restaurant/README.md +1 -1
  94. package/templates/restaurant/config.ts +12 -1
  95. package/templates/restaurant/dev/index.ts +1373 -275
  96. package/templates/restaurant/himalaya.template.json +2 -1
  97. package/templates/restaurant/tier5-src/_shared/collection-service.ts +23 -5
  98. package/templates/restaurant/tier5-src/_shared/engine-intl.ts +18 -0
  99. package/templates/restaurant/tier5-src/_shared/himi-restaurants.ts +12 -5
  100. package/templates/restaurant/tier5-src/_shared/member-bookings.ts +50 -7
  101. package/templates/restaurant/tier5-src/_shared/member-orders.ts +8 -0
  102. package/templates/restaurant/tier5-src/_shared/restaurant-ordering.ts +201 -0
  103. package/templates/restaurant/tier5-src/himi-branded-lite-menu-item/index.ts +144 -77
  104. package/templates/restaurant/tier5-src/himi-branded-lite-menus/index.ts +16 -5
  105. package/templates/restaurant/tier5-src/wix-spaces-food-order-detail/index.ts +76 -14
  106. package/templates/restaurant/tier5-src/wix-spaces-food-orders/index.ts +807 -55
  107. package/templates/restaurant-owner/tier5-src/_shared/collection-service.ts +23 -5
  108. package/templates/restaurant-owner/tier5-src/_shared/engine-intl.ts +18 -0
  109. package/templates/retail-store/config.ts +12 -1
  110. package/templates/retail-store/tier5-src/_shared/collection-service.ts +23 -5
  111. package/templates/retail-store/tier5-src/_shared/engine-intl.ts +18 -0
  112. package/templates/retail-store/tier5-src/_shared/himi-restaurants.ts +12 -5
  113. package/templates/retail-store/tier5-src/_shared/member-bookings.ts +50 -7
  114. package/templates/retail-store/tier5-src/_shared/restaurant-ordering.ts +201 -0
  115. package/templates/retail-store/tier5-src/himi-branded-lite-cart/index.ts +6 -0
  116. package/templates/retail-store-owner/tier5-src/_shared/collection-service.ts +23 -5
  117. package/templates/retail-store-owner/tier5-src/_shared/engine-intl.ts +18 -0
  118. package/templates/yoga-studio/config.ts +12 -1
  119. package/templates/yoga-studio/dev/index.ts +1755 -2699
  120. package/templates/yoga-studio/tier5-src/_shared/collection-service.ts +23 -5
  121. package/templates/yoga-studio/tier5-src/_shared/engine-intl.ts +18 -0
  122. package/templates/yoga-studio/tier5-src/_shared/himi-restaurants.ts +12 -5
  123. package/templates/yoga-studio/tier5-src/_shared/member-bookings.ts +50 -7
  124. package/templates/yoga-studio/tier5-src/himi-booking-bookings/index.ts +385 -74
  125. package/templates/yoga-studio-owner/tier5-src/_shared/collection-service.ts +23 -5
  126. package/templates/yoga-studio-owner/tier5-src/_shared/engine-intl.ts +18 -0
@@ -5,40 +5,31 @@
5
5
  // read; when the GET path 404s/errors on a drifted gateway the worker falls
6
6
  // back to the items QUERY (client-side find by id — correct whether or not the
7
7
  // gateway honored the filter). Only both failing lands in the error card.
8
+ //
9
+ // "Add to order" puts the dish in the site's real Wix cart (Restaurants online
10
+ // ordering, _shared/restaurant-ordering.ts) — never a device-local basket. It is
11
+ // offered only when the restaurant takes online orders now and the dish can be
12
+ // ordered as shown: a dish with options to pick (modifier groups) or a sold-out
13
+ // dish says so instead of adding the wrong thing.
8
14
  import { defineService } from "@himalaya/state";
9
- import { getItem, queryItems, type WixItem } from "@himalaya/wix-restaurants";
15
+ import { getItem, queryItems, type WixItem, type WixOperation } from "@himalaya/wix-restaurants";
16
+ import { addLineItemsToCurrentCartV2, ecomApplicationErrorCode, getCurrentCartV2 } from "@himalaya/wix-ecom";
10
17
  import { WIX_DETAIL_IMAGE } from "@himalaya/wix-media";
11
18
  import { descriptionSnippet, itemImage, itemLabels, itemPriceLine, itemPriceVariants, unwrapItem } from "../_shared/himi-restaurants.js";
19
+ import { addedLine, dishReference, lineQuantity, loadDishPlacement, loadOrderingOperation, restaurantItemCount, type DishPlacement } from "../_shared/restaurant-ordering.js";
12
20
  import { cleanTitle, initials } from "../_shared/himi-text.js";
13
21
  import { copyParam, copyTemplate, stashCopy } from "../_shared/himi-copy.js";
14
- import { localPreferences } from "../_shared/local-preferences.js";
15
-
16
- interface BasketItem {
17
- id: string;
18
- name: string;
19
- priceLine: string;
20
- quantity: number;
21
- }
22
22
 
23
23
  interface VariantRow {
24
24
  id: string;
25
+ /** Wix's own variant id ("" when the payload carries none — then it cannot be ordered). */
26
+ variantId: string;
25
27
  name: string;
26
28
  priceLine: string;
29
+ price: string;
27
30
  actionLabel: string;
28
31
  }
29
32
 
30
- function normalizeBasket(value: unknown): BasketItem[] {
31
- if (!Array.isArray(value)) return [];
32
- return value.flatMap((entry) => {
33
- const item = entry as Partial<BasketItem> | null;
34
- if (!item || typeof item.id !== "string" || !item.id || typeof item.name !== "string") return [];
35
- const quantity = Number.isFinite(item.quantity) ? Math.max(1, Math.min(99, Math.floor(item.quantity!))) : 1;
36
- return [{ id: item.id.slice(0, 256), name: item.name.slice(0, 256), priceLine: typeof item.priceLine === "string" ? item.priceLine.slice(0, 128) : "", quantity }];
37
- }).slice(0, 50);
38
- }
39
-
40
- const basket = localPreferences<BasketItem[]>("restaurant.basket", normalizeBasket);
41
-
42
33
  interface State {
43
34
  status: "loading" | "ready" | "error";
44
35
  id: string;
@@ -69,11 +60,32 @@ interface State {
69
60
  hasImage: boolean;
70
61
  noImage: boolean;
71
62
  hideContent: boolean;
72
- basketCount: number;
73
- basketLabel: string;
74
- basketMessage: string;
75
- canSaveBasket: boolean;
76
- cannotSaveBasket: boolean;
63
+ /** "Add to order" is offered (ordering open, dish orderable as shown). */
64
+ canOrder: boolean;
65
+ cannotOrder: boolean;
66
+ /** Why it is not offered: ordering closed, sold out, options to choose on the site, … */
67
+ orderNote: string;
68
+ noOrderNote: boolean;
69
+ /** Feedback for the last add. */
70
+ orderMessage: string;
71
+ /** Restaurant dishes in the cart, and the "View your order" entry. */
72
+ cartCount: number;
73
+ cartLabel: string;
74
+ hasCart: boolean;
75
+ noCart: boolean;
76
+ }
77
+
78
+ /** What load learned about ordering this dish — kept beside the state, never rendered. */
79
+ let operation: WixOperation | undefined;
80
+ let placement: DishPlacement | undefined;
81
+ let adding = false;
82
+ /** Bumped by every confirmed add: a load that an add overtook keeps the add's count and message. */
83
+ let addSeq = 0;
84
+ /** The service's live state (set once the service exists; see wix-spaces-food-orders). */
85
+ let latestState: () => State = () => { throw new Error("service not defined yet"); };
86
+
87
+ function navInput(): { id?: string; screenId?: string; menuId?: string; sectionId?: string } {
88
+ return (globalThis as { himalaya?: { input?: { id?: string; screenId?: string; menuId?: string; sectionId?: string } } }).himalaya?.input ?? {};
77
89
  }
78
90
 
79
91
  function inputId(): string {
@@ -81,14 +93,13 @@ function inputId(): string {
81
93
  // `id` nav param (himi.open { screenId: "himi_menu_item", id }) into worker input.
82
94
  // The screenId-slice below is #570 back-compat for pre-params releases with
83
95
  // baked `himi_menu_item_<id>` screenIds — do not author against this.
84
- const input = (globalThis as { himalaya?: { input?: { id?: string; screenId?: string } } }).himalaya?.input;
85
- if (input?.id) return input.id;
86
- return input?.screenId?.startsWith("himi_menu_item_") ? input.screenId.slice("himi_menu_item_".length) : "";
96
+ const input = navInput();
97
+ if (input.id) return input.id;
98
+ return input.screenId?.startsWith("himi_menu_item_") ? input.screenId.slice("himi_menu_item_".length) : "";
87
99
  }
88
100
 
89
101
  function derive(state: State): State {
90
102
  const empty = state.status === "ready" && !state.name;
91
- const basketCount = normalizeBasket(basket.read()).reduce((total, item) => total + item.quantity, 0);
92
103
  return {
93
104
  ...state,
94
105
  variants: state.variants.map((variant) => ({
@@ -105,25 +116,51 @@ function derive(state: State): State {
105
116
  hasImage: !!state.image,
106
117
  noImage: !state.image,
107
118
  hideContent: state.status !== "ready" || empty,
108
- basketCount,
109
- basketLabel: basketCount
110
- ? copyTemplate("menuItem.basketCount", "{count} {unit} in your basket", {
111
- count: basketCount,
112
- unit: basketCount === 1 ? "item" : "items",
119
+ cannotOrder: !state.canOrder,
120
+ noOrderNote: !state.orderNote,
121
+ cartLabel: state.cartCount
122
+ ? copyTemplate("menuItem.cartCount", "{count} {unit} in your order", {
123
+ count: state.cartCount,
124
+ unit: state.cartCount === 1 ? "dish" : "dishes",
113
125
  })
114
- : copyParam("menuItem.basketEmpty", "Your basket is empty"),
115
- canSaveBasket: basket.available,
116
- cannotSaveBasket: !basket.available,
126
+ : "",
127
+ hasCart: state.cartCount > 0,
128
+ noCart: state.cartCount === 0,
117
129
  };
118
130
  }
119
131
 
120
- function ready(state: State, item: WixItem | undefined): State {
132
+ const NOT_ORDERING = { canOrder: false, orderNote: "" } as const;
133
+
134
+ /** Whether this dish can be ordered from the app, and the note shown when it cannot. */
135
+ function orderability(item: WixItem, variants: VariantRow[], orderingOpen: boolean | "unknown"): Pick<State, "canOrder" | "orderNote"> {
136
+ const raw = item as unknown as { orderSettings?: { inStock?: unknown }; modifierGroups?: unknown; modifierGroupIds?: unknown };
137
+ if (orderingOpen === "unknown") {
138
+ return { canOrder: false, orderNote: copyParam("menuItem.orderingUnavailable", "Online ordering isn't available right now.") };
139
+ }
140
+ if (!orderingOpen) return { canOrder: false, orderNote: copyParam("menuItem.orderingClosed", "Online ordering is closed right now.") };
141
+ if (raw.orderSettings?.inStock === false) return { canOrder: false, orderNote: copyParam("menuItem.soldOut", "This dish is sold out.") };
142
+ const groups = [raw.modifierGroups, raw.modifierGroupIds].some((value) => Array.isArray(value) && value.length > 0);
143
+ // Options are chosen on the restaurant's website: adding the dish without them could
144
+ // order something the customer did not pick.
145
+ if (groups) return { canOrder: false, orderNote: copyParam("menuItem.optionsOnSite", "This dish has options. Order it on the restaurant's website.") };
146
+ // Wix's documented visitor cart flow does not define a price-variant selection. A storefront
147
+ // implementation suggests a key, but we cannot safely recover if Wix prices that line wrong
148
+ // and the cart refuses removal. Keep the choice visible, and send the customer to the site.
149
+ if (variants.length > 0) {
150
+ return { canOrder: false, orderNote: copyParam("menuItem.optionsOnSite", "This dish has options. Order it on the restaurant's website.") };
151
+ }
152
+ if (!placement) return { canOrder: false, orderNote: copyParam("menuItem.notOrderable", "This dish can't be ordered online.") };
153
+ return { canOrder: true, orderNote: "" };
154
+ }
155
+
156
+ function ready(state: State, item: WixItem | undefined, orderingOpen: boolean | "unknown", cartCount: number): State {
121
157
  if (!item || item.visible === false) {
122
158
  return derive({
123
159
  ...state, status: "ready", id: "", name: "", description: "", priceLine: "", image: "", initial: "",
124
160
  label1: "", label2: "", label3: "", noLabel1: true, noLabel2: true, noLabel3: true,
125
161
  variants: [], hasVariants: false, noVariants: true,
126
162
  selectedVariantId: "", selectedVariantLabel: "",
163
+ ...NOT_ORDERING, orderMessage: "", cartCount,
127
164
  emptyText: copyParam("menuItem.emptyText", "This dish is no longer on the menu."),
128
165
  });
129
166
  }
@@ -151,37 +188,61 @@ function ready(state: State, item: WixItem | undefined): State {
151
188
  selectedVariantLabel: "",
152
189
  image: itemImage((item as unknown as Record<string, unknown>).image, WIX_DETAIL_IMAGE),
153
190
  initial: initials(name),
191
+ ...orderability(item, variants, orderingOpen),
192
+ orderMessage: "",
193
+ cartCount,
154
194
  });
155
195
  }
156
196
 
157
- async function load(state: State, arg?: unknown): Promise<State> {
158
- stashCopy(arg);
159
- const id = inputId();
160
- if (!id) return ready(state, undefined);
197
+ async function loadDish(id: string): Promise<WixItem | undefined> {
161
198
  try {
162
199
  // The live GET path 200s with a WRAPPED payload (`{ item: {...} }`) while
163
200
  // the generated client types it bare — unwrap defensively (production R1:
164
201
  // the raw wrapper rendered a hollow "Dish" shell with loading/error/empty
165
202
  // all hidden). An unrecognizable 200 shape falls through to the query.
166
203
  const item = unwrapItem(await getItem(id));
167
- if (item) return ready(state, item);
204
+ if (item) return item;
168
205
  } catch {
169
206
  // Drifted GET path / transient failure → the query fallback below.
170
207
  }
171
208
  // Client-side find keeps this correct even when the gateway ignores the
172
- // filter shape.
209
+ // filter shape. A failure here throws to the error card.
210
+ const response = await queryItems({ filter: { id: { $eq: id } }, paging: { limit: 100 } });
211
+ return (response.items ?? []).find((candidate) => candidate.id === id);
212
+ }
213
+
214
+ async function load(state: State, arg?: unknown): Promise<State> {
215
+ stashCopy(arg);
216
+ const addAt = addSeq;
217
+ const id = inputId();
218
+ if (!id) return ready(state, undefined, false, 0);
219
+ const input = navInput();
220
+ // Ordering is best-effort beside the dish: a failed ordering read hides the
221
+ // button with a note, never the dish.
222
+ const where = loadDishPlacement(id, { menuId: input.menuId, sectionId: input.sectionId }).catch(() => undefined);
223
+ const count = getCurrentCartV2().then(restaurantItemCount, () => 0);
224
+ let item: WixItem | undefined;
173
225
  try {
174
- const response = await queryItems({ filter: { id: { $eq: id } }, paging: { limit: 100 } });
175
- return ready(state, (response.items ?? []).find((candidate) => candidate.id === id));
226
+ item = await loadDish(id);
176
227
  } catch {
177
228
  return derive({ ...state, status: "error" });
178
229
  }
230
+ const [found, cartCount] = await Promise.all([where, count]);
231
+ const { op, ok } = await loadOrderingOperation({ businessLocationId: found?.businessLocationId })
232
+ .then((selected) => ({ op: selected, ok: true }), () => ({ op: undefined, ok: false }));
233
+ operation = op;
234
+ placement = found;
235
+ const next = ready(state, item, ok ? !!op : "unknown", cartCount);
236
+ if (addSeq === addAt) return next;
237
+ // An add landed while this (re)load was in flight: its count and "Added" message are newer.
238
+ const latest = latestState();
239
+ return derive({ ...next, cartCount: latest.cartCount, orderMessage: latest.orderMessage });
179
240
  }
180
241
 
181
242
  const svc = defineService<State, {
182
243
  load: (state: State, arg?: unknown) => Promise<State>;
183
244
  selectVariant: (state: State, arg?: unknown) => State;
184
- addToBasket: (state: State) => State;
245
+ addToOrder: (state: State) => Promise<State>;
185
246
  }>("himiBrandedMenuItem", {
186
247
  state: derive({
187
248
  status: "loading",
@@ -211,11 +272,15 @@ const svc = defineService<State, {
211
272
  hasImage: false,
212
273
  noImage: true,
213
274
  hideContent: true,
214
- basketCount: 0,
215
- basketLabel: "",
216
- basketMessage: "",
217
- canSaveBasket: basket.available,
218
- cannotSaveBasket: !basket.available,
275
+ canOrder: false,
276
+ cannotOrder: true,
277
+ orderNote: "",
278
+ noOrderNote: true,
279
+ orderMessage: "",
280
+ cartCount: 0,
281
+ cartLabel: "",
282
+ hasCart: false,
283
+ noCart: true,
219
284
  }),
220
285
  actions: {
221
286
  load,
@@ -234,36 +299,38 @@ const svc = defineService<State, {
234
299
  name: selected.name,
235
300
  price: selected.priceLine,
236
301
  }),
237
- basketMessage: "",
302
+ orderMessage: "",
238
303
  });
239
304
  },
240
- addToBasket: (state) => {
241
- if (!basket.available) {
242
- return derive({ ...state, basketMessage: copyParam("menuItem.basketSignIn", "Sign in to save a basket for this business.") });
243
- }
305
+ addToOrder: async (state) => {
244
306
  const id = inputId();
245
- if (!id || !state.name || state.status !== "ready") return state;
246
- const selected = state.hasVariants
247
- ? state.variants.find((variant) => variant.id === state.selectedVariantId)
248
- : undefined;
249
- if (state.hasVariants && !selected) {
250
- return derive({ ...state, basketMessage: copyParam("menuItem.chooseVariantFirst", "Choose an option before adding this dish.") });
251
- }
252
- const basketId = selected ? `${id}::${selected.id}` : id;
253
- const basketName = selected ? `${state.name} — ${selected.name}` : state.name;
254
- const basketPrice = selected?.priceLine || state.priceLine;
255
- const current = basket.read();
256
- const existing = current.find((item) => item.id === basketId);
257
- const next = existing
258
- ? current.map((item) => item.id === basketId ? { ...item, quantity: Math.min(99, item.quantity + 1) } : item)
259
- : [...current, { id: basketId, name: basketName, priceLine: basketPrice, quantity: 1 }];
260
- basket.write(next);
261
- if (!basket.available) {
262
- return derive({ ...state, basketMessage: copyParam("menuItem.basketWriteFailed", "Your basket could not be saved. Try again.") });
307
+ if (adding || !state.canOrder || !operation || !placement || !id || state.status !== "ready") return state;
308
+ const reference = dishReference(id, operation.id, placement);
309
+ adding = true;
310
+ try {
311
+ const before = await getCurrentCartV2();
312
+ const prior = addedLine(before, reference);
313
+ const priorQuantity = prior ? lineQuantity(prior) : 0;
314
+ const cart = (await addLineItemsToCurrentCartV2([{ catalogReference: reference, quantity: 1 }])).cart ?? await getCurrentCartV2();
315
+ const line = addedLine(cart, reference);
316
+ if (!line || lineQuantity(line) <= priorQuantity) {
317
+ return derive({ ...latestState(), orderMessage: copyParam("menuItem.addRefused", "The restaurant couldn't add this dish right now.") });
318
+ }
319
+ addSeq++;
320
+ // Onto the live state: a reload while the add was in flight (ordering closed, say) stays.
321
+ return derive({ ...latestState(), orderMessage: copyParam("menuItem.addedToOrder", "Added to your order."), cartCount: restaurantItemCount(cart) });
322
+ } catch (error) {
323
+ const code = ecomApplicationErrorCode(error);
324
+ const orderMessage = code === "ITEM_NOT_FOUND_IN_CATALOG" || code === "INSUFFICIENT_INVENTORY"
325
+ ? copyParam("menuItem.noLongerOrderable", "This dish can't be ordered right now.")
326
+ : copyParam("menuItem.addFailed", "Could not add this dish. Try again.");
327
+ return derive({ ...latestState(), orderMessage });
328
+ } finally {
329
+ adding = false;
263
330
  }
264
- return derive({ ...state, basketMessage: copyParam("menuItem.basketAdded", "Added to your basket.") });
265
331
  },
266
332
  },
267
333
  });
268
334
 
335
+ latestState = () => svc.getState() as State;
269
336
  export { svc };
@@ -17,6 +17,9 @@ import { copyParam, stashCopy } from "../_shared/himi-copy.js";
17
17
  /** One section with its shaped dishes (the source of truth `all` keeps). */
18
18
  interface MenuSection {
19
19
  id: string;
20
+ /** The menu that lists this section ("" for the flat fallback) — the dish detail orders
21
+ * the dish under this menu + section, which is what Wix checkout validates. */
22
+ menuId: string;
20
23
  name: string;
21
24
  items: DishCard[];
22
25
  }
@@ -47,6 +50,9 @@ interface MenuRow {
47
50
  noLabel2: boolean;
48
51
  noLabel3: boolean;
49
52
  detailScreenId: string;
53
+ /** Where the dish is shown — passed to the detail as nav params ("" on header rows). */
54
+ menuId: string;
55
+ sectionId: string;
50
56
  }
51
57
 
52
58
  interface State {
@@ -87,10 +93,12 @@ function headerRow(section: MenuSection): MenuRow {
87
93
  noLabel2: true,
88
94
  noLabel3: true,
89
95
  detailScreenId: "",
96
+ menuId: "",
97
+ sectionId: "",
90
98
  };
91
99
  }
92
100
 
93
- function itemRow(item: DishCard): MenuRow {
101
+ function itemRow(item: DishCard, section: MenuSection): MenuRow {
94
102
  return {
95
103
  id: item.id,
96
104
  isHeader: false,
@@ -112,6 +120,8 @@ function itemRow(item: DishCard): MenuRow {
112
120
  noLabel2: item.noLabel2,
113
121
  noLabel3: item.noLabel3,
114
122
  detailScreenId: item.detailScreenId,
123
+ menuId: section.menuId,
124
+ sectionId: section.id === "all" ? "" : section.id,
115
125
  };
116
126
  }
117
127
 
@@ -128,7 +138,7 @@ function apply(all: MenuSection[], query: string): MenuRow[] {
128
138
  }))
129
139
  .filter((section) => section.items.length > 0)
130
140
  : all;
131
- return filtered.flatMap((section) => [headerRow(section), ...section.items.map(itemRow)]);
141
+ return filtered.flatMap((section) => [headerRow(section), ...section.items.map((item) => itemRow(item, section))]);
132
142
  }
133
143
 
134
144
  function derive(state: State): State {
@@ -163,9 +173,10 @@ export function composeSections(menus: WixMenu[], sections: WixSection[], dishes
163
173
  const sectionById = new Map(sections.filter((section) => section.id).map((section) => [section.id, section]));
164
174
  const orderedIds: string[] = [];
165
175
  const seen = new Set<string>();
176
+ const menuOf = new Map<string, string>();
166
177
  for (const menu of menus.filter((candidate) => candidate.visible !== false)) {
167
178
  for (const sectionId of menu.sectionIds ?? []) {
168
- if (sectionId && !seen.has(sectionId)) { seen.add(sectionId); orderedIds.push(sectionId); }
179
+ if (sectionId && !seen.has(sectionId)) { seen.add(sectionId); orderedIds.push(sectionId); menuOf.set(sectionId, menu.id ?? ""); }
169
180
  }
170
181
  }
171
182
  // Menus query failed / carried no section links → every section, in order.
@@ -182,11 +193,11 @@ export function composeSections(menus: WixMenu[], sections: WixSection[], dishes
182
193
  return dish ? [dish] : [];
183
194
  });
184
195
  if (items.length === 0) return [];
185
- return [{ id: section.id, name: cleanTitle(section.name) || copyParam("menu.sectionFallbackName", "Menu"), items }];
196
+ return [{ id: section.id, menuId: menuOf.get(section.id) ?? "", name: cleanTitle(section.name) || copyParam("menu.sectionFallbackName", "Menu"), items }];
186
197
  });
187
198
  if (composed.length > 0) return composed;
188
199
  // Sections unavailable (or none matched a dish) → flat single-section menu.
189
- return dishes.length ? [{ id: "all", name: copyParam("menu.sectionFallbackName", "Menu"), items: dishes }] : [];
200
+ return dishes.length ? [{ id: "all", menuId: "", name: copyParam("menu.sectionFallbackName", "Menu"), items: dishes }] : [];
190
201
  }
191
202
 
192
203
  function strOf(arg: unknown): string {
@@ -7,7 +7,7 @@ import { withOnlineWrites } from "../_shared/online-actions.js";
7
7
  // INTERACTIVE: tapping an upcoming reservation cancels it — optimistic + best-effort
8
8
  // cancelReservation (no-op on sample rows).
9
9
  //
10
- // RFC 0052 consumer: the list is a `himalaya.Agenda` — chronological, grouped
10
+ // The list is a `himalaya.Agenda` — chronological, grouped
11
11
  // by LOCAL day under sticky headers. A reservation list is a diary, and the
12
12
  // flat grid it replaced repeated "Fri, Aug 8 · 7:30 PM" on every card because
13
13
  // no row knew what day the row above it was on. `Agenda` owns the day headers
@@ -1,24 +1,86 @@
1
- // Tier-5 bundle: Wix Spaces clone — explicit secure-site handoff.
2
- // The owner-capable Restaurant Orders API must never be queried from a member device.
1
+ // Tier-5 bundle: one placed restaurant order for the signed-in member.
2
+ // Read through the member eCom orders search (filtered to the member's email and the order id),
3
+ // the same path as the order list — never the owner-capable Restaurants Orders API. An order that
4
+ // is not this member's, or has no Restaurants line, is "not found".
3
5
  import { defineService } from "@himalaya/state";
6
+ import { searchOrders, type WixOrder } from "@himalaya/wix-ecom";
4
7
  import { presence, detailInputId, type PresenceFlags } from "../_shared/detail-presence.js";
8
+ import { assertOrderMember, orderAccessDenied, orderMember } from "../_shared/member-orders.js";
9
+ import { RESTAURANTS_ORDERS_APP_ID } from "../_shared/restaurant-ordering.js";
10
+ import { formatDateMedium } from "../_shared/engine-intl.js";
11
+ import { copyParam, copyTemplate, stashCopy } from "../_shared/himi-copy.js";
5
12
 
6
13
  interface S extends PresenceFlags { status: "loading" | "ready"; name: string; status_: string; total: string; items: string; message: string }
7
14
 
8
- const svc = defineService<S, { load: (s: S) => Promise<S> }>("spacesFoodOrderDetail", {
9
- state: { status: "loading", name: "", status_: "", total: "", items: "", message: "", ...presence("loading") },
10
- actions: {
11
- // No order id → the shared not-found state; an id → the secure-site handoff.
12
- load: async () => (!detailInputId() ? { status: "ready", name: "", status_: "", total: "", items: "", message: "", ...presence("missing") } : {
13
- ...presence("found"),
15
+ const EMPTY = { name: "", status_: "", total: "", items: "", message: "" };
16
+
17
+ function statusLine(order: WixOrder): string {
18
+ const raw = order as WixOrder & { status?: string };
19
+ const status = raw.status === "CANCELED" ? copyParam("foodOrders.statusCanceled", "Canceled")
20
+ : order.fulfillmentStatus === "FULFILLED" ? copyParam("foodOrders.statusCompleted", "Completed")
21
+ : order.paymentStatus === "NOT_PAID" || order.paymentStatus === "PENDING" ? copyParam("foodOrders.statusAwaitingPayment", "Awaiting payment")
22
+ : copyParam("foodOrders.statusReceived", "Received");
23
+ const created = order.createdDate ? new Date(order.createdDate) : undefined;
24
+ return [status, created && !Number.isNaN(created.getTime()) ? formatDateMedium(created) : ""].filter(Boolean).join(" · ");
25
+ }
26
+
27
+ function fulfillmentLine(order: WixOrder): string {
28
+ const title = (order as WixOrder & { shippingInfo?: { title?: string } }).shippingInfo?.title ?? "";
29
+ return title ? copyTemplate("foodOrderDetail.fulfillment", "Fulfillment: {method}", { method: title }) : "";
30
+ }
31
+
32
+ /** Bumped by every load. The module-scope load and the screen's on-appear load overlap: only the
33
+ * newest commits, so an older, slower answer never replaces a newer one. */
34
+ let loadSeq = 0;
35
+ /** The service's live state (set once the service exists). */
36
+ let latestState: () => S = () => ({ status: "loading", ...EMPTY, ...presence("loading") });
37
+
38
+ async function read(id: string): Promise<S> {
39
+ const member = await orderMember();
40
+ if (!member.email) {
41
+ return { status: "ready", ...EMPTY, ...presence("found"),
42
+ message: copyParam("foodOrderDetail.signIn", "Sign in to see this order.") };
43
+ }
44
+ try {
45
+ const response = await searchOrders({ search: {
46
+ filter: { "buyerInfo.email": { $eq: member.email }, id: { $eq: id } },
47
+ cursorPaging: { limit: 1 },
48
+ } });
49
+ await assertOrderMember(member);
50
+ const order = (response.orders ?? []).find((candidate) => candidate.id === id
51
+ && (candidate.buyerInfo?.email ?? "").trim().toLowerCase() === member.email
52
+ && (candidate.lineItems ?? []).some((line) =>
53
+ (line as { catalogReference?: { appId?: string } }).catalogReference?.appId === RESTAURANTS_ORDERS_APP_ID));
54
+ if (!order) return { status: "ready", ...EMPTY, ...presence("missing") };
55
+ return {
14
56
  status: "ready",
15
- name: "",
16
- status_: "",
17
- total: "",
18
- items: "",
19
- message: "Live order details require a secure connection configured by this business.",
20
- }),
57
+ ...presence("found"),
58
+ name: copyTemplate("foodOrders.orderTitle", "Order #{number}", { number: order.number || order.id }),
59
+ status_: statusLine(order),
60
+ total: order.priceSummary?.total?.formattedAmount ?? "",
61
+ items: (order.lineItems ?? [])
62
+ .map((line) => `${line.quantity || 1} × ${line.productName?.translated || line.productName?.original || ""}`.trim())
63
+ .join("\n"),
64
+ message: fulfillmentLine(order),
65
+ };
66
+ } catch (error) {
67
+ return { status: "ready", ...EMPTY, ...presence(orderAccessDenied(error) ? "missing" : "failed") };
68
+ }
69
+ }
70
+
71
+ const svc = defineService<S, { load: (s: S, arg?: unknown) => Promise<S> }>("spacesFoodOrderDetail", {
72
+ state: { status: "loading", ...EMPTY, ...presence("loading") },
73
+ actions: {
74
+ load: async (_state, arg) => {
75
+ stashCopy(arg);
76
+ const id = detailInputId();
77
+ if (!id) return { status: "ready", ...EMPTY, ...presence("missing") };
78
+ const seq = ++loadSeq;
79
+ const next = await read(id);
80
+ return seq === loadSeq ? next : latestState();
81
+ },
21
82
  },
22
83
  });
84
+ latestState = () => svc.getState() as S;
23
85
  svc.load();
24
86
  export { svc };