@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.
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/bookings-v2/src/index.d.ts +24 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/ecom/src/extensions.d.ts +144 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/restaurants/src/extensions.d.ts +93 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap-checkout.mjs +27 -5
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap.mjs +19 -3
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +27 -5
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-ecom.mjs +58 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-restaurants.mjs +23 -0
- package/dist/cli.mjs +3110 -1993
- package/dist/preview-assets.generated.bin +0 -0
- package/fixtures/wix-canned.json +83 -0
- package/guides/hhp.md +39 -0
- package/guides/himi-json-output.md +22 -1
- package/guides/release-notes.md +17 -0
- package/guides/templates.md +11 -1
- package/guides/tier-5-add-a-screen.md +4 -0
- package/package.json +2 -2
- package/templates/beauty-salon/config.ts +12 -1
- package/templates/beauty-salon/dev/index.ts +1767 -2698
- package/templates/beauty-salon/tier5-src/_shared/collection-service.ts +23 -5
- package/templates/beauty-salon/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/beauty-salon/tier5-src/_shared/himi-restaurants.ts +12 -5
- package/templates/beauty-salon/tier5-src/_shared/member-bookings.ts +50 -7
- package/templates/beauty-salon/tier5-src/_shared/restaurant-ordering.ts +201 -0
- package/templates/beauty-salon/tier5-src/himi-booking-bookings/index.ts +385 -74
- package/templates/beauty-salon/tier5-src/himi-branded-lite-cart/index.ts +6 -0
- package/templates/beauty-salon-owner/tier5-src/_shared/collection-service.ts +23 -5
- package/templates/beauty-salon-owner/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/branded-lite/config.ts +12 -1
- package/templates/branded-lite/dev/index.ts +4955 -4801
- package/templates/branded-lite/tier5-src/_shared/collection-service.ts +23 -5
- package/templates/branded-lite/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/branded-lite/tier5-src/_shared/himi-restaurants.ts +12 -5
- package/templates/branded-lite/tier5-src/_shared/member-bookings.ts +50 -7
- package/templates/branded-lite/tier5-src/_shared/restaurant-ordering.ts +201 -0
- package/templates/branded-lite/tier5-src/himi-booking-bookings/index.ts +385 -74
- package/templates/branded-lite/tier5-src/himi-branded-lite-cart/index.ts +6 -0
- package/templates/branded-lite/tier5-src/himi-branded-lite-menu-item/index.ts +144 -77
- package/templates/branded-lite/tier5-src/himi-branded-lite-menus/index.ts +16 -5
- package/templates/branded-lite/tier5-src/himi-branded-lite-reservations/index.ts +1 -1
- package/templates/branded-lite/tier5-src/wix-spaces-food-order-detail/index.ts +76 -14
- package/templates/branded-lite/tier5-src/wix-spaces-food-orders/index.ts +807 -55
- package/templates/community-club/config.ts +12 -1
- package/templates/community-club/tier5-src/_shared/collection-service.ts +23 -5
- package/templates/community-club/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/community-club/tier5-src/_shared/himi-restaurants.ts +12 -5
- package/templates/community-club/tier5-src/_shared/member-bookings.ts +50 -7
- package/templates/community-club-owner/tier5-src/_shared/collection-service.ts +23 -5
- package/templates/community-club-owner/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/events-meetups/config.ts +12 -1
- package/templates/events-meetups/tier5-src/_shared/collection-service.ts +23 -5
- package/templates/events-meetups/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/events-meetups/tier5-src/_shared/himi-restaurants.ts +12 -5
- package/templates/events-meetups/tier5-src/_shared/member-bookings.ts +50 -7
- package/templates/events-meetups-owner/tier5-src/_shared/collection-service.ts +23 -5
- package/templates/events-meetups-owner/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/gym-fitness/config.ts +12 -1
- package/templates/gym-fitness/dev/index.ts +1753 -2697
- package/templates/gym-fitness/tier5-src/_shared/collection-service.ts +23 -5
- package/templates/gym-fitness/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/gym-fitness/tier5-src/_shared/himi-restaurants.ts +12 -5
- package/templates/gym-fitness/tier5-src/_shared/member-bookings.ts +50 -7
- package/templates/gym-fitness/tier5-src/_shared/restaurant-ordering.ts +201 -0
- package/templates/gym-fitness/tier5-src/himi-booking-bookings/index.ts +385 -74
- package/templates/gym-fitness/tier5-src/himi-branded-lite-cart/index.ts +6 -0
- package/templates/gym-fitness-owner/tier5-src/_shared/collection-service.ts +23 -5
- package/templates/gym-fitness-owner/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/nonprofit-cause/config.ts +12 -1
- package/templates/nonprofit-cause/tier5-src/_shared/collection-service.ts +23 -5
- package/templates/nonprofit-cause/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/nonprofit-cause/tier5-src/_shared/himi-restaurants.ts +12 -5
- package/templates/nonprofit-cause/tier5-src/_shared/member-bookings.ts +50 -7
- package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-service.ts +23 -5
- package/templates/nonprofit-cause-owner/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/owner-lite/tier5-src/_shared/collection-service.ts +23 -5
- package/templates/owner-lite/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/podcast-creator/config.ts +12 -1
- package/templates/podcast-creator/tier5-src/_shared/collection-service.ts +23 -5
- package/templates/podcast-creator/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/podcast-creator/tier5-src/_shared/himi-restaurants.ts +12 -5
- package/templates/podcast-creator/tier5-src/_shared/member-bookings.ts +50 -7
- package/templates/podcast-creator-owner/tier5-src/_shared/collection-service.ts +23 -5
- package/templates/podcast-creator-owner/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/portfolio-creative/config.ts +12 -1
- package/templates/portfolio-creative/tier5-src/_shared/collection-service.ts +23 -5
- package/templates/portfolio-creative/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/portfolio-creative/tier5-src/_shared/himi-restaurants.ts +12 -5
- package/templates/portfolio-creative/tier5-src/_shared/member-bookings.ts +50 -7
- package/templates/portfolio-creative/tier5-src/_shared/restaurant-ordering.ts +201 -0
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-cart/index.ts +6 -0
- package/templates/portfolio-creative-owner/tier5-src/_shared/collection-service.ts +23 -5
- package/templates/portfolio-creative-owner/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/restaurant/README.md +1 -1
- package/templates/restaurant/config.ts +12 -1
- package/templates/restaurant/dev/index.ts +1373 -275
- package/templates/restaurant/himalaya.template.json +2 -1
- package/templates/restaurant/tier5-src/_shared/collection-service.ts +23 -5
- package/templates/restaurant/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/restaurant/tier5-src/_shared/himi-restaurants.ts +12 -5
- package/templates/restaurant/tier5-src/_shared/member-bookings.ts +50 -7
- package/templates/restaurant/tier5-src/_shared/member-orders.ts +8 -0
- package/templates/restaurant/tier5-src/_shared/restaurant-ordering.ts +201 -0
- package/templates/restaurant/tier5-src/himi-branded-lite-menu-item/index.ts +144 -77
- package/templates/restaurant/tier5-src/himi-branded-lite-menus/index.ts +16 -5
- package/templates/restaurant/tier5-src/wix-spaces-food-order-detail/index.ts +76 -14
- package/templates/restaurant/tier5-src/wix-spaces-food-orders/index.ts +807 -55
- package/templates/restaurant-owner/tier5-src/_shared/collection-service.ts +23 -5
- package/templates/restaurant-owner/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/retail-store/config.ts +12 -1
- package/templates/retail-store/tier5-src/_shared/collection-service.ts +23 -5
- package/templates/retail-store/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/retail-store/tier5-src/_shared/himi-restaurants.ts +12 -5
- package/templates/retail-store/tier5-src/_shared/member-bookings.ts +50 -7
- package/templates/retail-store/tier5-src/_shared/restaurant-ordering.ts +201 -0
- package/templates/retail-store/tier5-src/himi-branded-lite-cart/index.ts +6 -0
- package/templates/retail-store-owner/tier5-src/_shared/collection-service.ts +23 -5
- package/templates/retail-store-owner/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/yoga-studio/config.ts +12 -1
- package/templates/yoga-studio/dev/index.ts +1755 -2699
- package/templates/yoga-studio/tier5-src/_shared/collection-service.ts +23 -5
- package/templates/yoga-studio/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/yoga-studio/tier5-src/_shared/himi-restaurants.ts +12 -5
- package/templates/yoga-studio/tier5-src/_shared/member-bookings.ts +50 -7
- package/templates/yoga-studio/tier5-src/himi-booking-bookings/index.ts +385 -74
- package/templates/yoga-studio-owner/tier5-src/_shared/collection-service.ts +23 -5
- 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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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 = (
|
|
85
|
-
if (input
|
|
86
|
-
return input
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
:
|
|
115
|
-
|
|
116
|
-
|
|
126
|
+
: "",
|
|
127
|
+
hasCart: state.cartCount > 0,
|
|
128
|
+
noCart: state.cartCount === 0,
|
|
117
129
|
};
|
|
118
130
|
}
|
|
119
131
|
|
|
120
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
302
|
+
orderMessage: "",
|
|
238
303
|
});
|
|
239
304
|
},
|
|
240
|
-
|
|
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 (
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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 {
|
|
@@ -1,24 +1,86 @@
|
|
|
1
|
-
// Tier-5 bundle:
|
|
2
|
-
//
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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 };
|